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.
True style encapsulation — component CSS never leaks out, theme variables still flow in.
Simple {{field}} bindings update only that text node. Block changes ({{#if}}) trigger targeted re-renders.
JS object definition works at runtime. Optional .domma SFC format for build pipelines.
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.
Lifecycle Log
Waiting for events…
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; }
`
});
Demonstrates props (type coercion, defaults), simulated async data loading
with {{#if loading}} conditional rendering, and a computed
initials property.
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 … */`
});
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.
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.
<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>
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
- Plugin detects
.dommafile extension - Extracts
<template>,<script>,<style>sections - Derives tag name from filename (
user-card.domma → user-card) - Wraps
export defaultdefinition into aDomma.component()call - Standard Rollup bundling handles the rest
Definition Options
| Option | Type | Description |
|---|---|---|
template | string | Inline Mustache template |
templateUrl | string | URL to external template file |
props | object | Declared HTML attributes with type/default |
data() | function | Returns initial reactive state |
computed | object | Derived values (re-evaluated each render) |
methods | object | Functions bound to the component context |
style | string | Scoped CSS injected into Shadow DOM |
Lifecycle Hooks
| Hook | When |
|---|---|
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' });