Components - Walkthrough

New Component

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 → 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.


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, hit Save Component - 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, 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>

Step 8 - Share it

Back on Components, the Export button 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 in onMount()
  • 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 →