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.
Summary
Functions
The web address a form sends its answers to after they are checked (sends_elsewhere?/1), or
nil.
Whether a stored handler answers event: one named exactly event, or one whose name carries the
same token and kind (Naming.same_event?/2).
Whether this form works as visitors type (it carries as_typed_attr/0).
The attribute that records a form working as visitors type, with no Send button.
The key this node binds, ignoring any literal name it also carries.
The variable this node reads a field off — field={f[:email]} gives :f — or nil when it reads
a field off nothing.
Whether this type makes an <input> or a <button> a button, which carries no value.
Whether this node is one of HTML's own value-carrying controls.
Every form this template declares, as the specs __page_forms__/0 carries, in document order.
Every phx-change and phx-submit name the forms in tree carry, in document order: the
current names follow/2 renames stored handlers to.
The key an attribute expression binds and what it reads off it: "f[:email]" gives
{:email, []}, "f[:email].name" gives {:email, [:name]}.
The field key this node carries: its binding first, its own name second.
The key an attribute expression binds, or nil.
The same answer as field_key/2, as text and interning nothing.
Every field the form at node owns, in document order and one entry per key.
The stored handlers, each renamed to the event its form answers to now.
Whether this node is a form — the one question the rest of the editor asks about a node.
The row of handler_forms/2 that names event, with its form's node_id, or nil.
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.
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.
The :let variable a form binds, as an atom, or nil when it binds none.
The names already bound inside the form at node, as TEXT, in document order and one per name.
as={:contact} or as="contact" gives "contact".
Every form this template declares, or the parse error that stopped the reading.
The same binding expression, bound to a different key: "f[:email].name" rebound to phone is
"f[:phone].name".
Every attribute on node that binds key, rebound to to, ready for
MishkaCmsCore.Runtime.PageBuilder.Tree.set_attr/4.
Whether this type sends the form the button is in: submit or image.
Whether a form posts to another address once its answers are checked: it names an action and a
phx-trigger-action the page sets.
assigns[:contact_form] gives :contact_form; anything else gives nil.
The spec this node declares, as a list of nothing or one, so it composes with a walk.
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.
A type given as a string or an atom, trimmed and lowercased; nil for none.
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.
Functions
@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.
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.
@spec as_typed?(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: boolean()
Whether this form works as visitors type (it carries as_typed_attr/0).
@spec as_typed_attr() :: String.t()
The attribute that records a form working as visitors type, with no Send button.
@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.
@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.
Whether this type makes an <input> or a <button> a button, which carries no value.
@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.
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.
@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.
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.
@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.
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.
@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.
@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.
@spec follow([map() | String.t()], [String.t() | nil], %{ required(String.t()) => 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.
@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.
@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.
@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.
@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.
@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.
@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.
@spec param_name(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: String.t() | nil
as={:contact} or as="contact" gives "contact".
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.
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.
@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.
Whether this type sends the form the button is in: submit or image.
@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.
@spec source_assign(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: atom() | nil
assigns[:contact_form] gives :contact_form; anything else gives nil.
@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.
@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.
A type given as a string or an atom, trimmed and lowercased; nil for none.
@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.