# `MishkaCmsCore.Runtime.Authoring.Hooks`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.3/lib/mishka_cms_core/runtime/authoring/hooks.ex#L5)

The runtime JS hooks an element can be attached to, and the two attributes that attach it.

## Why a picker and not a code box

`MishkaCmsCore.Runtime.Authoring.Interactions` covers what a page usually wants — toggle a class,
show something, hide something — with no code at any point. What it cannot do is anything else, and
the answer to "anything else" is a hook: real JavaScript, written on the JS hooks page, compiled
into the site's bundle and registered with LiveView.

Writing one stays there. This only *selects* from what is already in the bundle, which is the whole
reason it needs no gate of its own: it grants nothing the actor did not already have, it removes
the step where they hand-type `phx-hook` into Advanced and get the capitalisation wrong.

## The two attributes

`phx-hook` names the hook, and LiveView **requires a unique DOM id** on any element carrying one —
a hook on an element with no id silently never mounts, which is a bad afternoon. So attaching one
mints an id when the element has none, and the picker says so rather than leaving it to be
discovered.

The name written is `JavaScriptCompiler.browser_name/2`'s, not the row's: a global hook called
`collapse` registers as `GlobalCollapse` and a site's own as `<Site>Collapse`. Asking the compiler
is what keeps the two from drifting.

# `available`

```elixir
@spec available(map() | nil, String.t() | nil, String.t() | nil) :: [map()]
```

Every hook this site can attach, its own and the globals, as `%{name:, hook:, description:}`.

`name` is what goes in `phx-hook`; `hook` is the row's own name, which is what the JS hooks page
calls it and therefore what a reader is looking for.

# `known?`

```elixir
@spec known?(map() | nil, String.t() | nil, String.t() | nil, String.t()) :: boolean()
```

Whether `name` is a hook the picker is entitled to write.

The picker writes the name into the document, so a stale option — a hook archived in another tab —
must not be written as though it existed.

# `named_in`

```elixir
@spec named_in(
  map(),
  String.t() | nil,
  :all | {:with_globals, String.t()} | String.t()
) ::
  non_neg_integer()
```

How many templates — pages, layouts and components — attach `hook`: carry a `phx-hook` naming it
as the browser does (`JavaScriptCompiler.browser_name/2`). Every place a row holds markup is read:
its template, its clause templates, and the `~H` written in its helpers, body and prelude
(`Markup.sigils/1`) — a kit component often attaches one only in a helper. The markup is parsed,
so a comment or a string that only mentions the name is not counted; an EEx block, which the tree
keeps as one node, and a template the tree cannot read are matched on their source. `hook` needs
its `is_global` calculation loaded.

`scope` is where to look: a site's own hook is attached on its site alone; a global one on `:all`
sites, or on `{:with_globals, site_id}` — a site's rows and the global components — for a site
admin. Error pages are drawn by a controller, where no hook mounts, so they are not read.

# `registry`

```elixir
@spec registry(map() | nil, String.t() | nil, String.t() | nil) :: [map()]
```

Every hook the picker offers: this site's, the globals, and whatever a plugin contributes through
`builder:js_hooks`.

ONE LIST FOR THE PICKER AND FOR `known?/4`, which is the whole point of naming it — the same
`Interactions.registry/0` and `Conditions.registry/0` are. The picker read the appended list and
`known?/4` read `available/3`, so every hook an extension contributed was offered and then refused
on Apply.

---

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