Skip to content

Components ​

Single-file component (SFC) ​

A component is a .dust file that contains a <script> block followed by the template. The script returns a bare object — no class, no import, no manual registration.

dust
{! views/components/ProductList.dust !}
<script>
({
  props: {
    products: [],
    title: 'Products'
  },

  state: {
    filter: ''
  },

  get filteredProducts() {
    const q = this.state.filter.toLowerCase();
    return this.props.products.filter(p => p.name.toLowerCase().includes(q));
  },

  onFilter(e) {
    this.state.filter = e.target.value;
  },

  async onDelete(e) {
    const id = Number(e.target.dataset.id);
    await fetch(`/api/products/${id}`, { method: 'DELETE' });
    this.props.products = this.props.products.filter(p => p.id !== id);
  }
})
</script>

<div>
  <h2>{title}</h2>
  <input type="text" on:input="onFilter" value="{filter}" placeholder="Filter…">

  <ul>
    {#filteredProducts}
      <li>
        {.name} — ${.price}
        <button on:click="onDelete" data-id="{.id}">Delete</button>
      </li>
    {/filteredProducts}
  </ul>
</div>

Definition object ​

The <script> block returns a single object literal. Its keys are picked up as follows:

KeyTypePurpose
propsObjectDefault values for props. Overridden by caller-passed values from {@component} or data-props.
stateObjectInitial reactive state. Cloned per instance.
get xyz()GetterComputed value with auto-tracked dependencies, available as {xyz} in the template.
methodName()FunctionEvent handler (called by on:click="methodName") or any custom method.

Inside getters and methods, this.props, this.state, and this.<getter> are all available. Mutating state or props triggers a re-render — see Reactivity.

Template context ​

The template receives a flat merge of { ...props, ...state, ...derivedGetters }. So in the example above, {title} resolves to props.title, {filter} to state.filter, and {filteredProducts} to the getter result.

Rendering a component ​

Use the {@component} Dust helper:

dust
{@component "components/ProductList" products title="Soldes" /}

This renders the component server-side and emits a hydration-ready wrapper:

html
<div data-component="components/ProductList">
  <script type="application/json" data-igo-props>…serialized props…</script>
  <!-- fully rendered HTML -->
</div>

Caller params (products=…, title=…) override the corresponding props defaults from the definition.

Lifecycle ​

constructor(element)
    ↓
load template, set up form binding
    ↓
init()                          ← once, before the first render
    ↓
render() ←──────────────────┐
    ↓                        │
compute getters              │
    ↓                        │
dust.render(template, ctx)    │
    ↓                        │
morphdom reconcile            │
    ↓                        │
sync child props              │
    ↓                        │
sync delegated events         │
    ↓                        │
mount child components        │
    ↓                        │
afterRender()                 │
    ↓                        │
[state mutation] ─────────────┘

Hooks ​

Add any of these as methods in the definition object:

js
({
  async init()        { /* once, before the first render — props/state/store ready */ },
  async afterRender() { /* after each render — DOM is updated */ },
  async onError(err)  { /* if render throws */ }
})

init is the place for one-time setup — seeding the store from props, fetching initial data — with no need for a _seeded guard. afterRender is the right place for focus management, scroll restoration, or initialising third-party libs that need a stable DOM.

Child components ​

Components can be nested. Each [data-component] element becomes its own component instance with isolated state.

Inline:

dust
{! Parent template !}
<div>
  <h1>Dashboard</h1>
  {@component "components/ProductList" products /}
  {@component "components/CartSummary" cart /}
</div>

Each child component manages its own state, getters and events. When the parent re-renders, child DOM nodes are preserved (not recreated). Their data-props are re-evaluated and the child re-renders if its props changed.

Stable identity in dynamic lists ​

When you render a list of components, give each one a key= param. The helper writes it as data-component-key; the runtime uses that to match instances across re-renders.

dust
{#products}
  {@component "components/ProductCard" product=. key=.id /}
{/products}

Without an explicit key=, the component name is used — fine for one-off mounts but ambiguous for repeated children.

Class-based components (advanced) ​

For cases where you need lifecycle methods that don't fit the definition object — heavy custom logic, dynamic component classes, etc. — you can also extend IgoComponent directly:

js
const { IgoComponent } = require('@igojs/component/client');

class Counter extends IgoComponent {
  constructor(element) {
    super(element, 'components/Counter');
  }

  get events() {
    return [
      { selector: '.increment', eventType: 'click', handler: this.onIncrement }
    ];
  }

  onIncrement() {
    this.state.count = (this.state.count || 0) + 1;
  }
}

// Register at startup
start({ components: { 'components/Counter': Counter } });

This pre-6.0 pattern is still supported. Most apps should reach for SFC first — the on: event syntax replaces the events getter, and the definition object handles the boilerplate.

API reference ​

Instance properties ​

PropertyDescription
elementThe DOM element this component is mounted on
propsReactive Proxy over the props (read + mutate, triggers render)
stateDeep reactive Proxy over state
rawStatePlain state object — access without triggering renders

Lifecycle methods ​

MethodDescription
init()Hook called once, after the constructor and before the first render — one-time setup
render()Render the component (called automatically on state mutation)
afterRender()Render hook (after every render)
onError(error)Hook for render errors
destroy()Cleanup: cancel pending renders, unbind events, clear caches

Released under the ISC license.