# `MishkaCmsCore.Runtime.PageBuilder.Fields`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/fields.ex#L5)

What part of a form a palette component plays, what part a node in the document plays, and how a
field is staged for preview.

## One answer, four callers

`form_node?/2` and `field_node?/2` are the whole editor's answer to "is this node a form" and "is
this node a field". `MishkaCmsCore.Runtime.PageBuilder.Containment` refuses a form dropped inside
one, `MishkaCmsCore.Runtime.PageBuilder.PageForms.FormPlacement` binds a field to the form it lands
in, `MishkaCmsCore.Runtime.PageBuilder.PageForms.FormChecks` reports what is wrong with each, and
the compiled page reads the same markup back through
`MishkaCmsCore.Runtime.PageBuilder.PageForms`. They had four implementations and the narrowest one
— Containment's — never asked the kit at all, so a kit's own form wrapper was allowed to nest
inside another form and the audit then reported the nesting the drop was supposed to refuse.

Deciding a component's role parses that component's markup, and a page holds hundreds of copies of
one card, so both questions take the memo `roles/2` builds: one answer per distinct component
name, not one per node.

## The catalog is not the whole palette

A name with no catalog row is asked of the CMS's own function components — `local_modules/0`, the
same two modules the canvas resolves a dotted tag from — and their `__components__/0` metadata is
translated into the shape a catalog row carries, so everything downstream reads one dialect.

Without that, `<.input>` answered `:none`. It is `MishkaCmsCore.CoreComponents.input/1` rather than
a database row, and it is what every shipped Forms block and every Forms element is built out of —
so the drop never bound one to the form it landed in, and no field-level check ever fired on one.
Dragging a field out of its form left a page that renders on the canvas, raises nothing in the
audit rail, and then fails to compile on publish, because `field={f[:email]}` with no `:let` in
scope is an undefined variable.

## Declaration first, inference second

`role/1` asks `MishkaCmsCore.Builder.FormRole` what the component's own kit declared in its
`extra.form`. A kit that declares nothing — which is every kit that shipped before the key
existed — falls to inference over the component's attributes and its own markup.

An inferred role then passes through the `builder:form:role` transform
(`MishkaCmsCore.Builder.Events.form_role/2`); a declared one never does.

## `:error` is never inferred

An error component renders ordinary markup and declares the same `errors` list a field does, so
nothing in its structure says what it is. A kit that wants the role declares it.

## Detecting a Send button

A component is `:submit` when its template's one OUTER element is a `<button>` whose type can be
`submit`, or an `<input>` whose type can be `submit` or `image` — through its literal, the default
of the attribute it is bound to, or that attribute's values. Buttons inside the markup never count.
A row with no markup (the CMS's own `<.button>`) is asked of its declared `type` attribute.

Whether a placed node sends its form is decided per node by `submit_node?/3`: the node's own
literal type, else the component's default, else HTML's own default. A component that renders a
link in place of its button (one with no markup that declares `href`, `navigate` or `patch`)
never sends when given one; an outer `<button>` is rendered whatever the node carries, so it
still sends. A type the node computes (`type={@kind}`) cannot be read: `type_unread?/3`.

## Passing its form inside

`lends_form?/2` is true when the markup has `render_slot(@inner_block, v)` under a `:let={v}` on
something that hands out a form (`hands_form?/2`): Phoenix's `<.form>`, or a component that itself
lends one, followed through the called component's markup. A raw `<form>` hands out nothing,
because HEEx refuses `:let` on an HTML tag.

## Detecting a field

The load-bearing signal is an attribute typed as a `Phoenix.HTML.FormField` struct — 23 of the
installed kit's 136 components declare one, and it is exactly the set that renders a
value-carrying control. The attribute may be called anything at all: `binding_attr/1` answers with
its name rather than assuming the name `field`.

**Only Phoenix's own struct is inferred**, because only Phoenix's own struct is what the editor
can write. What goes into that attribute is `f[:email]`, and `Phoenix.Component.to_form/2` produces
nothing but a `%Phoenix.HTML.FormField{}`. Matching on the last dot-segment recognised a vendor's
`Acme.FormField` as well — and then wrote a Phoenix struct into it, which the kit's own
`%Acme.FormField{} = field` clause head cannot match, so the call fell through to the rendering
clause and raised `KeyError :name` on the published page. A kit with its own struct declares
`extra.form` instead, which is the kit saying what to write and where.

A kit that binds fields some other way falls to `structural?/1`: the component's markup is PARSED,
and a control it renders whose `name` and `value` (or `checked`) are bound to assigns the
component declares is a field. Reading that as text called a kit's submit button a field — the
same markup appears in `<input type="submit" name={@name} value={@value} />` — and called a
component that merely quotes an input inside a comment one too. The parse answers both: a control
whose `type` submits carries no value, and a comment is not a control.

Neither branch reads `helpers`. Helper clauses are bundled per source MODULE, so
`chelekom-select-option-group` carries `native_select`'s `%Phoenix.HTML.FormField{}` clause head
while rendering nothing but `<option>` — a detector that trusted them would call it a field.

## Detecting a wrapper

A wrapper is a component that renders a form of its own. Its template is PARSED and walked for a
`form` or `.form` node, so a wrapper emitting a raw `<form>` element is found and a template that
merely names one in a comment or a code sample is not.

`chelekom-simple-form` renders neither: it forwards everything to `chelekom-form-wrapper` through
an `{assigns}` splat, so its own markup never mentions a form. A `for` attribute the component
declares REQUIRED counts as well, because required is the component saying the form's data is its
own to hand on. So does a template whose one outer element is a component that is itself a
wrapper, read from that component's own markup.

## Which attributes a binding supersedes

`pair_attrs/1` names the two attributes a kit derives from the form field, so the drop can clear
the blanks a palette card is seeded with — leave `name=""` beside the binding that replaces it and
the blank wins, because a kit fills it with `assign_new/3`, which only fills an ABSENT key.

The kit's declaration wins; the kit's own markup answers next, read by the same parse that decides
`structural?/1`; and `nil` is the honest answer when neither says. It used to fall back to the
literal names `"name"` and `"value"`, which are Phoenix's spelling rather than the kit's — so a kit
calling them `input_name` and `input_value` had two attributes it does not have dropped and its two
real blanks left in place, which is exactly the failure the drop exists to prevent.

`bindings/1` answers the binding attribute and the pair together, for the caller that needs both —
which is every caller that writes one, because writing the binding and clearing what it supersedes
is one decision made once.

## Where the author's own word comes from

`field_label/2` is the read side of the same doctrine, and it was the half still written in
Phoenix's spelling: a two-word vocabulary of `name` then `label`, so a kit that calls them
something else contributed nothing an author had typed. It asks `pair_attrs/1` for the kit's name
attribute instead. See the function for what is left hardcoded and why.

## Why the previews need a field

A text field previewed on its own is a box with a label. It is not wrong — the kit's own docs
render all 589 field examples unwrapped, and no field template depends on a form ancestor — but it
is not what the reader is being asked about either. The modal exists to answer "what does this
look like", and a field's answer includes the form it lives in.

So `stage/2` wraps the preview source, and only the preview source.

## Why a dropped field IS wrapped, elsewhere

HTML forbids a nested `<form>`, and the failure is silent rather than loud: the parser's
form-element pointer discards the inner start tag — `data-node-id` and all — and lets the inner
`</form>` close the OUTER one, ejecting every later sibling from submission.

So the wrapping a dropped field needs is conditional, and it does not belong here. The insert
funnel wraps a field that lands with no form ancestor, in one undoable step, and `Containment`
refuses a form dropped inside a form.

# `binding_attr`

```elixir
@spec binding_attr(map()) :: String.t() | nil
```

The attribute name to write a `%Phoenix.HTML.FormField{}` binding on, or `nil` when this component
cannot take one — in which case the editor writes the kit's `name` and `value` pair instead.

# `bindings`

```elixir
@spec bindings(map()) :: {String.t() | nil, {String.t() | nil, String.t() | nil}}
```

Both of the above at once: `{binding attribute, {name attribute, value attribute}}`.

For the caller that needs both, which is the insert funnel — writing a binding and clearing the
pair it supersedes is one decision. Asked separately it rebuilt the kit's declaration twice and
could parse the component's own markup twice with it, and `chelekom-combobox`'s markup is 6.7 KB.

# `button_node?`

```elixir
@spec button_node?(map(), %{optional(String.t()) =&gt; atom()}, %{
  optional(String.t()) =&gt; map()
}) ::
  boolean()
```

Whether this placed node is a button of any kind: a `<button>`, an `<input>` typed as one, or a
`:submit` component placed as one. A component whose outer element is an `<input>` is a button
only when this node's type (else its default) is a button's; a computed type counts.

`roles` and `rules` are the memos `roles/2` and `submit_rules/2` build.

# `entry`

```elixir
@spec entry(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | String.t() | nil, [
  map()
]) :: map() | nil
```

The declaration behind a node: its palette row, or the CMS-local function component's own
metadata translated into the same shape, or `nil` when neither has heard of the name.

What a binding is written onto is read off it, so a caller holding a node and the palette asks
here rather than searching the catalog itself — searching the catalog alone is what left every
`<.input>` on every shipped Forms block unbound.

# `explicit_submit?`

```elixir
@spec explicit_submit?(map(), %{optional(String.t()) =&gt; atom()}, %{
  optional(String.t()) =&gt; map()
}) ::
  boolean()
```

Whether this node says in writing that it submits: `type="submit"` or `"image"` (or a literal
such as `{:submit}`) on the node, or on its component's outer element. A button left to a default
does not.

# `field?`

```elixir
@spec field?(map()) :: boolean()
```

Whether this component is a form field.

# `field_label`

```elixir
@spec field_label(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()]) ::
  String.t()
```

The author's own word for this control, which is what a field key is minted from.

The attributes are tried in this order, and the first one carrying a word the author actually
wrote wins; a blank is not a word, because the palette seeds a card with one so it renders standing
alone.

  1. The kit's OWN name attribute — `pair_attrs/1` answers what the kit called it, so a component
     spelling it `input_name` contributes what was typed into it. Reading only the literal `name`
     meant a kit with its own spelling contributed nothing and the key fell through to the
     component's name: a dropped `acme-text` came out `f[:acme_text]` while its `input_name`
     carried the word the author had just written.
  2. HTML's own `name`, which is what a raw `<input>` from the Elements library carries and what a
     component taking `name=` directly still means.
  3. The attribute the kit declares as its caption in `extra.form`, or one the component itself
     declares called `label`. The literal `label` is checked against the component's own
     declarations rather than written blind — a kit that spells it otherwise says so beside the
     rest of its bindings.

The node's own label last, which is the component's name and is the answer of last resort rather
than a choice anybody made.

Used by the insert funnel and by the audit's Fix, which had a copy of the two-word version each.

# `field_node?`

```elixir
@spec field_node?(map(), %{optional(String.t()) =&gt; atom()}) :: boolean()
```

Whether this node is a field: a component the kit calls one, or one of HTML's own value-carrying
controls.

Those control names are the one vocabulary in this work, and they are HTML's rather than a kit's.
They are written down in `MishkaCmsCore.Runtime.PageBuilder.PageForms.control?/1` and nowhere else,
because the compiled page's own reader has to agree with what the editor calls a field.

# `form_node?`

```elixir
@spec form_node?(map(), %{optional(String.t()) =&gt; atom()}) :: boolean()
```

Whether this node is a form: HTML's own `<form>`, Phoenix's `<.form>`, a component the kit calls a
wrapper, or any node that names a page assign with `for=`.

`roles` is the memo `roles/2` builds. The last answer is `PageForms.form?/1`, and it is why a kit
wrapper still counts when the palette is not to hand: a wrapper the author has already given a
page assign declares itself in the markup.

# `forwarded_type`

```elixir
@spec forwarded_type(map() | nil) :: String.t() | nil
```

The literal `type` a component's markup writes on the `<input>` its catch-all `:global` attribute
is spread onto, lowercased, or `nil` when it spreads it onto no `<input>` or the type is not
written as text.

# `hands_form?`

```elixir
@spec hands_form?(map(), [map()]) :: boolean()
```

Whether a `:let` on this node receives a form: Phoenix's `<.form>`, or a component whose markup
lends its form. A raw `<form>` does not.

# `lends_form?`

```elixir
@spec lends_form?(map() | nil, [map()]) :: boolean()
```

Whether this component's markup passes a form to what is written inside it:
`render_slot(@inner_block, v)` under a `:let={v}` on something `hands_form?/2` answers yes for.

`components` is the palette, read when the `:let` sits on another component.

# `local_modules`

```elixir
@spec local_modules() :: [module()]
```

The modules a dotted tag with no catalog row is asked of, in the order a compiled page imports
them — `MishkaCmsCoreWeb.Live.Admin.Builder.CanvasLive` resolves a component the same way, and
`MishkaCmsCore.Runtime.PageBuilder.Containment` asks them what declares a slot.

# `pair_attrs`

```elixir
@spec pair_attrs(map()) :: {String.t() | nil, String.t() | nil}
```

The `name` and `value` attribute names a binding supersedes, either of which is `nil` when this
component has no such attribute.

Declared first, read off the component's own markup second, `nil` last — never guessed. See the
moduledoc for what guessing cost.

# `role`

```elixir
@spec role(map(), [map()]) :: atom()
```

This component's part in a form: `:wrapper`, `:field`, `:error`, `:fieldset`, `:submit` or
`:none`.

A declaration wins; otherwise it is inferred and passed through `builder:form:role`. `:error` is
never inferred. `components` is the palette, read when the template's outer element is another
component.

Accepts a palette entry or anything shaped like a component row.

# `roles`

```elixir
@spec roles([map()], [map()]) :: %{required(String.t()) =&gt; atom()}
```

The part each component named in `nodes` plays, keyed by component name — the memo both node
questions take.

Asked once per distinct component in the document rather than once per node: deciding a role
parses the component's own template, and a page can hold hundreds of copies of one card.

# `stage`

```elixir
@spec stage(String.t(), map() | nil) :: String.t()
```

The preview source for a component, wrapped in a form when it is a field.

`phx-submit` is not decoration: a `<form>` with no handler still submits implicitly, so pressing
Enter in a previewed text field navigates the preview iframe and the LiveView is gone. With the
binding present LiveView intercepts the submit and the canvas's own catch-all `handle_event/3`
swallows it.

# `structural?`

```elixir
@spec structural?(map()) :: boolean()
```

The fallback signal, for a kit that carries no form-field struct: a control the component renders
whose `name` and value are both bound to assigns it declares.

Public because it is the half a second kit is judged on, and worth being able to check directly.

# `submit_node?`

```elixir
@spec submit_node?(map(), %{optional(String.t()) =&gt; atom()}, %{
  optional(String.t()) =&gt; map()
}) ::
  boolean()
```

Whether this placed node sends the form it is in: its own literal type, else its component's
default, else HTML's default. A component that renders a link in its place never sends.

`roles` and `rules` are the memos `roles/2` and `submit_rules/2` build.

# `submit_rules`

```elixir
@spec submit_rules(%{required(String.t()) =&gt; atom()}, [map()]) :: %{
  required(String.t()) =&gt; map()
}
```

How each `:submit` component in `roles` sends, keyed by name — the memo `submit_node?/3`,
`explicit_submit?/3`, `button_node?/3` and `type_unread?/3` read. Built once per component, not
per node.

# `takes?`

```elixir
@spec takes?(map(), String.t(), [map()]) :: boolean()
```

Whether an attribute `name` written on this node reaches what it renders: any attribute on an
HTML tag; on a component, one it declares, or one its catch-all `:global` attribute takes (a
`phx-`, `data-` or `aria-` name, one of HTML's global names, or one its `include` list names) when
its markup spreads that attribute. A component with no declaration takes nothing.

# `type_attr`

```elixir
@spec type_attr(map(), %{optional(String.t()) =&gt; atom()}, %{
  optional(String.t()) =&gt; map()
}) ::
  String.t() | nil
```

The attribute this placed button's type is written on: `type` on a `<button>` or an `<input>`,
the kit's own on a `:submit` component, and `nil` when the component's markup fixes the type or
the node renders a link, which reads no type.

`roles` and `rules` are the memos `roles/2` and `submit_rules/2` build.

# `type_unread?`

```elixir
@spec type_unread?(map(), %{optional(String.t()) =&gt; atom()}, %{
  optional(String.t()) =&gt; map()
}) ::
  boolean()
```

Whether this placed button computes its type (`type={@kind}`), so whether it sends its form cannot
be read. A link, and a component whose markup fixes its type, are read. An `<input>` computing its
type is a field (`PageForms.control?/1`), not a button.

`roles` and `rules` are the memos `roles/2` and `submit_rules/2` build.

# `wrapper?`

```elixir
@spec wrapper?(map(), [map()]) :: boolean()
```

Whether this component renders a form of its own, so the author placed the form themselves.
`components` is the palette, read when the template's outer element is another component.

# `written_value?`

```elixir
@spec written_value?(map()) :: boolean()
```

Whether this node carries a value the author wrote rather than one it reads off the form: a radio
option's `one`, a checkbox's `true`, the hidden `false` beside it.

It is what separates several controls that are ONE field — a group answering to one name — from
several fields that would write over each other, and it is why a binding leaves such a value where
it is. A palette card's seeded blank is not a value.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
