Theme Framework How the LeaseLeads theme frontend works

Components

The reusable pieces a Module is built from — view-only and behavioural, and how a host finds them.

On this page 6

A Component is a reusable piece rendered inside a Module (or another Component) via View::component(). Unlike a Module, an author never places it directly — it only ever appears because a host chose to render it. Buttons, images, content stacks, navigation, borders: these are Components.

Reach for a Component when the same markup shows up in more than one Module, or when a piece of a Module carries its own behaviour and deserves its own file. Promoting markup to a Component is how you stop copying a <figure> between six views.

Two kinds

A Component is one of two things, and the difference is does it have a JS file.

View-only

Most Components are just a PHP partial. text and button render markup from field data and stop there — there is no asset, no selector, no lifecycle.

php
// resources/public/views/components/text.php
$data = Settings::parseArgs($args['data'], Text::defaults());
$text_class = TextAppearance::cssClass($data);
?>
<<?= $data['tag']; ?>
  class="font-style <?= $text_class; ?>"
  style="--text-color: <?= ColorSelect::getCssColor($data['color']); ?>;">
  <?php echo Text::doText($data['content']); ?>
</<?= $data['tag']; ?>>

A view-only Component still uses the full Field machinery — it emits utility classes and CSS custom properties the SCSS reads. It just has no runtime behaviour, so it needs no JavaScript.

Note

A Component without JS is the common case. The taxonomy defines a Component by authorial placeability (none), not by whether it ships behaviour. Keep the partial pure: parse args, emit markup, defer all styling to CSS variables.

View + behaviour

Some Components need runtime behaviour. Image lazy-loads its real source when it scrolls into view and flags the wrapper loaded once decoded. It has both a view and an asset:

js
// resources/public/assets/components/Image/Image.js
class Image extends Instantiable {
  static selector = 'div.image'

  get isLazy() {
    return this.element.classList.contains('image--lazyload')
  }

  #bindEvents() {
    if ( ! this.isLazy ) return

    ;(new GlobalIntersectionObserver(this.element, () => {
      manipulateDOM(() => this.load())
    })).observe()
  }

  async init() {
    if ( this.isInitialized ) return
    this.#bindEvents()
    await super.init()
  }
}

const Final = new Traits(Image, [])

export { Final as Image }

A behavioural Component is an Instantiable like any Module — a static selector, a lifecycle, colocated SCSS. Two things set it apart:

  • It's isInitialized-guarded and it does not mix AutoInitializes. A Component never self-inits. It waits for its host to init it, so the host keeps ordering control. This is the sub-component contract.
  • It's discovered by a host's HasX trait, not a global loader entry. You won't find Image in Deferred.js. It has no processor entry of its own.
Important

The isInitialized guard is not optional for a behavioural Component. Its host may init it during hand-ordered work and again through autoInitializeComponents(); the guard makes the second call a no-op. Without it, the Component double-binds.

How a host discovers a behavioural Component

A behavioural Component is loaded by the HasX trait its host mixes in. The host declares HasImage; the trait gives it an imageComponents getter (which querySelectorAlls the host's subtree) and a bootImageComponents() that queues the Image asset — but only if a div.image is actually present.

js
// the host declares the capability
const Final = new Traits(Intro, [ /* … */ HasImage, HasButtons ])

So the rule is a two-sided contract: the view renders View::component('image', …), and the host's asset must mix the matching HasImage. Miss the trait and the image renders as static markup that never wires up its lazy-loading.

Note

Why HasX and not a global entry. A global loader entry (like a Module's) boots a component the moment its selector appears anywhere on the page — independently. A Component must instead boot under the control of its host, so its host can order init relative to everything else in its subtree. HasX is what hands that control to the host. See Sub-components & HasX.

When to promote markup to a Component

  • The same markup appears in two or more Module views → extract it.
  • A piece of a Module needs its own behaviour (lazy-load, a toggle, a carousel) → give it a selector and an asset.
  • The piece has editor-configurable style → back it with a Field so its options travel as CSS custom properties.

If none of those hold, leave it as markup in the Module view. Not every <div> needs to be a Component.

Note

Novaa-specific: the border Component. The notched-border overlay (.notched-border, loaded by HasBorder) is a Novaa design detail, not framework canon. Treat it as an example of the pattern, not a piece every theme ships.