Standalone Components
Vue-style Custom Elements with reactive data, computed properties, lifecycle hooks, and surgical DOM binding — built on Web Components with Shadow DOM.

What are Domma Components?

Domma Components unify the framework's primitives into a single declarative definition: a reactive Model for state, a Mustache template for markup, computed properties for derived values, and lifecycle hooks for side effects. Each component is a real Custom Element backed by Shadow DOM.

Shadow DOM

True style encapsulation — component CSS never leaks out, theme variables still flow in.

Surgical Updates

Simple {{field}} bindings update only that text node. Block changes ({{#if}}) trigger targeted re-renders.

Zero Build Step

JS object definition works at runtime. Optional .domma SFC format for build pipelines.

Example 1 — Counter reactive · computed · lifecycle

Reactive count field, a doubled computed property, methods, and lifecycle logging. The count display updates surgically (only that text node changes); the colour badge re-renders when the condition flips.

<counter-demo>

Lifecycle Log

Waiting for events…

counter-demo.js
Domma.component('counter-demo', {

  // Inline template (use templateUrl for real projects)
  template: `
    <div class="counter-wrap">
      <p class="count">Count: {{count}}</p>
      <p class="doubled">Doubled: {{doubled}}</p>
      {{#if high}}
        <span class="badge-high">High!</span>
      {{/if}}
      <div class="actions">
        <button class="dm-inc">+</button>
        <button class="dm-dec">−</button>
        <button class="dm-rst">Reset</button>
      </div>
    </div>
  `,

  data() { return { count: 0 }; },

  computed: {
    doubled() { return this.data.count * 2; },
    high()    { return this.data.count >= 10; }
  },

  methods: {
    increment() { this.set({ count: this.data.count + 1 }); },
    decrement() { this.set({ count: this.data.count - 1 }); },
    reset()     { this.set({ count: 0 }); }
  },

  onBeforeMount() { log('beforeMount'); },
  onMount() {
    log('mount');
    // $() can't pierce Shadow DOM — wrap root.querySelector() in $()
    $(this.root.querySelector('.dm-inc')).on('click', () => this.increment());
    $(this.root.querySelector('.dm-dec')).on('click', () => this.decrement());
    $(this.root.querySelector('.dm-rst')).on('click', () => this.reset());
  },
  onUpdated() { log('updated — count=' + this.data.count); },
  onUnmount() { log('unmount'); },

  style: `
    .counter-wrap { text-align: center; padding: 1.5rem; }
    .count   { font-size: 1.5rem; font-weight: 700; margin: 0 0 .25rem; }
    .doubled { color: #6b7280; font-size: 0.9rem; margin: 0 0 .75rem; }
    .badge-high { display: inline-block; background: #dc2626; color: #fff;
                  padding: .2em .6em; border-radius: 9999px;
                  font-size: .75rem; font-weight: 600; margin-bottom: .75rem; }
    .actions { display: flex; gap: .5rem; justify-content: center; }
    button { padding: .4rem 1rem; border: none; border-radius: 6px;
             background: var(--dm-primary, #6495ED); color: #fff;
             cursor: pointer; font-size: 1rem; }
    button:last-child { background: #6b7280; }
  `
});
Example 2 — User Card with Props props · async · conditional

Demonstrates props (type coercion, defaults), simulated async data loading with {{#if loading}} conditional rendering, and a computed initials property.

<user-card-demo user-id="42">
user-card-demo.js
Domma.component('user-card-demo', {

  props: {
    userId:     { type: M.types.number,  required: true },
    showAvatar: { type: M.types.boolean, default: true  }
  },

  data() {
    return { name: '', role: '', loading: true };
  },

  computed: {
    initials() {
      return this.data.name
        .split(' ').map(n => n[0] || '').join('').toUpperCase();
    }
  },

  methods: {
    async fetchUser() {
      this.set({ loading: true });

      // Simulate API call (replace with: H.get('/api/users/' + this.props.userId))
      await new Promise(r => setTimeout(r, 600));
      const users = {
        42: { name: 'Alice Nguyen', role: 'Senior Engineer' },
        7:  { name: 'Bob Okafor',   role: 'Product Manager' }
      };
      const user = users[this.props.userId]
        ?? { name: 'Unknown User', role: '—' };

      this.set({ name: user.name, role: user.role, loading: false });
    }
  },

  onMount() { this.fetchUser(); },

  onPropsChanged(name) {
    if (name === 'userId') this.fetchUser();
  },

  template: `
    <div class="card">
      {{#if loading}}
        <div class="skeleton-wrap">
          <div class="skeleton avatar"></div>
          <div class="skeleton-lines">
            <div class="skeleton line"></div>
            <div class="skeleton line short"></div>
          </div>
        </div>
      {{/if}}
      {{#unless loading}}
        <div class="user-row">
          {{#if showAvatar}}
            <div class="avatar">{{initials}}</div>
          {{/if}}
          <div>
            <p class="name">{{name}}</p>
            <p class="role">{{role}}</p>
          </div>
        </div>
      {{/unless}}
    </div>
  `,

  style: `/* … component-scoped CSS … */`
});
Example 3 — Router Integration R.route · props from URL

Use a component as a route view. Props are derived from URL parameters.

// Route definition — props come from URL params
R.route({
    path: '/users/:id',
    component: 'user-card-demo',             // Custom element tag name
    props: (params) => ({                    // Function receives route params
        userId: parseInt(params.id, 10)
    })
});

When this route activates, the router creates <user-card-demo user-id="…"> and mounts it in the route container — no extra glue code needed.

Example 4 — .domma Single-File Format optional · build pipeline

For build-pipeline projects, the optional Rollup plugin lets you co-locate template, script, and styles in a single .domma file (similar to Vue's .vue format). The filename becomes the tag name.

components/user-card.domma
<template>
  <div class="card">
    <div class="avatar">{{initials}}</div>
    <p class="name">{{name}}</p>
    <p class="role">{{role}}</p>
  </div>
</template>

<script>
export default {
  props: {
    userId: { type: M.types.number, required: true }
  },
  data() {
    return { name: '', role: '', loading: true };
  },
  computed: {
    initials() {
      return this.data.name.split(' ').map(n => n[0]).join('');
    }
  },
  methods: {
    async fetchUser() {
      const user = await H.get('/api/users/' + this.props.userId);
      this.set({ name: user.name, role: user.role, loading: false });
    }
  },
  onMount() { this.fetchUser(); }
};
</script>

<style>
.card   { padding: var(--spacing-md); text-align: center; }
.avatar { width: 3rem; height: 3rem; border-radius: 50%;
          background: var(--dm-primary); color: #fff;
          display: flex; align-items: center;
          justify-content: center; margin: 0 auto .75rem; }
.name   { font-weight: 700; margin: 0; }
.role   { color: #6b7280; font-size: .875rem; margin: 0; }
</style>
rollup.config.js
import { dommaPlugin } from './src/plugins/rollup-plugin-domma.js';

export default {
  input: 'src/main.js',
  plugins: [
    dommaPlugin(),   // Handles .domma files
    // … other plugins
  ]
};
How it works
  1. Plugin detects .domma file extension
  2. Extracts <template>, <script>, <style> sections
  3. Derives tag name from filename (user-card.domma → user-card)
  4. Wraps export default definition into a Domma.component() call
  5. Standard Rollup bundling handles the rest
API Reference

Definition Options

OptionTypeDescription
templatestringInline Mustache template
templateUrlstringURL to external template file
propsobjectDeclared HTML attributes with type/default
data()functionReturns initial reactive state
computedobjectDerived values (re-evaluated each render)
methodsobjectFunctions bound to the component context
stylestringScoped CSS injected into Shadow DOM

Lifecycle Hooks

HookWhen
onBeforeMount()Before template is rendered
onMount()After template rendered + bindings established
onUpdated()After any reactive field triggers a DOM update
onBeforeUnmount()Before element is removed from DOM
onUnmount()After element removed and cleaned up
onPropsChanged(name, old, new)When an observed attribute changes

Context (this) inside methods & hooks

this.data          // Reactive data snapshot (toJSON)
this.props         // Resolved props object
this.root          // shadowRoot reference
this.el            // The host <custom-element> itself

this.set({ field: value })  // Batch-update reactive data
                             // (triggers bindings + onUpdated)
// Methods are called via this.methodName()
// Computed values appear as this.computedName

// Accessing props in methods:
const id = this.props.userId;

// Updating state:
this.set({ loading: false, name: 'Alice' });