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

What can be learned about a page's forms by parsing the page's own template.

The answer is what `__page_forms__/0` carries into the compiled page, so everything the runtime
needs — the page assign, the param name, the DOM id, both event names, the success message and
every field with its key, its type and its rules — is read out of the markup the author already
wrote. There is no second place to declare any of it and therefore nothing that can drift.

## What declares a form

A node carrying `for={assigns[:something]}`. That is `Phoenix.Component.form/1`'s own attribute
name and the one a third-party wrapper forwards (`chelekom-form-wrapper` renders
`<.form :let={f} for={@for} as={@as} id={@id}>`), so nothing here reads a component name, a role or
a database row. A form that names no page assign, or no param name with `as`, declares no spec at
all: the compiler generates nothing for it and the audit rail is where the author is told.

## What declares a field

Any attribute of any descendant whose expression is `<letvar>[:key]`, where `<letvar>` is the name
the form node bound with `:let`. `field={f[:email]}`, `bind={f[:email]}` and a hand-written
`value={f[:email].value}` are all the same fact, and the key is authored in exactly one place —
there is no parallel `data-field` marker to drift from it.

A plain `<input name="email">` inside the form falls back to its `name`. That fallback is limited
to HTML's own value-carrying controls (`input`, `select`, `textarea`, and not the `input` types
that submit rather than carry), because `name` is an ordinary attribute on everything else: an
icon written `<.icon name="hero-user" />` inside a form is not a field, and reading `name` off any
node at all would put one in the spec.

## Which form a field belongs to

Its nearest one. Walking every descendant of a form would give an outer form the inner one's
fields the moment a nested form arrives — through the code drawer, a paste or an import, since the
editor itself refuses to nest one — and the outer form would then validate and clear keys that are
not its own.

## What declares a rule

The HTML constraint attributes already on the field node: `required`, `minlength`, `maxlength`,
`min`, `max`, `pattern`, plus `data-format` and `data-confirms` for the two things HTML has no word
for. One declaration that the browser enforces and the server re-checks.

## Why the spec's keys are atoms, and why the editor never asks for them

A spec is built once per compile, from a template the site's own author saved, and its keys are
handed to `Ecto.Changeset.cast/4` and to `Phoenix.Component.assign/3` — both of which want atoms.
`declared/1`, `read/1`, `spec/2` and `fields/2` are that half, and `control_key/1` is the one place
a `name=` attribute is interned.

The editor is the opposite case and asks `names/2` and `field_name/2`, which answer the same fact
in binaries. It matters because the editor asks on every pass of the audit rail and on every drop,
from a `name=` an author is still typing: `FormChecks.findings/3` is recomputed while the rail is
the open tab, so an atom per intermediate value — `e`, `em`, `ema`, `emai`, `email` — was interned
permanently from an unbounded source, and `Naming.field_key/2`, the only consumer, compared them
as text anyway. `MishkaCmsCore.Runtime.PageBuilder.PageForms.Naming` says the same thing from the
other side: minting an atom per label typed into a text box leaks the atom table for no gain.

Read-only. `MishkaCmsCore.Runtime.Compilers.LiveViewCompiler` still compiles the stored template
byte for byte, and nothing here rewrites what is published.

See also `MishkaCmsCore.Runtime.PageForm.Rules`, which executes the rules this module lifts, and
`MishkaCmsCore.Runtime.PageBuilder.PageForms.FormPlacement`, which writes the bindings it reads.

# `address`

```elixir
@spec address(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: String.t() | nil
```

The web address a form sends its answers to after they are checked (`sends_elsewhere?/1`), or
`nil`.

# `answered?`

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

Whether a stored handler answers `event`: one named exactly `event`, or one whose name carries the
same token and kind (`Naming.same_event?/2`).

`entries` are Events-tab entries (`%{name: _}` or `%{"name" => _}`) or bare names.

# `as_typed?`

```elixir
@spec as_typed?(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: boolean()
```

Whether this form works as visitors type (it carries `as_typed_attr/0`).

# `as_typed_attr`

```elixir
@spec as_typed_attr() :: String.t()
```

The attribute that records a form working as visitors type, with no Send button.

# `bound_key`

```elixir
@spec bound_key(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), atom() | nil) ::
  atom() | nil
```

The key this node binds, ignoring any literal `name` it also carries.

# `bound_var`

```elixir
@spec bound_var(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: atom() | nil
```

The variable this node reads a field off — `field={f[:email]}` gives `:f` — or `nil` when it reads
a field off nothing.

`bound_key/2` answers for a KNOWN form; this answers where no form is known, which is the one case
that matters: a field outside every form is bound to a `:let` variable nothing binds any more, and
`MishkaCmsCore.Runtime.PageBuilder.CodeCheck` refuses the whole page for it. A control that names
itself outright merely stops submitting. Two different costs, so the audit tells them apart.

`assigns` is never the answer. It is in scope in every template, so `assigns[:contact_form]` is a
page assign being read rather than a form binding waiting for a `:let`.

# `button_type?`

```elixir
@spec button_type?(String.t() | nil) :: boolean()
```

Whether this type makes an `<input>` or a `<button>` a button, which carries no value.

# `control?`

```elixir
@spec control?(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: boolean()
```

Whether this node is one of HTML's own value-carrying controls.

`input`, `select` and `textarea` carry a value, and the `type` values that submit or reset carry
none, read by `written_type/1` whatever their case. Nothing else is matched by name, and this
module is the ONE place HTML's control and type words are written down:
`MishkaCmsCore.Runtime.PageBuilder.Fields` asks it for the node question, for the markup half of
a component's role and for what a button sends, and the fallback that reads a control's own `name`
asks it here. It was stated four times, so adding HTML's `<output>` meant editing four files and
missing one made the audit and the insert funnel disagree about what a field is.

# `declared`

```elixir
@spec declared(String.t() | nil) :: [map()]
```

Every form this template declares, as the specs `__page_forms__/0` carries, in document order.

An unparseable template answers `[]`. A compile must not fail because one page's markup is broken,
and the audit rail is where a broken document is reported; `read/1` is the same question for a
caller that wants the parse error itself.

# `events`

```elixir
@spec events(MishkaCmsCore.Runtime.PageBuilder.Tree.t()) :: [String.t()]
```

Every `phx-change` and `phx-submit` name the forms in `tree` carry, in document order: the
`current` names `follow/2` renames stored handlers to.

# `field_access`

```elixir
@spec field_access(String.t(), atom() | nil) :: {atom(), [atom()]} | nil
```

The key an attribute expression binds **and what it reads off it**: `"f[:email]"` gives
`{:email, []}`, `"f[:email].name"` gives `{:email, [:name]}`.

`field_key_from/2` deliberately forgets the second half — the key is the same fact whichever
member of the field is read — but a renderer cannot forget it. `name={f[:email].name}` wants the
input name, a string; handing it the whole `%Phoenix.HTML.FormField{}` because both expressions
answer `:email` is how the canvas red-boxed a kit that binds by name and value
(`MishkaCmsCoreWeb.Live.Admin.Builder.CanvasLive`, which is the surface the form is authored on).

The source is parsed and never evaluated, and the path is whatever dots the author wrote — this
module does not decide which members exist, the struct does.

# `field_key`

```elixir
@spec field_key(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), atom() | nil) ::
  atom() | nil
```

The field key this node carries: its binding first, its own `name` second.

`var` is the form's `:let` variable, so a node bound to some other variable is not this form's
field even when it sits inside it.

# `field_key_from`

```elixir
@spec field_key_from(String.t(), atom() | nil) :: atom() | nil
```

The key an attribute expression binds, or `nil`.

`"f[:email]"` and `"f[:email].value"` are the same fact — the source is parsed and never
evaluated, the rule the canvas follows for every other expression — and `"g[:email]"` under a form
bound to `f` is not this form's field.

# `field_name`

```elixir
@spec field_name(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), atom() | nil) ::
  String.t() | nil
```

The same answer as `field_key/2`, as text and interning nothing.

The editor's half of the question — see the moduledoc for why the two halves exist.

# `fields`

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

Every field the form at `node` owns, in document order and one entry per key.

A key bound twice is one field: the label and the input of the same control both carry it, and the
form has one value for it either way.

# `follow`

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

The stored handlers, each renamed to the event its form answers to now.

An entry follows when its name carries the token and kind of one of the `current` event names but
is not that name, which is what a renamed form leaves behind. `retagged` maps an untagged name to
the tagged name a rename gave its form (`MishkaCmsCore.Runtime.PageBuilder.Inspector.retagged/3`),
and such an entry follows by that tagged name's token and kind; its code's quoted mentions of the
untagged name move with it, since no form answers that name any more. An exact match, a name no
current event shares a token with, and a rename that would give two entries one name are left as
they are. Every other key of an entry is kept.

# `form?`

```elixir
@spec form?(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: boolean()
```

Whether this node is a form — the one question the rest of the editor asks about a node.

# `handler_for`

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

The row of `handler_forms/2` that names `event`, with its form's `node_id`, or `nil`.

# `handler_forms`

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

Every named form in `tree`, in document order: its `node_id`, the `name` a person reads
(`Naming.label/1`) and its `handlers/2`. Read from the tree, so no field name becomes an atom.

# `handlers`

```elixir
@spec handlers(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map() | String.t()]) ::
  [
    %{kind: :validate | :submit, event: String.t(), custom?: boolean()}
  ]
```

A form's events as rows for the editor, On change first: `kind`, `event`, and `custom?` when a
stored handler answers it (`answered?/2`). An event the form does not name has no row.

# `let_var`

```elixir
@spec let_var(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: atom() | nil
```

The `:let` variable a form binds, as an atom, or `nil` when it binds none.

# `names`

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

The names already bound inside the form at `node`, as TEXT, in document order and one per name.

What a newly minted key is measured against, and the answer the editor takes. `fields/2` is the
compile-time half and interns each name; this one never does, because it is asked on every drop and
on every pass of the audit rail from a `name=` the author has not finished typing.
`MishkaCmsCore.Runtime.PageBuilder.PageForms.Naming.field_key/2` compares text either way.

# `param_name`

```elixir
@spec param_name(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: String.t() | nil
```

`as={:contact}` or `as="contact"` gives `"contact"`.

# `read`

```elixir
@spec read(String.t() | nil) :: {:ok, [map()]} | {:error, map()}
```

Every form this template declares, or the parse error that stopped the reading.

`{:error, %{line:, column:, message:}}` is `MishkaCmsCore.Runtime.PageBuilder.Tree.parse/1`'s own
answer, passed through so the caller can say where the document broke.

# `rebind`

```elixir
@spec rebind(String.t(), atom() | nil, String.t(), atom() | nil) :: String.t() | nil
```

The same binding expression, bound to a different key: `"f[:email].name"` rebound to `phone` is
`"f[:phone].name"`.

`nil` for a source that is not this form's binding at all, so a caller can leave it exactly as the
author wrote it. What is rebuilt is only what `field_access/2` recognised — the variable, the key
and the dots — so nothing here has to reproduce an expression it did not understand.

# `rebindings`

```elixir
@spec rebindings(
  MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(),
  atom() | nil,
  atom(),
  String.t(),
  atom() | nil
) :: [{String.t(), {:expr, String.t()}}]
```

Every attribute on `node` that binds `key`, rebound to `to`, ready for
`MishkaCmsCore.Runtime.PageBuilder.Tree.set_attr/4`.

All of them together, because one control carries the key more than once —
`name={f[:email].name}` beside `value={f[:email].value}` — and moving one of the pair alone would
leave the input posting under one key and redisplaying another. Attributes bound to some other key
are not touched.

Read by `MishkaCmsCore.Runtime.PageBuilder.PageForms.Naming.rekey/2`, which writes straight to a
tree, and by `MishkaCmsCore.Runtime.PageBuilder.PageForms.FormChecks`, which turns each one into
an op — one answer, two shapes.

`to_var` rebinds onto another form's `:let` variable as well; `nil` keeps `var`.

# `sending_type?`

```elixir
@spec sending_type?(String.t() | nil) :: boolean()
```

Whether this type sends the form the button is in: `submit` or `image`.

# `sends_elsewhere?`

```elixir
@spec sends_elsewhere?(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: boolean()
```

Whether a form posts to another address once its answers are checked: it names an `action` and a
`phx-trigger-action` the page sets.

# `source_assign`

```elixir
@spec source_assign(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: atom() | nil
```

`assigns[:contact_form]` gives `:contact_form`; anything else gives `nil`.

# `spec`

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

The spec this node declares, as a list of nothing or one, so it composes with a walk.

Both the page assign and the param name are required. A form missing either is not half-compiled
into a page that answers some events and not others.

# `trigger_assign`

```elixir
@spec trigger_assign(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: atom() | nil
```

The page assign a form's `phx-trigger-action` reads (`assigns[:contact_send]` gives
`:contact_send`), or `nil`. `MishkaCmsCore.Runtime.PageForm.submit/5` sets it on a valid submit, so
the browser then posts the form to its `action`.

# `type_word`

```elixir
@spec type_word(term()) :: String.t() | nil
```

A type given as a string or an atom, trimmed and lowercased; `nil` for none.

# `written_type`

```elixir
@spec written_type({:string, String.t()} | {:expr, String.t()} | :boolean | nil) ::
  String.t() | nil
```

The type an attribute value names, trimmed and lowercased as HTML reads it: a literal, or an
expression that is only a literal (`{"submit"}`, or `{:submit}` as the Inspector writes an atom
attribute). `nil` for an unset, bare, blank or computed value.

Takes what `MishkaCmsCore.Runtime.PageBuilder.Tree.Node.attr/2` answers.

---

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