Components Reference

Components

Components are small, reusable custom elements you author in one file and drop into any page. Each one is a single .dmc file with up to four sections — <template>, <props>, <script> and <style> — and the CMS compiles it to a self-contained browser module that registers a <dm-name> custom element. No build step, no framework wiring.

Manage them under Data → Components. This page is the complete reference; for guided learning start with the Walkthrough or the How-To recipes.


A component is a single file made of named blocks, much like a single-file component in other frameworks. Three blocks are required (<template>, <props>, <script>); <style> is optional. Block order does not matter.

<template>
<div class="dm-greeting">Hello, {{name}}!</div>
</template>

<props>
{
  "name": { "type": "string", "default": "world", "label": "Name to greet" }
}
</props>

<script>
export default {};
</script>

<style>
.dm-greeting { font-weight: 600; }
</style>

Saving compiles the source before it is written to disk — a malformed component never reaches the filesystem. The compiler reports precise, per-block errors (see Rules for every error code).


Plain HTML plus a Handlebars-style expression syntax. Data comes from the component's props and reactive data() state. Supported syntax:

SyntaxPurpose
{{ value }}Interpolate a prop or state value (HTML-escaped).
{{#if cond}}…{{/if}}Render the block only when cond is truthy.
{{#unless cond}}…{{/unless}}Render the block only when cond is falsy.
{{#each list}}…{{/each}}Repeat the block for each item. Inside, {{.}} is the item (for primitives), {{@index}}, {{@first}} and {{@last}} are available; object items expose their own keys.
{{#with obj}}…{{/with}}Scope the block to an object's keys.
<slot></slot>Project the light-DOM content placed between the element's tags.
<template>
<ul class="dm-list">
  {{#each items}}
    <li class="{{#if @first}}first{{/if}}">{{@index}}. {{.}}</li>
  {{/each}}
</ul>
{{#if title}}<h4>{{title}}</h4>{{/if}}
<slot></slot>
</template>

A JSON object (not an array). Each key is a prop name; each value declares a type and may add a default and a label (shown in the editor's preview panel). The type is validated at compile time.

Valid types: string, number, boolean, array, object.

{
  "initial":   { "type": "number",  "default": 0,     "label": "Initial value" },
  "step":      { "type": "number",  "default": 1,     "label": "Step size" },
  "showReset": { "type": "boolean", "default": false, "label": "Show reset button" },
  "tags":      { "type": "array",   "default": [],    "label": "Tag list" }
}

Props are passed as HTML attributes where the value is always a string; the runtime coerces it to the declared type (so step="2" arrives as the number 2). Read props inside onMount() and methods via this.props — see the script API below.


An ES module with a single export default { … } object describing the component. The export is the only requirement; every key inside it is optional.

export default {
  // Reactive state. Called WITHOUT `this` — do not read props here.
  data() { return { count: 0 }; },

  // Methods are bound to the component context (`this`).
  methods: {
    inc() { this.set({ count: this.data.count + this.props.step }); }
  },

  // Lifecycle — bound to `this`; props and the element are available here.
  onMount()   { this.set({ count: this.props.initial }); },
  onUnmount() { /* clean up timers, listeners, etc. */ }
};
KeyWhat it is
data()Returns the initial reactive state object. Called with no this binding — props are not available here; initialise from props in onMount().
methodsObject of functions bound to the component context.
onMount()Lifecycle hook fired when the element is attached to the DOM. Props and the element node are available.
onUnmount()Lifecycle hook fired when the element is removed — clear intervals/listeners here.

Inside methods and lifecycle hooks, this exposes:

MemberDescription
this.propsThe (type-coerced) props the element was used with.
this.dataThe current reactive state snapshot.
this.set({ … })Patch reactive state; the template re-renders with the new values.
this.elThe custom-element DOM node. Its rendered markup lives in this.el.shadowRoot — attach event listeners there.

Domma's browser globals ($, H, S, E, D, _, …) are available inside the script just as on any page, so a component can fetch data, read storage, or pop a toast.


Optional. CSS here is scoped to the component's shadow DOM, so selectors can't leak out and the page's styles can't leak in. By convention components prefix their root class with dm-name for clarity, but scoping is automatic either way.

<style>
.dm-counter { text-align: center; padding: 1rem; }
.dm-counter .value { font-size: 2.5rem; font-weight: 700; }
</style>

Two equivalent ways, both server-rendered into the page so the right component module is auto-injected:

Shortcode (Markdown)

[component name="counter" initial="10" step="2" showReset="true" /]

Custom element (HTML)

<dm-counter initial="10" step="2" show-reset="true"></dm-counter>

Both pass {initial, step, showReset} to the component. Note the attribute casing: camelCase props map to kebab-case attributes on the raw tag (showResetshow-reset); the shortcode accepts the prop name as written. When the page is rendered the server collects every dm-* tag used and injects one <script type="module" src="/api/components/name.js"> at the end of the body, which registers the element via Domma.component().

To project content into a component's <slot>, use the custom-element form with children:

<dm-reveal-panel heading="Click to reveal">
  <p>Hidden content appears here when toggled.</p>
</dm-reveal-panel>

Source files live in content/components/name.dmc, with an optional sidecar content/components/name.meta.json holding the bundled flag and project tag. The REST surface (all under /api/components):

Method & pathAuthPurpose
GET /api/componentsreadList components with metadata + parsed props.
GET /api/components/:namereadRaw .dmc source + parsed props.
GET /api/components/:name.jspublicCompiled browser module (what pages load).
POST /api/components/compileupdateTransient compile for the live preview (nothing written).
PUT /api/components/:nameupdateCreate or update (compiles first; errors returned, file untouched on failure).
DELETE /api/components/:namedeleteDelete source + sidecar.
GET /api/components/:name/exportreadDownload a .dmcomponent.json bundle.
POST /api/components/importupdateImport a bundle (?overwrite=true to replace).

A bundle is a small JSON envelope you can move between sites (Export / Import buttons on the Components page):

{ "format": 1, "name": "counter", "source": "<template>…", "bundled": false }

Bundled & plugin components

The bundled flag marks a component as part of fresh-install seed data. Plugins may also contribute components via the registerComponent() hook — these appear in the list with a plugin badge, are served from memory, and are read-only from the admin (you can't edit or delete them, and you can't save a component whose name a plugin already owns).


Fresh installs ship with these (never overwritten if you've edited them) — open any in Components to read the full source:

  • counter — state, methods and lifecycle in one small example
  • copy-button — an async method using the clipboard API
  • reveal-panel — conditional rendering plus a <slot>
  • testimonial-card — presentational only (empty export default {})
  • countdown-timer — an interval started in onMount and cleared in onUnmount

Next: How-To recipes →