# `StatifierUI.Live.ExpressionInput`
[🔗](https://github.com/riddler/statifier-ui/blob/v0.11.0/lib/statifier_ui/live/expression_input.ex#L2)

An expression field with completion: predicator's grammar and the host's
declared datamodel paths, offered at the caret.

This is the affordance statifier_blocks ADR-0005 decision 15 defers here -
"Rich expression editing is statifier-ui's" - and it is written to drop
straight into that package's `expression_component` seam:

    <StatifierBlocks.Editor.editor
      ...
      expression_component={&StatifierUI.Live.ExpressionInput.expression_input/1}
      path_candidates={StatifierBlocks.Datamodel.candidates(document, datamodel)}
    />

Nothing here depends on statifier_blocks, and nothing here knows what a
block is. The seam hands this component a five-key map - `:field`, `:id`,
`:name`, `:value`, `:candidates` - and `:field` is the one key it
deliberately ignores: reading it would mean knowing that package's view
model, and ADR-0004's dependency arrow points the other way. Any host with
an expression to edit can call it the same way.

## The assigns arrive as a bare map

The seam calls the override as a plain one-argument function rather than
through a HEEx component tag, so `attr` defaults never run. Every assign
outside those five keys is defaulted here, in `normalize/1`, which is what
makes the same function usable both ways - `<.expression_input .../>` in a
template and `component.(%{...})` from the seam.

## Edits round-trip through the host's own form

The rendered `<input>` carries the `name` it was handed and nothing else:
no `phx-change`, no `phx-target`, no event of its own. In the sb editor
that input sits inside `<form phx-change="config-change">`, so an edit is
already the host's event, arriving with every other field. The hook keeps
that true - after it writes a completion in, it dispatches a bubbling
`input` event, so a keyboard-driven completion and a typed character are
the same event to the host.

Because the event is the host's, so is how often it fires: a `:debounce`
assign is written verbatim as `phx-debounce` onto every form control this
component draws - the source input and each picklist row control - and no
key at all renders no attribute, which is byte for byte what this
component rendered before the assign existed. The sb seam supplies it from
the same `debounce` its own fields carry, so an `:expression` field
rendered through the seam debounces the way every other field in that form
does.

## Two modes, and the source text is the only representation

The field has a **picklist mode** beside its text mode. On every render the
source is classified by `StatifierUI.Expression.simple/2`: inside the
picklist-renderable subset it draws one row of dropdowns per clause -
field, operator, value - with a connective toggle and an add-clause button;
outside it it draws the text input. Source that does not parse is a third
answer rather than the second: the text input plus the parse error's own
position, per ADR-0007's picklist amendment. It never refuses a source
string and never rewrites one.

**Every control writes source text, and only source text.** There is no
structured model held anywhere: each `<option>`'s value *is* the complete
expression source that choosing it produces, written by
`Predicator.Simple.to_source/1` through `StatifierUI.Expression.source/2`,
and picking one copies that string into the same named `<input>` the text
mode edits. The two controls that have to compose rather than choose - a
free-text value and a multi-select list - are handed a source *template*
with the value's own spelling standing in it, plus the quoting, escaping
and list punctuation the writer uses, all measured off
`StatifierUI.Expression.value_source/2` rather than written out in
JavaScript. Nothing in the browser knows how predicator spells anything.

That is the load-bearing property. A picklist that kept clause rows as its
own state would be the second source of truth ADR-0007 rules out for the
diagram, arrived at through a form instead of a canvas.

## Switching modes

The switch to text is always offered; the switch to picklists is offered
only while the current text is inside the subset - an author is never
invited into a mode that cannot draw what they have written. The choice is
the viewer's and lives in the hook, so a re-render from the host does not
drag an author back out of the mode they picked. The server decides only
the *default*, which is picklists when the source is inside the subset.

The two switches sit in a `statifier-ui-expression-modes` container, and
that container is the one element in this package that carries a style of
its own: an inline `display: inline-flex; gap: 0.5rem`. HEEx drops the
whitespace between two adjacent elements, so on a host page with no
stylesheet the two buttons abutted and read as one run-together phrase
(sui-aln). The default is layout only - no colour, no font, no token to
reconcile with a host's palette, so the theming contract in
`docs/ops-embedding.md` still holds. A host restyles the container through
the class as usual; because the default is an attribute it wins on
specificity, so a rule that replaces `display` or `gap` rather than adding
to them needs `!important`.

Picklist mode needs the `StatifierUIExpressionPicklist` hook, because a
control that writes source text has to write it into the input; a host
that registers no hook gets the text field alone, the same degradation the
completion popup makes.

## Operator labels are display, the source is not

A dropdown shows `is one of` where the writer spells `IN`. The label is a
display string; the value stored is always the writer's own spelling,
untouched. Since the re-pin to a predicator carrying
`Predicator.Vocabulary`'s human labels (px-84i), operator labels are the
grammar's own phrases, delivered by `StatifierUI.Expression.operators/1` as
the `:label` beside the writer's `:lexeme` - so this module no longer makes
them, and no longer cases one either: every operator option it renders
carries the grammar's `:label` verbatim, so one spelling of a display
phrase exists in the system and it is the vocabulary's. The
`display_label/1` that used to lowercase a word-shaped lexeme is gone with
the job it did (sui-ne0). Its one caller was `op_options/5` here; the
grammar's phrases are already display-cased, and every one of them is
either multi-word or already lowercase, so it returned every label it
could be handed unchanged.

## Two affordances, and the second one is optional

Without JavaScript the field is a text input bound to a `<datalist>` of the
word-shaped completions - the same affordance sb's plain control gives for
paths, widened to the grammar. With `assets/js/expression_input.js`
registered as the `StatifierUIExpressionInput` hook (ADR-0009: the
JavaScript ships as source and the host's bundler compiles it), the hook
drops the datalist and offers a caret-aware list instead: the token under
the cursor is the prefix, arrow keys move, Enter or Tab inserts.

A host that registers no hook loses the popup and keeps the field. A
predicator without `Predicator.Vocabulary` loses the grammar entries and
keeps the declared paths. Both states are stamped on the element rather
than inferred - `data-hook` and `data-vocabulary` - so a page that offers
nothing says which of the two reasons it is.

## What it stamps

Per ADR-0007's data-attribute contract, the rendered structure is the
testable surface:

| Attribute | On | Meaning |
|---|---|---|
| `data-completions` | the input | the full completion list as JSON, the hook's whole input |
| `data-completion-count` | the input | how many were offered |
| `data-vocabulary` | the input | whether the grammar half resolved |
| `data-candidates` | the input | how many declared paths were supplied |
| `data-expression-source` | the input | marks the one element holding the source |
| `data-mode` | the wrapper | `picklist` or `text`, the mode rendered |
| `data-subset` | the wrapper | `inside`, `outside`, or `error` |
| `data-error-position` | the diagnostic | `line:column` of a parse failure |
| `data-clause-count` | the wrapper | how many clause rows were drawn |
| `data-clause-index` | a clause row | its position, from zero |
| `data-role` | a picklist control | `path`, `operator`, `value`, `connective` |
| `data-action` | a button | `add-clause`, `remove-clause`, `switch-text`, `switch-picklist` |
| `data-declared-kind` | a clause row | the kind the host declared for its path (`integer`, `list:string`, `one-of`, `shape`, ...), absent when none |
| `data-advisory` | an advisory row | `value-kind` or `operator`, why it is shown |
| `data-severity` | an advisory row | `info` - an advisory never blocks |

## What a declaration does and does not do

A `:path_types` entry decides three things: which operators the row offers
(asked of `StatifierUI.Expression.operators/1`, the same grammar call an
observed kind goes through), which control is drawn, when the value's
own shape agrees with the declaration, and what literal a *new* row is
seeded with. It never rewrites the author's source: the operator the source
carries is always offered even when the declared kind's list would drop it,
the value in the source is always kept and selected, and a scalar value is
never handed a list control (or the reverse). When the declaration and the
source disagree, an advisory row renders beside the clause and nothing
about the source changes.

Seeding is the one place a declaration writes rather than describes, and it
writes only where there was nothing: the row the "add clause" button
creates. A row seeded with `''` on a path declared a number would arrive
carrying an advisory about itself, which is a declaration telling an author
off for the shape it chose (sui-loj). So a declared number seeds `0`, a
boolean `true`, a date today, a datetime midnight today, a duration `1d`, a
`{:one_of, _}` its first value, and a list declaration seeds a `CONTAINS`
clause holding one member. A path with no declaration - and a `{:list, nil}`
that names no member kind - keeps the empty string this component always
seeded.

## Two ways to declare, and the map wins

A host that has a datamodel *document* may hand it over as `:document`
instead of projecting it itself: the component asks
`StatifierDatamodel.Index.path_types/1` for the same `path -> kind` map
that record defines, and uses it wherever a `:path_types` map was not
supplied. A non-empty `:path_types` wins whole, because a host that
supplies both has said the more specific thing.

Everything the projection answers *is* a `:path_types` entry, so nothing is
translated between them - which is why
`t:StatifierUI.Expression.declared_kind/0` admits `:number`, the tag that
projection answers for a document's `integer` and `decimal` alike. The map
is the wider of the two by exactly one entry, and the next section is that
entry.

## An inline shape, and where one can come from

`:path_types` admits one entry the projection never answers:
`{:shape, members}`, sd's inline arm. An inline shape has no document
spelling, so `StatifierDatamodel.Index.path_types/1` cannot produce one and
a `:document` alone never carries one; it arrives only when a consumer
holding a structural value the host never declared - `statifier_blocks`'
environment, or a host with its own reader - builds the map itself.

It is offered and typed one segment down. Each member becomes a path the
field offers, after the path that declares it and through nesting, and a
read of one is typed by that member - `card.brand` takes the `brand`
member's type where `card` is declared a shape, with an exact `:path_types`
entry for the longer path still winning. The path holding the shape itself
declares nothing: a structure is not a kind the grammar compares, so its
row offers the operators and the control an undeclared path offers and
raises no advisory, while `data-declared-kind` still says `shape` so a
renderer can tell the two apart. A member the shape does not promise is
offered and typed like any other: the promise says what may be missing at
run time, not which paths exist.

# `expression_input`

```elixir
@spec expression_input(map()) :: Phoenix.LiveView.Rendered.t()
```

The field: the picklist rows, the text input, and the switch between them.

See the module doc for the seam this is written against, for why every
control writes source text, and for what the rendered element stamps.

## Attributes

* `id` (`:string`) (required) - DOM id of the input itself.
* `name` (`:string`) (required) - form field name; the host's form owns the event.
* `value` (`:string`) - the expression source. Defaults to `""`.
* `candidates` (`:list`) - declared datamodel paths, from the host. Offered ahead of the grammar. Defaults to `[]`.
* `value_candidates` (`:map`) - values the host offers per clause path, as `%{path => [candidate]}`. Only the host knows its own value sets; a path with no entry gets a free-text control. Defaults to `%{}`.
* `path_types` (`:map`) - kinds the host declares per clause path, as `%{path => kind | {:list, kind} | {:one_of, values} | {:shape, members}}`. A declared kind decides the operator list and the value control; it never rewrites the author's source. A `{:shape, _}` entry offers and types its members one segment down, and declares nothing at the path holding it. Defaults to `%{}`.
* `document` (`:map`) - a decoded datamodel document. Its `StatifierDatamodel.Index.path_types/1` projection supplies `:path_types` when the host declares none directly; a non-empty `:path_types` wins over it. Defaults to `nil`.
* `mode` (`:atom`) - which mode to render first. `:auto` picks picklists when the source is inside the subset. Defaults to `:auto`. Must be one of `:auto`, `:text`, or `:picklist`.
* `hook` (`:string`) - `phx-hook` name; `nil` renders the datalist-only field. Defaults to `"StatifierUIExpressionInput"`.
* `picklist_hook` (`:string`) - `phx-hook` name for the picklist. Defaults to the shipped hook, or to `nil` when `:hook` is `nil` - a host registering no hooks has none of this package's JavaScript. Defaults to `nil`.
* `placeholder` (`:string`) - Defaults to `"an expression"`.
* `class` (`:string`) - Defaults to `nil`.
* `vocabulary_opts` (`:list`) - passed to `Predicator.Vocabulary.functions/1` - a host's own providers. Defaults to `[]`.
* `debounce` (`:any`) - What `phx-debounce` the controls this component draws carry, or `nil`
  for none. Written verbatim onto every form control it renders - the
  source input and each picklist row control - so the accepted values are
  LiveView's own: milliseconds as an integer or a string, or `:blur` for
  "post when the control loses focus".

  `nil` renders no attribute at all, which is both what this component
  rendered before the assign existed and the behaviour LiveView gives a
  control with no `phx-debounce`: post every change event as it happens.
  There is deliberately no non-`nil` default - the enclosing form owns the
  event, so how often it fires is the host's decision rather than this
  component's.

  Defaults to `nil`.
* `field` (`:any`) - accepted from the sb seam and never read. Defaults to `nil`.

# `hook_name`

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

The hook name a host registers for the completion popup:
`"StatifierUIExpressionInput"`.

It is the name `assets/js/index.js` exports and the name this component
renders as `phx-hook`, returned as a function so a host's `app.js` and a
test can name the same string without copying it.

## Examples

    iex> StatifierUI.Live.ExpressionInput.hook_name()
    "StatifierUIExpressionInput"

# `picklist_hook_name`

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

The hook name a host registers for the picklist:
`"StatifierUIExpressionPicklist"`.

The second of the two hooks `assets/js/index.js` exports. It is what
copies a chosen option's source string into the field and dispatches the
`input` event the host's form is already listening for.

## Examples

    iex> StatifierUI.Live.ExpressionInput.picklist_hook_name()
    "StatifierUIExpressionPicklist"

---

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