A component's INTERFACE — the attributes and slots it declares — as data the editor can edit and check.
Why this exists
A runtime component is markup plus a declaration, and until now the builder could only edit the
markup. The declaration lives in the attrs and slots columns of
MishkaCmsCore.Runtime.Resources.Component, and the only way to touch it was the Components table
form, where a slot's options are a raw JSON textarea. Meanwhile that same declaration drives almost
everything the page builder does with the component: which controls the Inspector offers, what
.properties and .slots list, which properties are marked required, whether a drop into a slot
is allowed at all.
So an author could build a component visually and had no way to say what it takes — and a template
that reads @size without attr :size does not fail at save time. It compiles, and then raises
KeyError the first time the component renders on a page, which is the same shape as the
key :src not found an empty required slot attribute used to produce.
What it checks
validate/2 refuses the declaration itself: a name that is not a valid Elixir identifier, a
duplicate, an unknown type, a struct attribute with no module, and the one combination Phoenix
rejects outright — required: true together with a default.
undeclared/3 is the other direction, and the one that catches the KeyError before it happens: it
reads the template for assigns and slots the declaration does not mention. site and
component_name are never reported, because
MishkaCmsCore.Runtime.Compilers.ComponentCompiler declares both on every runtime component.
Reading a template with a regex has the limits MishkaCmsCore.Runtime.PageBuilder.Inspector's
renders_slot?/2 documents, and the same rule applies here: this is ADVICE, shown beside a button
that writes the declaration for you. It never refuses a save, so a false positive costs a line of
text and never a blocked author.
Summary
Types
One declared attribute, as it is stored on the resource.
One declared slot, as it is stored on the resource.
Functions
The attributes a record declares, always a list.
Whether this row says it is required, whichever of the two shapes it is in.
The slots a record declares, always a list.
Every attribute type this editor will write, in the order it offers them.
What the template reads and the declaration never mentions.
What is wrong with this declaration, as {kind, index, sentence} for the row it belongs to.
One field of a row as trimmed text, atom keys and string keys alike, "" when it is not there.
Types
Functions
The attributes a record declares, always a list.
Whether this row says it is required, whichever of the two shapes it is in.
A row on its way out of the form carries required at the top level; a row read back off the
record carries it under opts, which is where the compiler looks. Public because reading a
declaration is not only this module's business — the layers rail draws the same rows.
The slots a record declares, always a list.
@spec types() :: [String.t()]
Every attribute type this editor will write, in the order it offers them.
@spec undeclared(String.t() | nil, [attr()], [slot()]) :: %{ attrs: [String.t()], slots: [String.t()] }
What the template reads and the declaration never mentions.
%{attrs: [names], slots: [names]}, in the order they first appear, so the list reads the way the
markup does. A name that is declared as a slot is never reported as a missing attribute, and the
two the compiler always provides are never reported at all.
@spec validate([attr()], [slot()]) :: [{:attr | :slot, non_neg_integer(), String.t()}]
What is wrong with this declaration, as {kind, index, sentence} for the row it belongs to.
Empty means it can be written. The sentences are meant to be read by whoever is filling the form, so they say what to do rather than what rule was broken.
One field of a row as trimmed text, atom keys and string keys alike, "" when it is not there.