Components How-To

Components

Short, copy-paste recipes for the things you'll actually do. For a guided build-from-scratch see the Walkthrough; for the full API see the Reference.


  1. Open Data → ComponentsNew Component.
  2. Name it in lowercase-with-hyphens (e.g. price-tag). The name is permanent and becomes the tag <dm-price-tag>.
  3. Fill the <template>, <props> and <script> tabs (the editor watches the live preview as you type).
  4. Save. It compiles first — fix any reported errors and it lands on disk.

Interpolate a prop or state value with double braces:

<template><span class="dm-price">{{currency}}{{amount}}</span></template>
<props>
{
  "currency": { "type": "string", "default": "£" },
  "amount":   { "type": "number", "default": 0 }
}
</props>
<script>export default {};</script>

Use it: [component name="price-tag" currency="$" amount="49" /]


Render the markup with data-action hooks, then attach one delegated listener to the shadow root in onMount. Mutate state with this.set() — the template re-renders automatically.

<template>
<div class="dm-counter">
  <strong>{{count}}</strong>
  <button data-action="inc">+</button>
</div>
</template>
<props>{ "step": { "type": "number", "default": 1 } }</props>
<script>
export default {
  data() { return { count: 0 }; },
  methods: {
    inc() { this.set({ count: this.data.count + this.props.step }); }
  },
  onMount() {
    this.el.shadowRoot.addEventListener('click', (e) => {
      if (e.target.dataset.action === 'inc') this.inc();
    });
  }
};
</script>

data() runs without a this binding, so it can't read props. Seed the value in onMount() instead:

data() { return { count: 0 }; },
onMount() { this.set({ count: this.props.initial }); }

{{#if showReset}}<button data-action="reset">Reset</button>{{/if}}
{{#unless soldOut}}<button>Buy</button>{{/unless}}

Pass an array prop (attribute value is JSON) and iterate with {{#each}}:

<template>
<ul>{{#each items}}<li>{{@index}}. {{.}}</li>{{/each}}</ul>
</template>
<props>{ "items": { "type": "array", "default": [] } }</props>
<script>export default {};</script>
<dm-my-list items='["Alpha","Beta","Gamma"]'></dm-my-list>

<template>
<div class="dm-callout"><slot></slot></div>
</template>
<dm-callout><p>Anything here is projected into the slot.</p></dm-callout>

Domma's HTTP helper H is available inside the script. Fetch in onMount and push results into state:

export default {
  data() { return { entries: [] }; },
  async onMount() {
    const res = await H.get('/api/v1/announcements');
    this.set({ entries: res.entries || [] });
  }
};

Start the interval in onMount; always tear it down in onUnmount so it doesn't leak when the element is removed:

export default {
  data() { return { now: '' }; },
  _timer: null,
  methods: { tick() { this.set({ now: new Date().toLocaleTimeString() }); } },
  onMount()   { this.tick(); this._timer = setInterval(() => this.tick(), 1000); },
  onUnmount() { if (this._timer) clearInterval(this._timer); }
};

Anything in <style> is scoped to the component, so you can use short class names freely:

<style>
.dm-callout { padding: 1rem; border-left: 3px solid var(--dm-primary, #5aa8ff); }
</style>

Either form works and auto-loads the component's module:

[component name="counter" step="2" /]

<dm-counter step="2"></dm-counter>


  1. On Components, click Export on the row — you'll download name.dmcomponent.json.
  2. On the other site, click Import and pick that file. On a name clash you'll be asked to confirm an overwrite.

In the editor, set the Project dropdown. That writes meta.project to the sidecar so the component shows under that project's filtered list and is scoped accordingly.


Next: Walkthrough →  ·  Rules & gotchas