A Phoenix.LiveView.JS command as a term, printed to source and read back from it.
Nothing is spelled by hand
An action says what it wants as a triple — the command's name, its positional arguments, and its options — and every step after that belongs to somebody else's code:
Phoenix.LiveView.JSdecides whether the options are legal. The triple is applied to it and the%Phoenix.LiveView.JS{}it answers with is thrown away; what is kept is that it answered at all. Sotransition:,time:,display:andblocking:are supported by this module knowing nothing about them, an option a later LiveView adds arrives supported, and the message an author sees for a wrong one is LiveView's own.Macro.escape/1turns the terms into quoted Elixir, which is what gets a class name containing a double quote, a backslash or#{right — the third being the one that is not a broken string but interpolation running in somebody's page.- The code formatter prints it, at infinite width so that one command is one line.
Code.string_to_quoted/1parses it back, andMishkaCmsCore.Builder.Literalunwraps the literals into the terms an action matches on.
This is what replaced a private table of ~s|JS.#{verb}("#{class}", to: "#{target}")|. A string
built that way has to be re-checked after the fact, because building it is where the mistake goes;
a term printed by the formatter cannot have the mistake in it.
Where the application happens
In the editor, once, when an author presses Apply — MishkaCmsCore.Runtime.PageBuilder.Interactions
calls this from the admin LiveView. What reaches a document is the printed source and only that,
so nothing here is in a visitor's page, in the compiled module, or on the render path.
What a page is allowed to contain
A command names JS and carries data. That is the whole rule, and it is kept by construction
rather than by inspection: an action hands over terms, data?/1 refuses anything that is not data
before it is escaped, and the source is built here. There is no path by which a package's text
reaches a document.
Summary
Types
One command: the Phoenix.LiveView.JS function name, its positional arguments, and its options.
Functions
The commands a source string holds, in the order they run, or :no.
The source for one command, or for a pipe of them.
Types
One command: the Phoenix.LiveView.JS function name, its positional arguments, and its options.
{:add_class, ["is-open"], [to: ".pb-7f3a91b2"]}
{:push, ["pb:say"], [value: %{"kind" => "info", "message" => "Saved"}]}
{:show, [], [to: ".pb-7f3a91b2", time: 300]}
Functions
The commands a source string holds, in the order they run, or :no.
A pipe reads as a list; anything that is not JS calls carrying literal data reads as :no and
belongs to whoever hand-wrote it.
iex> MishkaCmsCore.Builder.Command.read(~s|JS.toggle_class("on", to: ".pb-1")|)
{:ok, [{:toggle_class, ["on"], [to: ".pb-1"]}]}
iex> MishkaCmsCore.Builder.Command.read(~s|JS.show(to: some_variable)|)
:no
The source for one command, or for a pipe of them.
iex> MishkaCmsCore.Builder.Command.write({:add_class, ["is-open"], [to: ".pb-1"]})
{:ok, ~s|JS.add_class("is-open", to: ".pb-1")|}
iex> MishkaCmsCore.Builder.Command.write({:show, [], [to: ".pb-1", speed: "fast"]})
{:error, "invalid option for show\nExpected keys to be one of [:to, :transition, :display, :time, :blocking], got: :speed\n"}