Contents
- Domma CMS User Manual
- 1. Using the CMS
- 2. Tutorials
- 3. Components
- 4. API Reference
- 5. Tools
Components Walkthrough
Updated by Darryl Waterhouse on 29 September 2026 · 3 min read
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.
Step 1 - Create the component
Open Data > Components, click New component in the banner 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.
Step 2 - Declare the props
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.
Step 3 - Write the template
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.
Step 4 - Add the script (state + behaviour)
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.
Step 5 - Style it (scoped)
<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.
Step 6 - Watch the preview, then save
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, press Save in the banner (or Ctrl+S) - the source compiles before it is written, so a typo surfaces as a clear error rather than a broken page.
Step 7 - Use it on a page
Display-only, in any Markdown page:
[component name="star-rating" max="5" value="4" readonly="true" /]
Interactive, as a raw tag with an id:
<dm-star-rating max="5" value="0" id="r1"></dm-star-rating>
A page's content cannot carry a <script> - the sanitiser strips it - so the code that reacts to rating-change belongs in site-wide JavaScript, such as a plugin's inject.bodyEnd snippet, or in another component that wraps this one:
document.getElementById('r1')?.addEventListener('rating-change', (e) => {
console.log('User picked', e.detail.value);
});
Step 8 - Share it
Back on Components, right-click the row and choose Export .dmcomponent.json - it gives you star-rating.dmcomponent.json - import that on another Domma site to reuse the widget verbatim.
What you used
- 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 →