Components - How-To

New Component

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.


Create a component

  1. Open Data → Components → New 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.

Render a value

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" /]


Add state and respond to a click

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>

Initialise state from a prop

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 }); }

Show something conditionally

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

Loop over a list

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>

Accept arbitrary inner content (slot)

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

Fetch data from the API

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 || [] });
  }
};

Run something on a timer (and clean up)

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); }
};

Style it without leaking

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>

Use it on a page

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

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

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

Move a component between sites

  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.

File it under a project

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