# `MishkaGervaz.Form.Types.Field.KeyList`
[🔗](https://github.com/mishka-group/mishka_gervaz/blob/v0.0.6/lib/mishka_gervaz/form/types/field/key_list.ex#L1)

List field type whose rows have declared keys — `MishkaGervaz.Form.Types.Field.KeyMap` repeated,
with rows the author can add and remove.

Use it for a `{:array, :map}` column whose entries have a fixed, small shape: a Phoenix slot's
`attrs`, a list of redirects, a set of social links. Use `:json` instead when the entries are not
known ahead of time.

## Example

As a field of its own, over an `{:array, :map}` column:

    field :social_links, :key_list do
      options [
        [name: :platform, type: :text, placeholder: "e.g. github"],
        [name: :url, type: :text, placeholder: "https://…"]
      ]

      ui do
        label "Social links"
        add_label "+ Add link"
      end
    end

Or as a sub-field of a nested row, which is where a Phoenix slot's `attrs` lives:

    nested_field :attrs, :key_list do
      options [
        [name: :name, type: :text, placeholder: "e.g. label"],
        [name: :type, type: :select, options: ~w(string integer boolean any)],
        [name: :required, type: :toggle, label: "Required"]
      ]

      ui do
        label "Slot Attributes"
        add_label "+ Add Attribute"
        remove_label "Remove"
      end
    end

Keys are declared exactly as `:key_map` declares them, a list or `fn state -> keys end` — see its
docs for the options each key takes. `add_label` and `remove_label` name the two buttons; they
default to `+ Add` and `Remove`.

## Where it works

Both positions carry the same controls; only the events behind the two buttons differ, because
the two lists sit at different depths in the params:

| Position                        | Column               | Rows added and removed by       |
|---------------------------------|----------------------|----------------------------------|
| a field of its own              | `{:array, :map}`     | `add_nested` / `remove_nested`   |
| a sub-field of a `:nested` field | a constrained map    | `add_key_row` / `remove_key_row` |

A sub-field only gets the buttons on the constrained-map path, where a row has an index to
address. On an embedded nested field its rows still render, read-only.

## Empty rows are dropped

A row whose declared keys are all blank is omitted rather than stored as a map of empty strings,
so adding a row and then leaving it costs nothing. Values are cast to their declared type before
they reach the changeset; see `MishkaGervaz.Form.Types.Field.Nested`.

See `MishkaGervaz.Form.Behaviours.FieldType` and `MishkaGervaz.Form.Types.Field`.

# `keys`

```elixir
@spec keys(map() | keyword() | nil) :: [map()]
```

The declared keys of one row. Delegates to `MishkaGervaz.Form.Types.Field.KeyMap.keys/1`.

# `keys`

```elixir
@spec keys(map() | keyword() | nil, map()) :: [map()]
```

The declared keys of one row, with `options fn state -> keys end` read with the form `state`.
Delegates to `MishkaGervaz.Form.Types.Field.KeyMap.keys/2`.

# `rows`

```elixir
@spec rows(term()) :: [map()]
```

The rows in order, whether they arrived as a list or as the index-keyed map a form posts.

A form sends `attrs[0][name]`, `attrs[1][name]` and so on, which Phoenix hands back as
`%{"0" => …, "1" => …}`; the indices give the order. A value read back off the column is already
a list and is returned as it stands. Anything that is not a map — including the hidden marker the
template posts so an empty list can be expressed at all — is dropped.

    iex> MishkaGervaz.Form.Types.Field.KeyList.rows(%{"1" => %{"a" => 1}, "0" => %{"a" => 0}})
    [%{"a" => 0}, %{"a" => 1}]

---

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