Skip to content
DevinimJS
Phase 1 · Lesson 7

Outlet & Composition — Preserving Light DOM Children

Let PHP or static HTML provide content inside your components. Use this.outlet and prepare() to blend server content with reactive behavior.

What you'll learn
  • How DevinimJS uses Light DOM (not Shadow DOM) for natural CSS styling
  • Use this.outlet to render consumer-provided children at a specific position
  • Inspect children before render with prepare(fragment)
  • Compose PHP-rendered content with reactive component behavior

Light DOM by design

DevinimJS components do not use Shadow DOM. This means:

  • Global CSS (CKCSS, site styles) reaches component markup directly
  • No CSS encapsulation barriers — components inherit the page's look
  • Children placed inside the component tag in HTML are real DOM children
<!-- These children exist in the Light DOM -->
<dv-card>
  <h2>Product Name</h2>
  <p>Description from the server</p>
</dv-card>

What is this.outlet?

this.outlet is an HtmlString marker. Place ${this.outlet} in your template where you want the consumer-provided Light DOM children to appear:

class DvCard extends BaseComponent {
  template() {
    return html`
      <div class="card-wrapper">
        <header>${this.str('title', 'Card')}</header>
        <div class="card-content">
          ${this.outlet}  <!-- Children go here -->
        </div>
      </div>
    `;
  }
}
define('dv-card', DvCard);

When rendered, ${this.outlet} becomes <dv-outlet> with display: contents. The children are moved inside it, but the <dv-outlet> wrapper is transparent in layout.

Example: Card with server content

<!-- HTML provided by PHP -->
<dv-card data-title="Pricing">
  <table>
    <tr><td>Basic</td><td>$9/mo</td></tr>
    <tr><td>Pro</td><td>$29/mo</td></tr>
  </table>
</dv-card>

<!-- The component renders: -->
<!-- <div class="card-wrapper">
       <header>Pricing</header>
       <div class="card-content">
         <dv-outlet style="display:contents">
           <table>...</table>
         </dv-outlet>
       </div>
     </div> -->

prepare(fragment) — inspect children before render

Sometimes you need to know what children were provided before rendering. Use prepare():

class DvTabs extends BaseComponent {
  prepare(fragment) {
    // fragment is a DocumentFragment of Light DOM children
    const panels = fragment.querySelectorAll('[data-tab]');
    this._tabCount = panels.length;
  }

  template() {
    return html`
      <div class="tabs">
        <span>${this._tabCount} tabs</span>
        ${this.outlet}
      </div>
    `;
  }
}

Live demo: Composition in action

Live demo — panel with server-like content
  • Item 1: Widget — $9.99
  • Item 2: Gadget — $24.99
  • Item 3: Doodad — $4.99

Total: $39.97

Outlet and morph

The <dv-outlet> element is excluded from the DOM morph engine. When your component re-renders due to a state change, the children inside the outlet are not touched:

template() {
  return html`
    <div>
      <span>Status: ${this.state.loading ? 'Loading...' : 'Ready'}</span>
      ${this.outlet}  <!-- Children never re-render -->
    </div>
  `;
}

This is critical: server-rendered children (PHP loops, CMS content) survive component re-renders.

PHP integration pattern

The outlet pattern shines when combined with server-rendered content:

<!-- PHP/Laravel Blade -->
<dv-tabs data-label="Product details">
  @foreach ($sections as $section)
    <section data-tab="{{ $section->title }}">
      {!! $section->content !!}
    </section>
  @endforeach
</dv-tabs>

The PHP template renders the children. DevinimJS adds tab navigation behavior around them. Each owns its domain.

Recap

Light DOM first

No Shadow DOM. CSS reaches component markup naturally.

this.outlet

Place ${this.outlet} where Light DOM children belong.

prepare(fragment)

Inspect children before the first render.

Outlet survives morph

Server content inside the outlet is not touched during re-renders.