# `MishkaCmsCore.Builder.Action`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/builder/action.ex#L5)

What an element can be made to do when it is clicked — one declared thing, so a package can add one.

## The contract

An action is a descriptor in the `builder:interactions` registry (ADR-14, see
`MishkaCmsCore.Builder.Events`) whose `run` names a module implementing this behaviour. Core seeds
seven; a plugin appends its own the way it appends an audit check:

    use MishkaInstaller.Event.Hook, event: "builder:interactions", initial: %{priority: 50}

    def call(%{actions: actions} = payload) do
      {:reply, %{payload | actions: actions ++ [MyPkg.Actions.Toast.descriptor()]}}
    end

The descriptor is a plain map because it travels through a hook payload, and `run` is a module
rather than a closure for the same reason — the rule `MishkaCmsCore.Builder.Descriptor` already
keeps for every other registry.

## Client or server, and nothing in between

An action says where it runs, and the two are not alike:

  * `:client` writes a `Phoenix.LiveView.JS` command. It runs in the browser with no round trip and
    survives patches because it is part of what the server rendered. Six of core's seven are this,
    and anything that only moves classes or visibility around belongs here.
  * `:server` writes `JS.push/2` and names a handler. Use it only when something the server owns
    has to change — a flash lives on the socket, so "Show a message" has no client-side answer.

A server action also names the module the compiled page calls, as `{module, function}`. That module
must be runtime code: it is named inside every published page that uses the action, and a visitor's
page has no business naming anything under `MishkaCmsCore.Builder.*` or
`MishkaCmsCore.Runtime.PageBuilder.*`. Core's own points at
`MishkaCmsCore.Runtime.PageMessage`.

## What the panel draws

`needs/1` returns the boxes this action wants, and the Inspector renders them. A package never
touches panel code and never ships markup for a form: it says it wants a line of text called
`message` and gets a labelled input, remembered and read back.

## Both directions are terms

`write/3` answers a `MishkaCmsCore.Builder.Command.t/0` — the command's name, its arguments and its
options — and `read/2` is handed one back. Neither side spells Elixir. What is written is escaped
and printed by the code formatter, and what is read was parsed and unwrapped, so a class name
containing a double quote is a class name containing a double quote in both directions and there is
no place for a quoting mistake to live.

It is also what makes an option free: `Phoenix.LiveView.JS` validates the keys of whatever it is
given, so `transition:`, `time:`, `display:` and `blocking:` work on the commands that take them
without core holding a list of which those are.

## Reading it back

`read/2` is what lets the panel re-open an action the author already saved, with its boxes filled.
The default answers `:no`, which is honest rather than clever: the panel then shows the expression
as hand-written and refuses to rewrite something it does not understand. Implement it — a function
head is usually enough — if you want yours to round-trip.

## A whole action

    defmodule MyPkg.Actions.Scroll do
      use MishkaCmsCore.Builder.Action

      alias MishkaCmsCore.Builder.Action

      def descriptor,
        do: action(key: "scroll", label: "Scroll to it", priority: 60)

      @impl true
      def needs(_action), do: [text("offset", "Offset", placeholder: "80")]

      @impl true
      def write(_action, context, values),
        do: {:dispatch, ["scroll-to"], [to: Action.aim(context), detail: values]}

      @impl true
      def read(_action, {:dispatch, ["scroll-to"], opts}), do: {:ok, opts[:detail]}
      def read(_action, _other), do: :no
    end

# `context`

```elixir
@type context() :: %{scope: String.t(), target: String.t() | nil, opener: map()}
```

What the click already knows: the clicked element's own scope class, the scope it is aimed at (or
`nil` for itself), and what the target says about being opened — see
`MishkaCmsCore.Builder.PaletteEntry.opens/1`.

# `field`

```elixir
@type field() :: %{
  name: String.t(),
  label: String.t(),
  type: :text | :select,
  placeholder: String.t(),
  options: [{String.t(), String.t()}]
}
```

One box in the panel. `:text` is a line of text, `:select` a list of `{value, label}`.

# `values`

```elixir
@type values() :: %{optional(String.t()) =&gt; String.t()}
```

What the author typed, keyed by field name.

# `event`

```elixir
@callback event(action :: map()) :: String.t() | nil
```

A server action's event name — what `JS.push/2` sends and the compiled page answers.

# `handler`

```elixir
@callback handler(action :: map()) :: {module(), atom()} | nil
```

A server action's handler as `{module, function}`, called with `(socket, params)` and returning a
socket. Runtime code only: it is named inside every published page using the action.

# `needs`

```elixir
@callback needs(action :: map()) :: [field()]
```

The boxes the panel draws for this action, in order.

A field's `name` is how its value reaches `write/3` and how `read/2`'s answer is matched back to
the box, so `target`, `action` and `node_id` are reserved — the panel's form already posts those
three as the click's own context and one of your own by those names would be dropped.

# `offer?`

```elixir
@callback offer?(action :: map(), context()) :: boolean()
```

Whether to offer this action at all, given what the click is aimed at.

# `read`

```elixir
@callback read(action :: map(), MishkaCmsCore.Builder.Command.t()) ::
  {:ok, values()} | :no
```

Reads a command back into the values that wrote it, or `:no` if it is not this one's.

# `where`

```elixir
@callback where(action :: map()) :: :client | :server
```

Where this action runs. `:client` is a `JS` command; `:server` is a push and a handler.

# `write`

```elixir
@callback write(action :: map(), context(), values()) ::
  MishkaCmsCore.Builder.Command.t() | [MishkaCmsCore.Builder.Command.t()] | nil
```

The command to write, a list of them to pipe, or `nil` when there is nothing to write yet.

Terms, never source. Whatever is in the arguments and the options is escaped and printed for you,
and `Phoenix.LiveView.JS` is asked whether the options are ones it takes before anything is stored.

# `action`
*macro* 

A descriptor for this module's action. `key` is what the document stores and the select posts;
`label` is what a reader sees; `priority` decides where it sits in the list.

# `aim`

```elixir
@spec aim(context()) :: String.t()
```

The selector an action lands on: the target's scope when the click is aimed at one, and the
clicked element's own otherwise.

Sanitised down to what a class name can hold. Both scopes are minted by `Tree.ensure_scope/2` and
neither is an author's own word, so this changes nothing today — it is here so that a package
reaching for the selector cannot be the first place a stray character lands in a generated
selector.

# `key`

```elixir
@spec key(map()) :: String.t()
```

This action's key, the string the document stores.

# `module`

```elixir
@spec module(map()) :: module() | nil
```

The module implementing this action, or `nil` for a descriptor that names none.

# `select`

```elixir
@spec select(String.t(), String.t(), [{String.t(), String.t()}]) :: field()
```

A list to pick from, as `{value, label}`. The first is what a fresh action starts on.

# `text`

```elixir
@spec text(String.t(), String.t(), keyword()) :: field()
```

A line of text. `placeholder` is the example a non-technical author reads instead of a spec.

---

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