MishkaCmsCore. Runtime. PageBuilder. PageForms. Naming
(Mishka CMS Core v0.0.1)
Copy Markdown
View Source
One seed per form, and the five names derived from it.
| derived | from seed contact | with token k7m2q |
|---|---|---|
| DOM id | contact-form | contact-form |
| param name | as={:contact} | as={:contact} |
| page assign | for={assigns[:contact_form]} | the same |
| change event | contact_validate | contact-k7m2q-validate |
| submit event | contact_submit | contact-k7m2q-submit |
Tokens
A form carries a random 5-character token in both event names. A rename keeps it, so a handler
stored under the old event name is still found by token and kind (event_key/1). A form arriving
in the tree gets a token when it has none or shares one, and any form whose token an earlier form
already holds gets a fresh one (dedupe/2). A form without a token keeps the untagged names until
it is renamed or placed again; a markup edit of an ancestor is neither (dedupe/3).
There is no control anywhere in the editor that edits one of the five alone, so they cannot
disagree: the Inspector's Form name control writes attrs/2 in one batch, and every other route
into the tree goes through dedupe/1. Two forms on one page are therefore impossible to collide
rather than merely reported as colliding.
Everything here is a string
A seed is a binary, a field key is a binary, and nothing in this module calls String.to_atom/1.
What the editor is producing is SOURCE TEXT — as={:contact} and field={f[:email]} are markup,
and the atom in them is a literal the page compiler interns once, not a value this module needs.
Minting an atom per label typed into a text box would leak the atom table for no gain, so a caller
writing a key into markup interpolates it as ":" <> key.
The param name is written as as={:contact} rather than as="contact" because that is what a
hand-written LiveView form carries, and it is what Phoenix.Component.form/1 documents;
MishkaCmsCore.Runtime.PageBuilder.PageForms.param_name/1 reads both forms back.
Why a seed is re-minted on the way out
attrs/1 slugs whatever it is handed. A seed that reached the tree some other way — hand-typed
into the code drawer, pasted from another page — could otherwise produce as={:2 items}, which is
not an atom literal and does not compile, or for={assigns[:my form_form]}, which is not an
assign. Slugging is idempotent, so a seed that is already legal survives untouched.
A seed already in the page is minted the same way before it is compared, so what a rename writes
is measured against what every other form would be called — as={:"my form"} cannot be renumbered
into a name a second form is already using.
Why renumbering runs at the op layer
dedupe/2 is applied to the whole tree after a mutating op rather than at one insert route,
because paste (kind: :insert, built by MishkaCmsCore.Runtime.PageBuilder.EditorSession.paste/2)
and duplicate (kind: :duplicate) never pass through the insert funnel — and "duplicate this form"
is how an author actually makes the second one. MishkaCmsCore.Runtime.PageBuilder.Tree.duplicate/2
re-mints NODE ids and copies every attribute verbatim, so the copy arrives carrying the original's
seed in all five places.
rekey/2 is the same rule one level down: a duplicated FIELD arrives carrying the key its
original owns, and several inputs bound to one key overwrite each other on submit. Both take the
ids the op just created, and in both the arrival is what gets renamed — see dedupe/2 for what
renaming the wrong one costs.
Gaps are filled, not skipped
A page whose forms are contact and contact_3 mints contact_2 next. Numbering is a search for
the lowest free member of a family, not a counter, so deleting the second of three forms does not
push the next one to contact_4.
See also MishkaCmsCore.Runtime.PageBuilder.PageForms for what declares a form in the first place.
Summary
Functions
The five attributes a form node carries with the untagged event names, in the order a reader
expects them, ready for MishkaCmsCore.Runtime.PageBuilder.Tree.set_attr/4.
The five attributes with token inside both event names: contact-k7m2q-validate and
contact-k7m2q-submit. A nil token writes the untagged names, as attrs/1 does.
Renumbers every form that shares a seed with one that claimed it first, rewriting all five derived names in one pass.
Which attributes a rename touches — read by the Inspector so it can batch them as one op.
One event name: contact-k7m2q-submit with a token, contact_submit with nil.
{token, kind} of a tagged event name, or nil for any other name.
A field key minted from a label, unique within its form.
The token a form carries in its phx-submit, else in its phx-change, or nil.
The form's own token when no other form in tree carries it; otherwise, or when it has none, a
fresh one. What a renumbered form is written with.
people_name/1 for a name an author typed rather than a seed: "Contáct Us" is "Contact us", and
a name that leaves no seed is "Form 1".
A fresh token no event name in tree carries: 5 characters of a-z2-7.
attrs/2 for the form node, and its phx-trigger-action under the new seed when it sends to
another address (PageForms.sends_elsewhere?/1), so a rename or a renumber keeps that working.
What every writer of the five names writes.
The name of form number number, as a person reads it: "Form 3".
The name a site admin reads for a form's seed: form is "Form 1", form_3 is "Form 3", and any
other seed is its words with a capital ("contact_us" is "Contact us"). nil for nil.
Re-keys a field that arrived carrying a key the form it landed in already uses.
The event attributes of node rewritten to carry token, ready for
MishkaCmsCore.Runtime.PageBuilder.Tree.set_attr/4.
Whether two event names carry the same token and the same kind, whatever their seeds.
Whether two event names carry the same token, so they belong to the same form.
The seed a label mints: lowercased, accents folded away, every other run of characters turned into
a single _, and never starting with a digit.
Every form already in this tree, as {node id, seed} in document order.
The token inside a tagged event name, or nil.
The phx-trigger-action a form sending to another address carries: the page assign
MishkaCmsCore.Runtime.PageForm.submit/5 sets once its answers are checked, assigns[:contact_send]
for the seed contact.
A seed based on base that no form in tree is using.
Functions
The five attributes a form node carries with the untagged event names, in the order a reader
expects them, ready for MishkaCmsCore.Runtime.PageBuilder.Tree.set_attr/4.
The five attributes with token inside both event names: contact-k7m2q-validate and
contact-k7m2q-submit. A nil token writes the untagged names, as attrs/1 does.
@spec dedupe( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), Enumerable.t(), Enumerable.t() ) :: MishkaCmsCore.Runtime.PageBuilder.Tree.t()
Renumbers every form that shares a seed with one that claimed it first, rewriting all five derived names in one pass.
arrived is the node ids the op just created, and THEY ARE THE ONES THAT YIELD. Claiming in
document order renamed whichever form came first, so dropping a second Contact form ABOVE the
first rewrote the form already on the page — its param name, its page assign and both event names
— and the contact_submit clause its author had written on the Events tab was left bound to
nothing while the page went on compiling. A form nobody has typed a handler for yet is the only
one that can be renamed for free.
With no arrivals named, document order decides, which is what a whole document arriving at once
(MishkaCmsCore.Runtime.PageBuilder.EditorSession.put_tree/3) wants: there is no incumbent there.
Tokens follow the same order. A renumbered form keeps its token unless an earlier form holds it,
and gets a fresh one otherwise. A form whose token an earlier form holds gets a fresh one. An
arriving form with no token gets one (retokened/2). An incumbent form with no token is left
alone.
carried is the event names the page's forms carried before the change. An arriving form with no
token that answers one of them is the same form parsed again (a markup edit of an ancestor), so it
keeps its untagged names unless its seed is taken.
Answers the tree it was given when nothing needs a new name or token.
@spec derived() :: [String.t()]
Which attributes a rename touches — read by the Inspector so it can batch them as one op.
One event name: contact-k7m2q-submit with a token, contact_submit with nil.
{token, kind} of a tagged event name, or nil for any other name.
A field key minted from a label, unique within its form.
taken is the keys already bound in that form, as atoms or as binaries — the parsed side of the
editor reads them out of f[:email] expressions as atoms, and this compares them as text.
@spec form_token(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil) :: String.t() | nil
The token a form carries in its phx-submit, else in its phx-change, or nil.
@spec kept_token( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() ) :: String.t()
The form's own token when no other form in tree carries it; otherwise, or when it has none, a
fresh one. What a renumbered form is written with.
people_name/1 for a name an author typed rather than a seed: "Contáct Us" is "Contact us", and
a name that leaves no seed is "Form 1".
@spec mint_token(MishkaCmsCore.Runtime.PageBuilder.Tree.t()) :: String.t()
A fresh token no event name in tree carries: 5 characters of a-z2-7.
@spec names_for( MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), String.t() | nil, String.t() | nil ) :: [{String.t(), {:string | :expr, String.t()}}]
attrs/2 for the form node, and its phx-trigger-action under the new seed when it sends to
another address (PageForms.sends_elsewhere?/1), so a rename or a renumber keeps that working.
What every writer of the five names writes.
@spec numbered_name(pos_integer() | String.t()) :: String.t()
The name of form number number, as a person reads it: "Form 3".
The name a site admin reads for a form's seed: form is "Form 1", form_3 is "Form 3", and any
other seed is its words with a capital ("contact_us" is "Contact us"). nil for nil.
@spec rekey(MishkaCmsCore.Runtime.PageBuilder.Tree.t(), Enumerable.t()) :: MishkaCmsCore.Runtime.PageBuilder.Tree.t()
Re-keys a field that arrived carrying a key the form it landed in already uses.
The same rule forms get, applied to the keys inside one. MishkaCmsCore.Runtime.PageBuilder.Tree
copies every attribute verbatim on a duplicate or a paste, so duplicating a field gave several
inputs one key: all of them render name="contact[email]", typing in one shows that value in all
of them, only the last one submitted survives, and the spec carries a single field — with nothing
in the audit rail to say so. Duplicating a field is how a non-programmer builds the second one.
Every node bound to the colliding key moves together, because a label and its input are one field and re-keying the input alone would point the label at a field that no longer exists.
A whole form arriving is not a collision. Each form scopes its own params, so the copy keeps every
key it had and only dedupe/2 renames it.
@spec retokened(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), String.t()) :: [ {String.t(), {:string, String.t()}} ]
The event attributes of node rewritten to carry token, ready for
MishkaCmsCore.Runtime.PageBuilder.Tree.set_attr/4.
Only names this module writes change: a tagged name keeps its own seed and kind, and an untagged
<seed>_validate/<seed>_submit of the form's own seed gets the token. Any other name is a
developer's and is left out.
Whether two event names carry the same token and the same kind, whatever their seeds.
Whether two event names carry the same token, so they belong to the same form.
The seed a label mints: lowercased, accents folded away, every other run of characters turned into
a single _, and never starting with a digit.
A label that leaves nothing usable — blank, punctuation only, or written in a script that has no
ASCII form — falls back to form, because every one of the five derived names has to
exist.
@spec seeds(MishkaCmsCore.Runtime.PageBuilder.Tree.t()) :: [{String.t(), String.t()}]
Every form already in this tree, as {node id, seed} in document order.
The seeds are PARSED off the nodes rather than counted, so a page whose only form is contact_2
answers contact_2 and the next one minted is not the same name again.
A node is a form when MishkaCmsCore.Runtime.PageBuilder.PageForms.form?/1 says so — it holds the
page assign its values live in. Asking only for an as would take as at its HTML word, and as
is an ordinary attribute of an ordinary tag:
<link rel="preload" as="font" href="/inter.woff2" />Two of those on a page read as two forms both seeded font, and dedupe/1 renames the second by
writing five form attributes onto a <link> — breaking the preload, minting a form nobody placed,
and raising a duplicate-seed blocker about it. It runs on every insert, duplicate, wrap AND on
every save, draft restore and history revert, so the wreckage is what gets stored.
The token inside a tagged event name, or nil.
The phx-trigger-action a form sending to another address carries: the page assign
MishkaCmsCore.Runtime.PageForm.submit/5 sets once its answers are checked, assigns[:contact_send]
for the seed contact.
@spec unique(MishkaCmsCore.Runtime.PageBuilder.Tree.t(), String.t() | nil) :: String.t()
A seed based on base that no form in tree is using.
base may be an author's label; it is minted through seed/1 first, so the answer is always a
legal name.