Components Rules

Components

The constraints the compiler enforces, and the handful of behaviours that commonly trip people up. Everything here is validated at save time — a component that breaks a rule never reaches disk.


  • Names must match ^[a-z][a-z0-9-]*$ — start with a lowercase letter, then lowercase letters, digits and hyphens.
  • Valid: counter, copy-button, price-tag-2. Invalid: Counter, 1counter, my_component, ../etc/passwd.
  • The name becomes the element tag <dm-name> and is permanent — it can't be changed after creation. To rename, create a new component and delete the old one.

  • <template>, <props> and <script> are required. A missing one fails with MISSING_BLOCK.
  • <style> is optional and defaults to empty.
  • Blocks are matched by their tags and may appear in any order.

  • The <props> body must be valid JSON and an object (not an array, not a bare value) — otherwise INVALID_PROPS_JSON.
  • Every prop must declare a type that is one of string, number, boolean, array, object. Anything else (or a missing type) fails validation.
  • default and label are optional. label only affects the editor's preview panel.

  • The <script> body must be exactly one export default { … }; statement — the compiler strips that prefix and embeds the object. No export default means SCRIPT_MISSING_EXPORT.
  • Use an object expression — not a class, not multiple exports. An empty export default {}; is valid for a purely presentational component.

1. data() can't see props. It's called with no this binding, so this.props is unavailable there. Initialise prop-derived state inside onMount() (which is bound) using this.set(...).
2. Boolean coercion is string-based. Attribute values are strings. A boolean prop is true only for the string "true"; showReset="false" is still a non-empty string and coerces to true. To switch a boolean off, set it to "true" or omit the attribute entirely.
3. Attribute casing. On the raw custom element, camelCase props are written as kebab-case attributes — showReset becomes show-reset. The [component] shortcode accepts the prop name as written.
4. Array / object props are JSON in the attribute. Pass them as a JSON string — e.g. items='["a","b"]' — and the runtime parses them to the declared type.
5. Listen on the shadow root. A component's rendered markup lives in this.el.shadowRoot. Attach delegated listeners there (one listener, switch on data-action) rather than to document.
6. Clean up in onUnmount(). Anything you start in onMount — intervals, timeouts, external listeners — must be torn down in onUnmount so it doesn't leak when the element is removed.

  • Components contributed by a plugin show a plugin badge, are served from memory, and cannot be edited or deleted from the admin (attempting it returns PLUGIN_OWNED).
  • You also can't save a disk component whose name a plugin already owns — pick a different name.

  • A bundle must have a numeric format no newer than the CMS supports, plus name and source strings — otherwise INVALID_BUNDLE.
  • Importing over an existing name returns CONFLICT unless you confirm the overwrite (the Import button prompts you).
  • Imported source is compiled like any save, so a broken bundle is rejected with the same errors.

  • {{ }} interpolation is HTML-escaped, so prop values can't inject markup.
  • The markdown sanitiser keeps a live allowlist of dm-* tags, refreshed whenever you save or delete a component — a component you just created is immediately usable in page content. on* handler attributes and javascript: URLs are still stripped globally.
  • Saving recompiles and invalidates the cached module, so edits take effect on the next page load without a server restart.

CodeMeaning
INVALID_NAMEName doesn't match the naming rule.
MISSING_BLOCKA required <template> / <props> / <script> block is absent.
INVALID_PROPS_JSONProps aren't a JSON object, or a prop has an invalid/missing type.
SCRIPT_MISSING_EXPORTThe script has no export default object.
PLUGIN_OWNEDTried to edit/delete (or shadow) a plugin-contributed component.
INVALID_BUNDLE / CONFLICTImport bundle malformed, or a name collision needs overwrite confirmation.

Back to Reference  ·  How-To  ·  Walkthrough