Components
How to write, call and override a template component with a declared contract of props, slots and attributes that engine-dmo check enforces at every call site.
A component is a reusable piece of markup with a contract. The contract, component.yaml, declares the props the component takes, the slots a caller may fill and whether the caller may add attributes. engine-dmo check holds every call to the contract, on every site, and the engine holds it again at render, so a wrong prop is an error with a file, line and column rather than a page that renders wrong.
Use a component for anything you would otherwise copy between templates: a card, a page header, a notice, a pager, an icon. Use {% include %} only for a fragment of one layout that needs the caller’s whole context.
A first component
Scaffold it. engine-dmo new component writes the contract, the markup and a co-located stylesheet into the shared template root:
engine-dmo new component teaser created templates/_shared/components/teaser/component.yaml
created templates/_shared/components/teaser/teaser.html
created templates/_shared/components/teaser/teaser.css Add --site <key> to write it into one site’s own root instead, and --force to replace files that are already there.
Declare its inputs in component.yaml:
# templates/_shared/components/teaser/component.yaml
name: teaser
description: A news story as a card for a grid, its title the one link.
props:
doc: { type: article, required: true, description: "The story the card shows" }
heading: { type: enum, values: [h2, h3], default: h3, description: "The title's level" }
tone: { type: enum, values: [plain, feature], default: plain }
note: { type: string, description: "A line in place of the summary" }
slots:
meta: { required: false, description: "Below the title, in place of the date" }
attrs: root Write the markup. Every declared prop is a variable, slots holds the filled slots, and attrs holds the caller’s attributes:
{# templates/_shared/components/teaser/teaser.html #}
<article class="teaser teaser--{{ tone }}" {{ attrs }}>
{% if doc.heroImage %}
{{ doc.heroImage.img("card", class="teaser__img") }}
{% if doc.heroImage.credit %}<p class="teaser__credit">{{ doc.heroImage.credit }}</p>{% endif %}
{% endif %}
<{{ heading }} class="teaser__title"><a href="{{ doc.uri|url }}">{{ doc.title }}</a></{{ heading }}>
{% if slots.meta %}<p class="teaser__meta">{% slot 'meta' %}{% endslot %}</p>{% endif %}
{% if note %}<p>{{ note }}</p>{% elif doc.summary %}<p>{{ doc.summary }}</p>{% endif %}
</article> Call it from any template on the site:
{% for article in items %}
{% component 'teaser' doc=article heading='h2' %}
{% endfor %}
{% component 'teaser' doc=entry tone='feature' %}
{% slot 'meta' %}<time datetime="{{ entry.publishedAt|iso }}">{{ entry.publishedAt|date:"medium" }}</time>{% endslot %}
{% endcomponent %} Run engine-dmo check. It validates the contract, the markup and every call.
Where a component lives
A component is a directory under components/ in a template root:
| File | Required | Purpose |
|---|---|---|
components/<name>/component.yaml | Yes, somewhere on the site’s chain | The contract |
components/<name>/<name>.html | Yes | The markup; its file name repeats the directory name |
components/<name>/<name>.css, <name>.js | No | Co-located styles and script, collected into each site’s bundle by the Vite plugin (Frontend build) |
The name is the directory name: lower case letters, digits, - and _. Both files resolve along the site’s template chain, so a site root can override a shared component (Overriding a component).
The contract
component.yaml is decoded strictly: an unknown key is an error, so a misspelt requried cannot quietly make a prop optional.
| Key | Type | Default | Meaning |
|---|---|---|---|
name | string | the directory name | Must equal the directory name when given |
description | string | none | One sentence; shown on hover in the language server and in the generated template API |
props | map of name to prop | none | The inputs a call passes as name=value |
slots | map of name to slot | none | The regions of markup a call may fill |
attrs | root, none or named | none | Whether a call may pass attrs= |
Props
| Key | Type | Default | Meaning |
|---|---|---|---|
type | string | none (required) | One of the prop types below |
required | boolean | false | A call without it is an error |
default | any | none | The value when a call leaves the prop out; must suit the type |
values | list | none | The accepted values; only and always for type: enum |
description | string | none | Shown in hints, hover and the template API |
required and default are exclusive. attrs and slots are reserved and cannot be prop names.
| Prop type | Accepts |
|---|---|
string | A string, or any value that prints as one |
number | An integer or a decimal |
boolean | true or false |
enum | One of values, compared as text |
list | A list |
map | A map, or an engine value with properties (an entry, an image) |
html | Markup. Only markup the engine produced (such as ptext output) prints unescaped; a plain string is escaped where it is printed |
attrs | An attribute string the engine built, or a map |
document | Any document (an entry, a relation’s target, a list item) |
any | Anything |
a type key of your project, such as article or listing | A document of that type; a document of another type is an error |
A prop the call leaves out takes its default, or is empty when it has none, so the markup can test any declared prop with {% if note %} without an error.
Slots
| Key | Type | Default | Meaning |
|---|---|---|---|
required | boolean | false | A call that does not fill it is an error |
description | string | none | Shown in hints and the template API |
attrs
| Value | Meaning |
|---|---|
none | The default. A call that passes attrs= is an error, since the attributes would be dropped |
root | A call may pass attrs=. The markup must print {{ attrs }}, normally on its root element; engine-dmo check reports a component that declares root and never prints it |
named | A call may pass attrs=, and the markup decides where to print them. Nothing checks where |
Calling a component
{% component 'name' prop=expression prop2=expression %} - The name is a quoted string literal, so
engine-dmo checkcan find the contract. A variable name is a parse error. - Props are
name=expressionpairs separated by spaces. A prop passed twice is an error. attrs=is not a prop. It passes the call site’s attributes: a value an engine helper built, such assigmx.fragment(...), or a map. The template language has no map literal, so in practiceattrs=takes a helper’s value:
{% component 'teaser' doc=article attrs=sigmx.fragment('results', on='click') %} A map passed as attrs prints as escaped name="value" pairs in key order; true prints the bare name, and false or an empty value leaves the attribute out.
A call with slots
A call has a body, closed by {% endcomponent %}, exactly when the first thing after the tag (whitespace aside) is a {% slot %}:
{% component 'notice' tone='warning' %}
{% slot 'body' %}<p>{{ t("closures.notice") }}</p>{% endslot %}
{% endcomponent %} - Every region of the body is a named slot. Text or markup outside a slot is an error, both at check and at render. There is no unnamed default slot.
- A slot filled twice in one call is an error.
- A slot’s content renders in the caller’s scope, so it can use the caller’s loop variables and
{% set %}names.
Inside the markup
The markup sees:
- the page’s globals (
site,locale,page,t(),path(),url(),assets,imagesand the rest of the globals); - each declared prop: the value passed, else its default, else empty;
attrs, the call’s attributes, ready to print;slots, the filled slots by name.
It never sees the caller’s own variables (a loop variable, a {% set %}), so a component cannot come to depend on the page that happens to call it. Pass what it needs as a prop.
Print a slot with a fallback, or test whether it was filled:
{% slot 'meta' %}<span>{{ t("teaser.no_date") }}</span>{% endslot %}
{% if slots.meta %}<p class="teaser__meta">{% slot 'meta' %}{% endslot %}</p>{% endif %} {% slot 'name' %}fallback{% endslot %} in the markup prints what the call filled, or the fallback when it filled nothing. A {% slot %} the markup prints must be declared in the contract.
Components can call other components.
What is checked
engine-dmo check (and the language server as you type) reports each of these at its line and column, every site along its own chain. The engine refuses the same mistakes at render, so a value the check could not see (a prop computed from a variable) still fails loudly.
| Mistake | Example message |
|---|---|
| Unknown prop | teaser has no prop "colour", with the declared props or a “did you mean” |
| Missing required prop | required prop doc is missing from the call to teaser |
Enum value not in values | prop heading of teaser: "h4" is not one of h2, h3 |
| Literal of the wrong type | prop size of icon: a string was passed where a number is expected |
| Document of the wrong type | prop doc of teaser is a article, and a event is passed |
| Unknown slot | unknown slot "footer", with the declared slots |
| Required slot left empty | required slot body is not filled. The check reports it for a call with a body; a call with no body at all is refused at render only |
| Content outside a slot | component notice has content outside a {% slot %} in its call |
attrs= on an attrs: none component | attrs passed to teaser, whose contract says attrs: none, so they would be dropped |
attrs: root and no {{ attrs }} in the markup | component teaser declares attrs: root and its markup never prints {{ attrs }} |
| A slot printed in the markup and not declared | slot "summary" is not declared in this component's contract |
| A variable in the markup that is neither a prop nor a global | "tone" is not in the template context |
A contract key that does not exist, a bad prop type, an enum without values, required with a default, a default of the wrong type | Reported at the key in component.yaml |
| A directory with no contract, or a contract with no markup | Reported at the directory |
All of these are errors (MRD-1006, invalid configuration), and engine-dmo check exits 3.
What is not checked
A prop typed with one of your types (doc: { type: article }) is held to that type at the call, but fields read on it inside the markup are not. {{ doc.photo }} on a type with no photo field passes engine-dmo check and prints nothing at render, with no warning, even with template.strict: true. The same holds for a field read on a loop variable or a query() result. Take field names from engine-dmo describe type <type>, and read the rendered page (engine-dmo render <path>) to see each one print.
Dates have no properties: write {{ entry.publishedAt|iso }} and {{ entry.publishedAt|date:"medium" }}. A property such as entry.publishedAt.iso is not reported and prints nothing.
Overriding a component on one site
A site whose template chain puts its own root before _shared can override a component by adding the same directory to its root (Multisite templates):
templates/
_shared/components/teaser/component.yaml
_shared/components/teaser/teaser.html
town/components/teaser/teaser.html the town's own markup - An override may carry markup alone. It then inherits the contract it overrides.
- An override may carry its own
component.yaml, which must take the same inputs: the same props with the same types, requiredness and enum values, the same slots with the same requiredness, and the sameattrs. Descriptions and defaults may differ. Anything else is an error at the override’s contract (component teaser on site town overrides _shared/components/teaser/component.yaml and takes different inputs: ...). A component with different inputs needs a name of its own. - The override’s markup may extend the shared markup and replace blocks of it:
{% extends "components/teaser/teaser.html" %}inside the override resolves to the next file of that name along the chain, never to itself.
Every call site, on every page and in every shared template, then renders the site’s version, unchanged.
Components and the frontend build
The co-located <name>.css and <name>.js files are not linked by the engine. With the meridian() Vite plugin, each site’s main bundle imports the CSS and JS of every component that resolves for that site, so a site’s override brings its own styles and not the shared ones. engine-dmo assets manifest lists which component directory each site resolves. See Frontend build.
Component styles are not scoped or class-prefixed by the build. Prefix your class names (the scaffold uses c-<name>).
Caching and components
A component inside a {% cache %} block registers the documents passed to it as props, so publishing one of them clears the block. See Caching.
Functions
Every function a Meridian template can call (links and URLs, interface strings and locale helpers, content queries, head and structured data, feeds, forms and security, analytics, assets and images, embeds, sigmx, upstream sources, and the DMO functions) with arguments, keywords, return values and examples.
Images
How to print responsive images from asset fields and theme files with img(), picture() and figure(), configure named transforms, breakpoints and art direction in config/images.yaml, and check what a page downloads.