Theme Framework How the LeaseLeads theme frontend works

Conventions & standards

The small rules every file follows — a checklist a reviewer can scan.

On this page 3

None of these are enforced by tooling. They're the shape every file already has, and the reason the machinery in A page's life works without manual wiring. Break one and the failure is usually silent — the component just never loads. Use this page as a review checklist.

The shape of a component

Here is the whole Border Component — small enough to see every rule at once. It's the reference theme's notched-border (Novaa-only), but the structure is universal:

js
import './Border.scss'
import { Instantiable } from '../../utilities/Instantiable/Instantiable.js'
import { Traits } from '../../utilities/Traits/Traits.js'

class Border extends Instantiable {
  static selector = '.notched-border'
}

const Final = new Traits(Border, [])

export { Final as Border }

The checklist

Colocate the three halves. A component is a view (views/{type}/name.php, kebab-case) plus an asset folder (assets/{type}/Name/, PascalCase) holding exactly its Name.js and Name.scss. Nothing else lives in that folder; that folder holds nothing else.

Import the SCSS as a side-effect, first, at the top of the JS.

js
import './Border.scss'

This is how a component's styles enter the bundle — the import() that loads its JS pulls the CSS along with it. No stylesheet is registered anywhere else. If the JS doesn't import the SCSS, the styles never ship.

Extend Instantiable. Every element-backed behaviour extends it. It gives you the element → instance registry, data-* parsing into options, and the lifecycle. A component with no behaviour still extends it (a CSS-only component) purely so its JS import can pull the SCSS.

Declare a static selector. This string is how the AssetLoader and every host find the element. It is the entire presence gate — no selector, no loading.

js
static selector = '.notched-border'

Apply Traits once, at the end, and export the result. The last two lines of every file are the same shape — compose the capabilities into the class, then export it:

js
const Final = new Traits(Class, [ /* AutoInitializes, HasImage, FadesIn, … */ ])
export { Final as Class }

Add a capability by editing the array, never by subclassing. An empty array ([]) is fine for a component with no traits.

Important

The export alias must equal the registered name. export { Final as Border } works because the loader descriptor says name: 'Border' and resolution does exports['Border']. Rename one without the other and resolution throws at load. See The Globals registry for the name/moniker rules.

Wire behaviour in async init(), and make it idempotent. init() is where real interactions bind — it runs after everything you declared in boot has loaded. Hosts call autoInitializeComponents() here to init their sub-components in order, then await super.init(). The lifecycle guards init() with isInitialized, so it's safe to call more than once.

js
async init () {
  await this.autoInitializeComponents()
  await super.init()
}

Never define styles in JS. JavaScript toggles state classesis-open, is-animated, is-settled, loaded — and the colocated SCSS owns what each one looks like. No inline styles, no style objects, no CSS strings in JS.

Tip

"No styles in JS" is not "no JS." Writing behaviour is the job. The line is narrow: JS flips a class; SCSS decides what the class means. If you find yourself setting element.style, the rule you want is probably a state class plus a SCSS block.

Let a Field emit editor styles. Editor choices for colour, spacing, and borders reach the frontend as CSS custom properties and utility classes emitted by PHP Field classes — never as hard-coded values in your SCSS or JS. Your SCSS consumes those variables. See The Backend Bridge.