MishkaCmsCore. Runtime. PageBuilder. PageForms. FormPlacement
(Mishka CMS Core v0.0.1)
Copy Markdown
View Source
Wraps a field dropped with no form around it, and binds a field dropped inside one.
Prevention rather than a report, because "this field is not inside a form" is a mistake with
exactly one sensible repair and no reason to make the author read about it. One template rewrite,
so one :insert op and one undo step.
It is deliberately NOT a MishkaCmsCore.Runtime.PageBuilder.Containment refusal. Building a form
top-down — three fields first, wrapper after — is an ordinary gesture, and refusing it turns the
intermediate state of a legal workflow into an error message. Containment refuses only the
nesting HTML itself forbids.
What gets a binding
A component the kit calls a field — MishkaCmsCore.Runtime.PageBuilder.Fields.field_node?/2, the
editor's one answer to that, which reads the kit's own declaration first — is bound where it
stands. Anything else is asked of the controls UNDER it, because a template is one field rather
than one node: el-checkbox is a <label> over a hidden input and a checkbox, el-radio-group a
<fieldset> over its options, and in both the head carries no value at all while every control
beneath it answers to one name.
Asking only the head left those two never bound. They kept their literal name, so the browser
posted agree at the event's TOP level while MishkaCmsCore.Runtime.PageForm.scoped/2 reads only
params["contact"] — the consent tick was dropped from every submission, the spec claimed a key no
input carried, and nothing in the audit rail said so.
One name is one field, and several names are several fields the drop has no business choosing between, so a head over controls with different names is left exactly as it arrived. So is a template that brings a form of its own, and a head already carrying a binding.
The attribute the binding is written on is the kit's, not this module's: Fields.binding_attr/1
names the component's Phoenix.HTML.FormField attribute whatever the kit called it, and a kit that
takes no such struct gets the Fields.pair_attrs/1 pair instead — name={f[:email].name} and
value={f[:email].value}. A raw <input name="field"> from the Elements library takes the same
pair, which is what turns a palette template that has to render standalone into a real field on
the page.
A value the author wrote is kept
value="one" on a radio, value="true" on a checkbox and the value="false" on the hidden input
beside it are the option's identity, not the box the visitor types in. Binding them would give
every option in a group the form's own value and leave the submission with nothing to tell them
apart, so a control that already carries a written value takes the name binding alone. A palette
card's seeded blank is not a value and is superseded as it always was.
Where the key comes from
MishkaCmsCore.Runtime.PageBuilder.Fields.field_label/2 — the author's own words where there are
any, asked of the kit's own attributes rather than of Phoenix's spelling — minted through
MishkaCmsCore.Runtime.PageBuilder.PageForms.Naming.field_key/2 so it is legal and unique inside
the form it is joining. Two email fields in one form are email and email_2, never one key with
two inputs writing over each other.
What is already taken is MishkaCmsCore.Runtime.PageBuilder.PageForms.names/2, the text half:
every drop used to intern an atom per name already in the form, from markup an author is still
typing, only for the minting to compare them as strings.
Where a field lands
place/3 is what every placement asks. A field joins the form it is dropped in or against, the
form's own top or bottom edge included; anywhere else it starts a new form that comes with the
styled Send button. A field never lands after its form's first Send button: a spot past it is moved
in front of it (landing/4), wherever in the form the Send button sits. A kit wrapper that hands
its form to its content arrives wired on a Page (wiring/4).
Moving a field between forms
unit/3 is the whole field a node belongs to (a checkbox's label with its hidden twin, a radio
group), move_ops/3 moves it to another form or into a new one and merge_ops/3 moves every field
of a form into another. A form left with no fields is removed in the same step.
See MishkaCmsCore.Runtime.PageBuilder.PageForms for what makes a node a form in the first place.
Summary
Functions
Whether a template being dragged in is a field: its head is one, or the controls under it answer to one name.
Whether the form form can be made to work as they type: a working form on a Page that passes
the attribute recording it (PageForms.as_typed_attr/0) on to what it renders. The Form panel's
How it works, the audit's "Make it work as they type" and how_ops/3 all ask this.
The template with its field bound to the form it is landing in, or the template untouched.
How binding node to key changes it: what to set, and what that supersedes.
Whether this node in the document is part of a field (unit/3 answers for it).
tree with multipart turned on (files_ops/3) for every form the node node_id sits in, is or
holds: what a node put or moved there asks of the form it lands in.
The op that turns multipart on for form when a file field sits inside it in tree, so the
files chosen there are sent with it; [] when there is none, the form already carries it, or it
does not pass multipart on.
A working form's people name (Naming.people_name/1 of its seed), or nil.
The form node sits in, or nil.
Every working form in tree a field can join, as {node id, people name} in document order.
The ops that switch how the working form form_id sends, as one undo step, and the sentence the
author is told.
Where a field added at the end of form lands, as {parent id, index}: in front of the form's
first Send button, wherever inside the form it sits, or at the end when there is none.
What the canvas marks while a field is dragged: forms maps every form's node id to its people
name, "" for a form that does not work yet, and landings holds each form's first Send button,
which a field dropped at the form's end lands in front of (landing/4).
The ops that move every field of the form from_id into the form into_id, in order and in front
of its first Send button, then remove from_id; one undo step, and the sentence the author is
told.
The ops that move the field node_id belongs to (unit/3) into another form, as one undo step,
and the sentence the author is told.
How a drag of the existing node params["node_id"] lands, when it is a field on a Page.
The target that asks move_ops/3 for a new form.
Whether this section's compiler emits __page_forms__/0 — whether a form written here could ever
reach a destination.
A form as a sentence names it: "Form 2", "the Contact form", or "the form" when it has no name.
The template to insert, the spot it lands in and what was done about forms.
The ops that replace the raw <form> node_id with a working <.form> in one undo step: the
same attributes and children, then wiring/4. The form keeps its own id, so what already points
at it still does. A form that posts to its own action is a developer's and is refused.
Whether replace_ops/3 can make the raw <form> node a working form.
The styled Send button a form is given: the Elements palette's el-submit, read by id so every
place that adds one inserts the same markup.
What the author is told after a placement, or nil when there is nothing to say.
The whole field node belongs to, or nil when it is not part of one: the smallest element
holding every control of its key in its form (a checkbox's label with the hidden twin, a radio
group), widened inside a form to the highest element below the form that holds nothing of another
field (its label, its column), and outside every form only to a <label> or <fieldset> around
it. Never the form itself, never a slot, and never anything that also holds another field or a
Send button.
Every field of form as its unit/3, outermost first, in document order.
Whether Make it a working form can make the idle form node one: it posts to no web address of
its own, and it hands its form to its content (wiring/4) or is a plain HTML form
(replace_ops/3).
The ops that make an idle form-lending node (Fields.hands_form?/2) a working form: :let, the
five names with a fresh token, and a binding for every field inside it, ready for
MishkaCmsCore.Runtime.PageBuilder.EditorSession.apply_ops/4.
The template to insert, and whether a form was put around it.
Types
@type outcome() :: :none | {:joined, String.t() | nil} | :started | :wired
What a placement did about forms.
Functions
@spec arriving_field?( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()] ) :: boolean()
Whether a template being dragged in is a field: its head is one, or the controls under it answer to one name.
@spec as_typed_offered?(map(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil) :: boolean()
Whether the form form can be made to work as they type: a working form on a Page that passes
the attribute recording it (PageForms.as_typed_attr/0) on to what it renders. The Form panel's
How it works, the audit's "Make it work as they type" and how_ops/3 all ask this.
The template with its field bound to the form it is landing in, or the template untouched.
Untouched covers every case that is not a field joining a form: a block that brings its own form,
a drop with no form above it, a field already carrying a binding, and a form whose :let variable
is missing — binding to a variable the form does not bind would not compile.
@spec binding( MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()], atom(), atom() | String.t() ) :: [{:set, String.t(), {:expr | :string, String.t()}} | {:drop, String.t()}]
How binding node to key changes it: what to set, and what that supersedes.
{:set, name, source} and {:drop, name}, in the order they must be applied. The caller renders
them in whatever shape it applies ops in — this module writes straight to a tree, the audit's Fix
builds MishkaCmsCore.Runtime.PageBuilder.Op structs — but WHICH attributes move is decided once,
here, because it was decided twice and the two answers drifted.
Why anything is dropped
A palette card is seeded name="" and value="" so it renders standing alone on a canvas, and a
kit derives both from the form field with assign_new/3, which fills only an ABSENT key. Leave the
empty pair beside the binding that replaces it and the empty pair wins: the input renders name=""
and submits nothing at all, while the page validates a key no input carries.
Why the value is sometimes left
node rather than its palette entry, because the entry cannot answer this: a control the author
already gave a written value is a radio option, a checkbox's true or the hidden false beside
it, and the value there is the option's identity. It keeps it and takes the name binding alone.
@spec field?( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()] ) :: boolean()
Whether this node in the document is part of a field (unit/3 answers for it).
@spec filed(MishkaCmsCore.Runtime.PageBuilder.Tree.t(), String.t() | nil, [map()]) :: MishkaCmsCore.Runtime.PageBuilder.Tree.t()
tree with multipart turned on (files_ops/3) for every form the node node_id sits in, is or
holds: what a node put or moved there asks of the form it lands in.
@spec files_ops( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()] ) :: [map()]
The op that turns multipart on for form when a file field sits inside it in tree, so the
files chosen there are sent with it; [] when there is none, the form already carries it, or it
does not pass multipart on.
@spec form_name(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil) :: String.t() | nil
A working form's people name (Naming.people_name/1 of its seed), or nil.
@spec form_of( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil, [map()] ) :: MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil
The form node sits in, or nil.
@spec forms(MishkaCmsCore.Runtime.PageBuilder.Tree.t()) :: [{String.t(), String.t()}]
Every working form in tree a field can join, as {node id, people name} in document order.
@spec how_ops(map(), String.t(), :typing | :send) :: {:ok, [map()], String.t()} | {:error, String.t()}
The ops that switch how the working form form_id sends, as one undo step, and the sentence the
author is told.
:typingremoves every Send button inside the form and records the choice on it (PageForms.as_typed_attr/0). Itsphx-submitstays, so Enter never reloads the page. A formas_typed_offered?/2refuses is refused with a sentence.:sendremoves the record and, when no Send button is left, adds the styled one (send_button/0) at the end of the form's content. The form stays selected.
@spec landing( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()], keyword() ) :: {String.t(), non_neg_integer()}
Where a field added at the end of form lands, as {parent id, index}: in front of the form's
first Send button, wherever inside the form it sits, or at the end when there is none.
A Send button inside one of the form's own slots (a kit wrapper's <:actions>) is outside the
form's :let, so the field lands at the end of the form's content instead, after its last child
that is not a slot.
Options
:dragged- the node, or nodes, arriving, so a spot whose parent refuses any of them is passed over for the next one out.:dragged_tree- the tree:draggedbelongs to.
@spec marks(MishkaCmsCore.Runtime.PageBuilder.Tree.t(), [map()], map()) :: {%{forms: map(), landings: MapSet.t()}, map()}
What the canvas marks while a field is dragged: forms maps every form's node id to its people
name, "" for a form that does not work yet, and landings holds each form's first Send button,
which a field dropped at the form's end lands in front of (landing/4).
memo is the roles already worked out, keyed by component name; the answer carries it grown.
The ops that move every field of the form from_id into the form into_id, in order and in front
of its first Send button, then remove from_id; one undo step, and the sentence the author is
told.
The ops that move the field node_id belongs to (unit/3) into another form, as one undo step,
and the sentence the author is told.
target is a working form's node id, or new_target/0 for a new form with a Send button placed
after the form the field leaves. Every control of the field is bound to the form it joins, under a
key that form is not using. A form left with no fields is removed in the same step.
How a drag of the existing node params["node_id"] lands, when it is a field on a Page.
params["index"] is counted as Tree.move/4 counts it, with the dragged node already out of its
row.
{:plain, params} is an ordinary move, its spot kept in front of its form's Send button. A field
dragged into another form or out of every form answers {:run, ops, sentence}: its whole field
(unit/3) moved and rebound into the other form, or a new form with a Send button around it, as
one undo step. A form that binds no variable takes the field as it is.
@spec new_target() :: String.t()
The target that asks move_ops/3 for a new form.
Whether this section's compiler emits __page_forms__/0 — whether a form written here could ever
reach a destination.
Total: anything without a section is not a page.
A form as a sentence names it: "Form 2", "the Contact form", or "the form" when it has no name.
The template to insert, the spot it lands in and what was done about forms.
params carries "parent_id" and "index", and "anchor_id"/"anchor_before" when the drop
was aimed beside a node. On a Page a field joins the form it lands in or is dropped against (the
answer's params then name the spot inside that form), and anywhere else it starts a new form with
a Send button; a kit wrapper that hands its form to its content arrives wired, unless it already
names its own phx-submit. Everywhere else a field is only bound to a form the author wrote
around it.
The ops that replace the raw <form> node_id with a working <.form> in one undo step: the
same attributes and children, then wiring/4. The form keeps its own id, so what already points
at it still does. A form that posts to its own action is a developer's and is refused.
Options
:events- the page's Events-tab entries. Defaults to[].
@spec replaceable?(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: boolean()
Whether replace_ops/3 can make the raw <form> node a working form.
@spec send_button() :: String.t()
The styled Send button a form is given: the Elements palette's el-submit, read by id so every
place that adds one inserts the same markup.
What the author is told after a placement, or nil when there is nothing to say.
@spec unit( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()] ) :: MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil
The whole field node belongs to, or nil when it is not part of one: the smallest element
holding every control of its key in its form (a checkbox's label with the hidden twin, a radio
group), widened inside a form to the highest element below the form that holds nothing of another
field (its label, its column), and outside every form only to a <label> or <fieldset> around
it. Never the form itself, never a slot, and never anything that also holds another field or a
Send button.
@spec units( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()] ) :: [MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()]
Every field of form as its unit/3, outermost first, in document order.
@spec wirable?(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil, [map()]) :: boolean()
Whether Make it a working form can make the idle form node one: it posts to no web address of
its own, and it hands its form to its content (wiring/4) or is a plain HTML form
(replace_ops/3).
@spec wiring( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()], keyword() ) :: {:ok, [map()]} | {:error, String.t()}
The ops that make an idle form-lending node (Fields.hands_form?/2) a working form: :let, the
five names with a fresh token, and a binding for every field inside it, ready for
MishkaCmsCore.Runtime.PageBuilder.EditorSession.apply_ops/4.
tree is the page the names must be unique in. A seed the node already carries in as is kept.
A phx-submit the page's code already answers (:events) is the developer's: then only :let,
for and as are written, and its fields keep the names that code reads. A form that posts to
its own action is a developer's and is refused.
Options
:within- the treenodesits in, when it is nottree(a template on its way in).:events- the page's Events-tab entries. Defaults to[].:seed- the name to start from when the node carries noas. Defaults to"form".:keep_id-truekeeps anidthe node already has. Defaults tofalse.
The template to insert, and whether a form was put around it.
parent_id is where the drop is about to land, so the question "is there a form here" is asked of
the page rather than of the template being dropped. A form put around a field comes with the
styled Send button.