Components Walkthrough
Components
We'll build one component end-to-end: a star rating widget you can drop onto any
page with [component name="star-rating" max="5" value="3" /]. By the end you'll have
used every section of a .dmc file, reactive state, an event listener, a custom event,
and the live preview. Follow along in a real New Component editor.
Open Data → Components → New Component and name it
star-rating. Remember: lowercase + hyphens, and the name is permanent because it
becomes the element tag <dm-star-rating>. The four source tabs
(<template>, <props>, <script>,
<style>) are on the left; a live preview is on the right.
Start with the inputs, because they shape everything else. A rating needs a maximum number of
stars and a current value. Put this in the <props> tab:
{
"max": { "type": "number", "default": 5, "label": "Number of stars" },
"value": { "type": "number", "default": 0, "label": "Current rating" },
"readonly": { "type": "boolean", "default": false, "label": "Read-only (display only)" }
}
These appear in the preview's Preview props panel, so you can poke values as you build.
We render max stars and mark each as filled if its index is below the current rating.
Because {{#each}} needs a list, we'll build a stars array in state
(next step) where each item knows whether it's on. For now, the markup:
<template>
<div class="dm-stars" role="img" aria-label="{{value}} of {{max}}">
{{#each stars}}
<button class="star {{#if on}}on{{/if}}" data-action="rate" data-index="{{n}}">★</button>
{{/each}}
</div>
</template>
Each star is a button carrying its 1-based position in data-index, so a single click
handler can read which star was pressed.
Three jobs: build the stars array from value, rebuild it whenever the
rating changes, and handle clicks. Remember data() can't see props, so we seed state
in onMount().
<script>
export default {
data() { return { stars: [], value: 0 }; },
methods: {
// Build the star list for a given rating.
render(value) {
const max = this.props.max;
const stars = [];
for (let n = 1; n <= max; n++) stars.push({ n, on: n <= value });
this.set({ stars, value });
},
rate(n) {
if (this.props.readonly) return;
this.render(n);
// Tell the outside world the rating changed.
this.el.dispatchEvent(new CustomEvent('rating-change', {
detail: { value: n }, bubbles: true
}));
}
},
onMount() {
this.render(this.props.value);
this.el.shadowRoot.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action="rate"]');
if (btn) this.rate(Number(btn.dataset.index));
});
}
};
</script>
Note the patterns from the Reference: one delegated listener on
shadowRoot, this.set() to re-render, this.props for inputs,
and a bubbling CustomEvent so a page can react to the chosen rating.
<style>
.dm-stars { display: inline-flex; gap: .15rem; }
.dm-stars .star {
background: none; border: 0; cursor: pointer;
font-size: 1.5rem; line-height: 1; color: #d0d0d0; padding: 0;
}
.dm-stars .star.on { color: #f5b301; }
</style>
These selectors are scoped to the component, so .star won't collide with anything else
on the page.
As you typed, the editor recompiled and re-mounted the component in the preview iframe. Toggle
readonly and change value in the Preview props panel to sanity-check
both modes. When it looks right, hit Save Component — the source compiles before it
is written, so a typo surfaces as a clear error rather than a broken page.
Display-only, in any Markdown page:
[component name="star-rating" max="5" value="4" readonly="true" /]
Interactive, reacting to the custom event with a little page script:
<dm-star-rating max="5" value="0" id="r1"></dm-star-rating>
<script>
document.getElementById('r1').addEventListener('rating-change', (e) => {
console.log('User picked', e.detail.value);
});
</script>
Back on Components, the Export button
gives you star-rating.dmcomponent.json — import that on another Domma site to reuse the
widget verbatim.
- Props with types + defaults, coerced from attributes
- Template interpolation,
{{#each}}and{{#if}} - State via
data()+this.set(), seeded from props inonMount() - A single delegated shadow-root listener and
data-*dispatch - A bubbling CustomEvent for page-level integration
- Scoped CSS, the live preview, and export/import
For the rules that keep components well-behaved — naming, the data()-can't-see-props
gotcha, boolean coercion, plugin-owned names — read Rules.
Next: Rules & gotchas →