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.
.dmc file formatA 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).
<template> - markupPlain HTML plus a Handlebars-style expression syntax. Data comes from the component's
props and reactive data() state. Supported syntax:
| Syntax | Purpose |
|---|---|
{{ 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>
<props> - typed inputsA 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.
<script> - behaviourAn 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. */ }
};
| Key | What it is |
|---|---|
data() | Returns the initial reactive state object. Called with no this binding - props are not available here; initialise from props in onMount(). |
methods | Object 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:
| Member | Description |
|---|---|
this.props | The (type-coerced) props the element was used with. |
this.data | The current reactive state snapshot. |
this.set({ … }) | Patch reactive state; the template re-renders with the new values. |
this.el | The 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.
<style> - scoped CSSOptional. 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:
[component name="counter" initial="10" step="2" showReset="true" /]
<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
(showReset → show-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 & path | Auth | Purpose |
|---|---|---|
GET /api/components | read | List components with metadata + parsed props. |
GET /api/components/:name | read | Raw .dmc source + parsed props. |
GET /api/components/:name.js | public | Compiled browser module (what pages load). |
POST /api/components/compile | update | Transient compile for the live preview (nothing written). |
PUT /api/components/:name | update | Create or update (compiles first; errors returned, file untouched on failure). |
DELETE /api/components/:name | delete | Delete source + sidecar. |
GET /api/components/:name/export | read | Download a .dmcomponent.json bundle. |
POST /api/components/import | update | Import 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 }
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 examplecopy-button - an async method using the clipboard APIreveal-panel - conditional rendering plus a <slot>testimonial-card - presentational only (empty export default {})countdown-timer - an interval started in onMount and cleared in onUnmountNext: How-To recipes →