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

Types

What a placement did about forms.

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

outcome()

@type outcome() :: :none | {:joined, String.t() | nil} | :started | :wired

What a placement did about forms.

Functions

arriving_field?(tree, head, components)

Whether a template being dragged in is a field: its head is one, or the controls under it answer to one name.

as_typed_offered?(session, form)

@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.

bind(template, session, parent_id)

@spec bind(String.t(), map(), String.t() | nil) :: String.t()

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.

binding(node, components, var, key)

@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.

field?(tree, node, components)

Whether this node in the document is part of a field (unit/3 answers for it).

filed(tree, node_id, components)

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.

files_ops(tree, form, components)

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.

form_name(node)

@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.

form_of(tree, node, components)

The form node sits in, or nil.

forms(tree)

Every working form in tree a field can join, as {node id, people name} in document order.

how_ops(session, form_id, how)

@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.

  • :typing removes every Send button inside the form and records the choice on it (PageForms.as_typed_attr/0). Its phx-submit stays, so Enter never reloads the page. A form as_typed_offered?/2 refuses is refused with a sentence.
  • :send removes 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.

landing(tree, form, components, opts \\ [])

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 :dragged belongs to.

marks(tree, components, memo \\ %{})

@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.

merge_ops(session, from_id, into_id)

@spec merge_ops(map(), String.t(), String.t()) ::
  {:ok, [map()], String.t()} | {:error, String.t()}

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.

move_ops(session, node_id, target)

@spec move_ops(map(), String.t(), String.t()) ::
  {:ok, [map()], String.t()} | {:error, String.t()}

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.

moving(session, params)

@spec moving(map(), map()) :: {:plain, map()} | {:run, [map()], String.t()}

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.

new_target()

@spec new_target() :: String.t()

The target that asks move_ops/3 for a new form.

page_forms?(arg1)

@spec page_forms?(map()) :: boolean()

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.

phrase(name)

@spec phrase(String.t() | nil) :: String.t()

A form as a sentence names it: "Form 2", "the Contact form", or "the form" when it has no name.

place(template, session, params)

@spec place(String.t(), map(), map()) :: {String.t(), map(), outcome()}

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.

replace_ops(session, node_id, opts \\ [])

@spec replace_ops(map(), String.t(), keyword()) ::
  {:ok, [map()]} | {:error, String.t()}

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 [].

replaceable?(node)

Whether replace_ops/3 can make the raw <form> node a working form.

send_button()

@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.

told(arg1)

@spec told(outcome()) :: String.t() | nil

What the author is told after a placement, or nil when there is nothing to say.

unit(tree, node, components)

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.

units(tree, form, components)

Every field of form as its unit/3, outermost first, in document order.

wirable?(node, components)

@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).

wiring(tree, node, components, opts \\ [])

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 tree node sits in, when it is not tree (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 no as. Defaults to "form".
  • :keep_id - true keeps an id the node already has. Defaults to false.

wrap(template, session, parent_id)

@spec wrap(String.t(), map(), String.t() | nil) :: {String.t(), boolean()}

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.