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

The list or map an attribute is holding, read out of a document and written back into one.

## Why this is not a text box

`chelekom-filterable-gallery` keeps its whole content in two `list` attributes — the filter
buttons and the pictures behind them — and thirty-nine attributes across the shipped kit are
shaped the same way. The panel drew a one-line box for every one of them and wrote what was typed
as a STRING, so a component that maps over its own attribute raised the moment anything was
entered. The report reads "we cannot add any image and it's not clear how it works".

So the panel needs a control that knows it is holding a collection, and that control needs to read
the list, change one item, and write the whole thing back as one edit. That is this module.

## What counts as readable

`MishkaCmsCore.Builder.Literal` decides, and it decides by walking the quoted form rather than
evaluating it: a list of strings, a list of maps, a keyword list. Anything else — a binding, a
function call, an interpolation — answers `:error`, and a caller holding `:error` must leave the
attribute alone rather than overwrite what it could not read. An author who has written
`media={@photos}` means it.

## And what is written back

`inspect/1`, which is the exact shape the editor can read again. One `set_attr` per change, so one
undo step per add, per removal and per edit.

# `add`

```elixir
@spec add(list(), term()) :: list()
```

# `blank`

```elixir
@spec blank(list(), term()) :: term()
```

What an empty row of this collection looks like, read off the ones already there.

A list of maps gets a map with the same keys blank, so the new card carries the same fields as its
neighbours; a list of anything else gets an empty string. An empty list has nothing to read, and
the caller's own sample is the answer — `MishkaCmsCore.Runtime.PageBuilder.Starter` has one for
the shapes it knows.

# `pair?`

```elixir
@spec pair?(term()) :: boolean()
```

Whether an item is one side of a map or a keyword list rather than a value on its own.

A `map` attribute is read out as its pairs — `chelekom-flash` holds `%{"info" => "…"}` and
`chelekom-pagination` a map of query parameters — and a pair is edited as a pair, one field for
each side.

# `put`

```elixir
@spec put(list(), non_neg_integer(), term()) :: list()
```

One item changed, added or dropped, and the whole collection back.

Out-of-range indexes are left alone rather than raising: the panel and the document can disagree
for one render after an undo, and a stale click must not take the editor down.

# `put_in_item`

```elixir
@spec put_in_item(list(), non_neg_integer(), atom() | String.t(), term()) :: list()
```

One field of one item changed. The key is the field's name, as an atom or as the text the browser
sent: a name the item has no field for leaves the collection as it was, and no atom is made of it.

# `put_side`

```elixir
@spec put_side(list(), non_neg_integer(), :key | :value, term()) :: list()
```

One side of a pair changed, the other left as it was.

# `read`

```elixir
@spec read(
  MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | term(),
  String.t(),
  term()
) ::
  {:ok, list()} | :error
```

The collection an attribute holds on this node, or the declared default when it holds nothing.

`:error` when the attribute is there and is not data — a binding rather than a list.

# `remove`

```elixir
@spec remove(list(), non_neg_integer()) :: list()
```

# `write`

```elixir
@spec write(list()) :: String.t()
```

The source text for a collection, ready to be written as an expression attribute.

---

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