Flicker.FacetEditor behaviour (Flicker v0.1.0)

Copy Markdown View Source

The contract every rich facet editor implements (Spec 019).

Per ADR-011 an editor is a modal sub-context whose only output is token text. It holds no committed state, it never queries on its own, and it commits exactly once — when its value is complete. Everything it can express, a user can type; everything a user can type, it can be opened on. That symmetry is what keeps Flicker.Query.input the single source of truth.

The callbacks

  • serialise/2 — the value's canonical token text, without the key: prefix or a trailing space (the component adds both). Locale- invariant, per ADR-013.
  • parse/2 — the inverse: an editor's own value from a token's value text, for reopening a committed facet.
  • complete?/2 — whether this value may commit yet. A half-made range is a draft, and no route (Enter, click, Done) may commit one.
  • render/1 — the editor's markup.
  • draft_label/3 — optional one-line summary of an incomplete value, for the footer ("Jun 18, 2026 → pick an end date").
  • modal?/0 — optional; false for an editor whose value is complete in one interaction and so needs no pop-out. The switch is the only one.

serialise/2 and parse/2 are a round-trip pair, and the property that pair must satisfy is the load-bearing test of this spec:

serialise(parse(serialise(value))) == serialise(value)

Note that this is idempotence of serialise/2, not structural identity of the value — deliberately. A preset serialises to its id (last-30-days) and parses back resolved (a %Range{} with real endpoints and the preset id), which is the whole point of presets staying relative. The two forms mean the same thing and serialise identically; they are not the same struct. For every non-preset value the property does collapse to plain identity.

The second half matters just as much: the text an editor emits must re-parse to a real filter through Flicker.Query.parse/2. An editor that drifts from the grammar fails that, which is what makes token text a safe sole output.

Summary

Types

An editor's own working value. Its shape is the editor's business.

Callbacks

Whether value is complete enough to commit.

A one-line summary of an incomplete value, or nil.

Whether this editor needs a modal pop-out. Defaults to true.

An editor value from a token's value text, for reopening a committed facet.

The editor's markup.

Canonical token text for value — no key: prefix, no trailing space.

Functions

Whether facet has a rich editor at all.

The editor for facet — its own :editor when set, otherwise the default for its type, or nil for a type that has none (a plain :string facet, or an unbounded numeric one, where typing straight through beats any control).

Whether opening facet's editor should take over the surface with a pop-out.

The full token an editor's value commits as — key:value — or nil when the value isn't complete.

Types

value()

@type value() :: term()

An editor's own working value. Its shape is the editor's business.

Callbacks

complete?(value, t)

@callback complete?(value(), Flicker.Facet.t()) :: boolean()

Whether value is complete enough to commit.

draft_label(value, t, keyword)

(optional)
@callback draft_label(value(), Flicker.Facet.t(), keyword()) :: String.t() | nil

A one-line summary of an incomplete value, or nil.

modal?()

(optional)
@callback modal?() :: boolean()

Whether this editor needs a modal pop-out. Defaults to true.

parse(t, t)

@callback parse(String.t(), Flicker.Facet.t()) :: {:ok, value()} | :error

An editor value from a token's value text, for reopening a committed facet.

render(assigns)

@callback render(assigns :: map()) :: Phoenix.LiveView.Rendered.t()

The editor's markup.

serialise(value, t)

@callback serialise(value(), Flicker.Facet.t()) :: String.t()

Canonical token text for value — no key: prefix, no trailing space.

Functions

editable?(facet)

@spec editable?(Flicker.Facet.t()) :: boolean()

Whether facet has a rich editor at all.

Examples

iex> Flicker.FacetEditor.editable?(Flicker.Facet.new(key: :created, type: :date_range))
true

iex> Flicker.FacetEditor.editable?(Flicker.Facet.new(key: :name, type: :string))
false

for_facet(facet)

@spec for_facet(Flicker.Facet.t()) :: module() | nil

The editor for facet — its own :editor when set, otherwise the default for its type, or nil for a type that has none (a plain :string facet, or an unbounded numeric one, where typing straight through beats any control).

Examples

iex> Flicker.FacetEditor.for_facet(Flicker.Facet.new(key: :created, type: :date_range))
Flicker.FacetEditor.Calendar

iex> Flicker.FacetEditor.for_facet(Flicker.Facet.new(key: :verified?, type: :boolean))
Flicker.FacetEditor.Switch

iex> Flicker.FacetEditor.for_facet(Flicker.Facet.new(key: :status, type: :enum, values: [:a]))
Flicker.FacetEditor.Set

iex> Flicker.FacetEditor.for_facet(Flicker.Facet.new(key: :name, type: :string))
nil

iex> Flicker.FacetEditor.for_facet(Flicker.Facet.new(key: :n, type: :integer))
nil

iex> Flicker.FacetEditor.for_facet(
...>   Flicker.Facet.new(key: :n, type: :integer, bounds: %{min: 0, max: 9})
...> )
Flicker.FacetEditor.Dial

modal?(facet)

@spec modal?(Flicker.Facet.t()) :: boolean()

Whether opening facet's editor should take over the surface with a pop-out.

false for an editor whose value is complete in one interaction — the switch. Atomic commit is mandatory either way; a pop-out is only what multi-step values need.

Examples

iex> Flicker.FacetEditor.modal?(Flicker.Facet.new(key: :created, type: :date_range))
true

iex> Flicker.FacetEditor.modal?(Flicker.Facet.new(key: :verified?, type: :boolean))
false

iex> Flicker.FacetEditor.modal?(Flicker.Facet.new(key: :name, type: :string))
false

to_token(editor, value, facet)

@spec to_token(module(), value(), Flicker.Facet.t()) :: String.t() | nil

The full token an editor's value commits as — key:value — or nil when the value isn't complete.

The trailing space is deliberate: it matches what every other suggestion inserts, so Flicker.FacetSuggest.replace_current_token/3 leaves the caret ready for the next token.

Examples

iex> facet = Flicker.Facet.new(key: :verified?, type: :boolean)
...> Flicker.FacetEditor.to_token(Flicker.FacetEditor.Switch, true, facet)
"verified?:true "

iex> facet = Flicker.Facet.new(key: :created, type: :date_range)
...>
...> Flicker.FacetEditor.to_token(
...>   Flicker.FacetEditor.Calendar,
...>   %Flicker.Facet.Range{from: ~D[2026-07-01]},
...>   facet
...> )
"created:2026-07-01.. "

iex> facet = Flicker.Facet.new(key: :created, type: :date_range)
...>
...> Flicker.FacetEditor.to_token(
...>   Flicker.FacetEditor.Calendar,
...>   %Flicker.Facet.Range{},
...>   facet
...> )
nil