When an element is on the page at all, without anyone writing Elixir.
What it writes
HEEx already has the feature: :if={…} is how a developer says it, and it is the one attribute
every template in this project uses for the job. So there is nothing to invent — the panel offers a
short list of questions a page can answer about itself, and each one writes the expression a
developer would have written:
<div :if={assigns[:current_user]}>Every kit builder in the field has this under some name; Elementor calls it Display Conditions. The difference here is that the answer is ordinary markup, so an author who later learns Elixir finds their own condition in Advanced rather than a setting stored somewhere else.
Why assigns[:key] and not @key
Because the same document is rendered on surfaces that do not all carry the same assigns. @flash
raises KeyError where nothing assigned one, and a layout, an error page, a component preview and
the canvas are each rendered by a different caller. assigns[:key] answers nil instead, which is
the same answer as "no", and is the form the compiled templates in this project already use.
It is also why a flash is read as assigns[:flash]["info"] rather than through Phoenix.Flash:
nil[key] is nil in Elixir, so the whole chain is safe on a surface that assigned no flash at
all, and put_flash/3 stores under exactly those two string keys.
Reading it back
By matching the expression against what this module writes, never by matching text. Anything else
— a condition somebody wrote by hand, one piped into something — reads as :custom and the panel
shows it rather than offering to replace something it does not understand. Same contract
MishkaCmsCore.Runtime.PageBuilder.Interactions keeps for a click.
Summary
Functions
The attribute a condition is written into.
Reads an attribute value back into {:ok, rule}, :none, or :custom.
Every condition offered, core's first and a plugin's after them.
The conditions offered, as {value, label} in panel order.
Core's own conditions, in panel order — the seed every plugin appends to.
The expression source for one condition, or nil when there is nothing to write.
Functions
@spec attribute() :: String.t()
The attribute a condition is written into.
Reads an attribute value back into {:ok, rule}, :none, or :custom.
:custom is the important one: the element carries a condition somebody wrote, and the panel must
show it rather than replace it.
@spec registry() :: [map()]
Every condition offered, core's first and a plugin's after them.
Why a plain map and not a behaviour
Because a condition IS an expression. An action had to grow a module — it decides where it runs, what boxes to draw, what to write and how to read it back — and none of that has an answer here: a rule is a name and a piece of HEEx. So the entry is what it describes,
%{key: "is_admin", label: "Visitor is an admin", source: "assigns[:current_user] && …"}and read/1 recognises a contributed rule for free, because it matches the quoted form of
whatever is in this list rather than a table of its own.
Registered through builder:conditions exactly as every other registry — core seeded first, so a
plugin adds and never replaces. See MishkaCmsCore.Builder.Events.
The conditions offered, as {value, label} in panel order.
@spec seed() :: [map()]
Core's own conditions, in panel order — the seed every plugin appends to.
The expression source for one condition, or nil when there is nothing to write.
nil for the empty rule — "Always", which is the absence of the attribute rather than an
expression that is always true — and nil for a rule this module does not offer.