# Lists and Inputs

The pad from the previous chapters works with a single experiment in memory. In
this chapter we add file management: save experiments as `.ex` files, list
them in a sidebar, create new ones, switch between them, and delete the ones
you no longer need.

Along the way we will learn about dynamic list rendering, scoped IDs,
`text_input`, `checkbox`, and the `Command.focus/1` command.

## Saving experiments to files

Experiments are plain Elixir source files. We will store them in
`priv/experiments/`, a directory outside the compilation paths so they
do not get compiled by Mix at startup. Each file is a module with a
`view/0` function, exactly like the starter code from chapter 4.

These are standard Elixir file operations, not Plushie concepts:

```elixir
@experiments_dir "priv/experiments"

defp list_experiments do
  File.mkdir_p!(@experiments_dir)

  @experiments_dir
  |> File.ls!()
  |> Enum.filter(&String.ends_with?(&1, ".ex"))
  |> Enum.sort()
end

defp save_experiment(name, source) do
  File.mkdir_p!(@experiments_dir)
  File.write!(Path.join(@experiments_dir, name), source)
end

defp load_experiment(name) do
  Path.join(@experiments_dir, name) |> File.read!()
end

defp delete_experiment(name) do
  Path.join(@experiments_dir, name) |> File.rm!()
end
```

We will call these from `update/2` when the user interacts with the file
management UI.

## Updating the model

The model needs a few new fields:

```elixir
%{
  source: "...",
  preview: nil,
  error: nil,
  event_log: [],
  # New fields
  files: [],              # list of filenames in experiments dir
  active_file: nil,       # currently selected filename, or nil
  new_name: "",           # text input for creating new experiments
  auto_save: false        # auto-save toggle (wired up in chapter 10)
}
```

In `init/1`, we load the file list and optionally load the first file:

```elixir
def init(_opts) do
  files = list_experiments()
  {source, active} = case files do
    [first | _] -> {load_experiment(first), first}
    [] -> {@starter_code, nil}
  end

  model = %{
    source: source,
    preview: nil,
    error: nil,
    event_log: [],
    files: files,
    active_file: active,
    new_name: "",
    auto_save: false
  }

  case compile_preview(source) do
    {:ok, tree} -> %{model | preview: tree}
    {:error, msg} -> %{model | error: msg}
  end
end
```

## Dynamic lists with keyed_column

To display the file list, we need to render a dynamic list of items. The
`for` comprehension works inside do-blocks just like in regular Elixir:

```elixir
column spacing: 4 do
  for file <- model.files do
    button(file, file)
  end
end
```

This works, but there is a subtlety. When you add or remove a file,
`column` matches children to their previous state by position. If you add
a file at the top of the list, every child shifts down one position. The
second file inherits the first file's widget state (focus, scroll position,
text cursor), the third inherits the second's, and so on.

`keyed_column` solves this by matching children by their ID instead of
position. Items keep their state no matter where they move in the list:

```elixir
keyed_column spacing: 4 do
  for file <- model.files do
    button(file, file)
  end
end
```

Use `keyed_column` for any list that changes at runtime. Use `column` for
static layouts where the children are fixed.

## Scoped IDs

Each file in the list needs controls, at least a delete button. But if
every delete button has `id: "delete"`, how does `update/2` know which file
to delete?

This is what **scoped IDs** solve. When you wrap widgets in a named
`container`, the container's ID becomes part of the scope chain. Events from
widgets inside carry that scope.

```elixir
container "hello.ex" do
  button("delete", "x")
end
```

When the delete button is clicked, the event arrives as:

```elixir
%WidgetEvent{type: :click, id: "delete", scope: ["hello.ex", "main"], window_id: "main"}
```

The `scope` list contains ancestor container IDs (nearest parent first)
with the window ID as the last element. You pattern match on the head
to extract the file name:

```elixir
def update(model, %WidgetEvent{type: :click, id: "delete", scope: [file | _]}) do
  delete_experiment(file)
  # ... update model
end
```

This works no matter how deeply nested the button is. Any named container
between the button and the root adds its ID to the scope chain. We will use
scoped IDs throughout the pad for any list where items have their own
controls.

For a full treatment of scoping rules, ID resolution, and edge cases, see the
[Scoped IDs reference](../reference/scoped-ids.md).

## The file list sidebar

Here is the sidebar view. It sits to the left of the editor in a `column`
with a fixed width:

```elixir
defp file_list(model) do
  column width: 180, height: :fill, padding: 8, spacing: 8 do
    text("sidebar-title", "Experiments", size: 14)

    scrollable "file-scroll", height: :fill do
      keyed_column spacing: 2 do
        for file <- model.files do
          container file do
            row spacing: 4 do
              button(
                "select",
                file,
                width: :fill,
                style: if(file == model.active_file, do: :primary, else: :text)
              )
              button("delete", "x")
            end
          end
        end
      end
    end

    # New experiment input (covered below)
    row spacing: 4 do
      text_input("new-name", model.new_name,
        placeholder: "name.ex",
        on_submit: true
      )
    end
  end
end
```

Each file gets a `container` scoped by the filename. Inside it, a select
button and a delete button. The active file is highlighted with `:primary`
style. We will refine the styling in [chapter 8](08-styling.md).

## Text input

`text_input` is a single-line input widget. It takes an ID, the current
value (from your model), and options:

```elixir
text_input("new-name", model.new_name,
  placeholder: "name.ex",
  on_submit: true
)
```

- `placeholder:` - grey hint text shown when the input is empty.
- `on_submit: true` - enables the `:submit` event when the user presses
  Enter. Without this, only `:input` events (on every keystroke) are emitted.

The `:input` event delivers the current text as `value`:

```elixir
def update(model, %WidgetEvent{type: :input, id: "new-name", value: text}) do
  %{model | new_name: text}
end
```

The `:submit` event fires when Enter is pressed (and `on_submit: true`):

```elixir
def update(model, %WidgetEvent{type: :submit, id: "new-name"}) do
  create_new_experiment(model)
end
```

## Checkbox: auto-save toggle

`checkbox` is a boolean toggle widget:

```elixir
checkbox("auto-save", model.auto_save)
```

It emits a `:toggle` event with the new boolean value:

```elixir
def update(model, %WidgetEvent{type: :toggle, id: "auto-save", value: checked}) do
  %{model | auto_save: checked}
end
```

For now we just track the flag in the model. In [chapter 10](10-subscriptions.md)
we will wire it up with a debounce timer so saving happens automatically
when the checkbox is checked and the content changes.

## Commands: focus

Sometimes `update/2` needs to trigger a side effect. Instead of returning a
bare model, you return a `{model, command}` tuple.

`Plushie.Command.focus/1` sets keyboard focus on a widget by its scoped
path. After creating a new experiment, we want focus to jump back to the
editor:

```elixir
alias Plushie.Command

defp create_new_experiment(model) do
  name = String.trim(model.new_name)

  if name == "" or not String.ends_with?(name, ".ex") do
    model
  else
    template = """
    defmodule Pad.Experiments.#{name |> Path.rootname() |> Macro.camelize()} do
      import Plushie.UI

      def view do
        column padding: 16 do
          text("hello", "New experiment")
        end
      end
    end
    """

    save_experiment(name, template)
    files = list_experiments()
    model = %{model | files: files, active_file: name, source: template, new_name: ""}

    case compile_preview(template) do
      {:ok, tree} -> {%{model | preview: tree, error: nil}, Command.focus("editor")}
      {:error, msg} -> {%{model | error: msg, preview: nil}, Command.focus("editor")}
    end
  end
end
```

The `{model, Command.focus("editor")}` return tells the runtime to set focus
on the widget with ID `"editor"` after processing the update.

Commands are pure data (`Plushie.Command` structs). The runtime executes
them after `update/2` returns. See the [Commands reference](../reference/commands.md)
for the full list.

## Wiring up file switching and deletion

Add these clauses to `update/2`:

```elixir
# Switch to a different file
def update(model, %WidgetEvent{type: :click, id: "select", scope: [file | _]}) do
  if model.active_file != nil do
    save_experiment(model.active_file, model.source)
  end

  source = load_experiment(file)
  model = %{model | active_file: file, source: source}

  case compile_preview(source) do
    {:ok, tree} -> %{model | preview: tree, error: nil}
    {:error, msg} -> %{model | error: msg, preview: nil}
  end
end

# Delete an experiment
def update(model, %WidgetEvent{type: :click, id: "delete", scope: [file | _]}) do
  delete_experiment(file)
  files = list_experiments()

  if file == model.active_file do
    case files do
      [first | _] ->
        source = load_experiment(first)
        model = %{model | files: files, active_file: first, source: source}
        case compile_preview(source) do
          {:ok, tree} -> %{model | preview: tree, error: nil}
          {:error, msg} -> %{model | error: msg, preview: nil}
        end

      [] ->
        %{model | files: [], active_file: nil, source: @starter_code, preview: nil, error: nil}
    end
  else
    %{model | files: files}
  end
end
```

Both use scope binding to extract the filename. File switching saves the
current content first, then loads the new file.

## Updating the view

The main view now includes the sidebar:

```elixir
def view(model) do
  window "main", title: "Plushie Pad" do
    column width: :fill, height: :fill do
      row width: :fill, height: :fill do
        # Sidebar
        file_list(model)

        # Editor
        text_editor "editor", model.source do
          width {:fill_portion, 1}
          height :fill
          highlight_syntax "ex"
          font :monospace
        end

        # Preview
        container "preview", width: {:fill_portion, 1}, height: :fill, padding: 8 do
          if model.error do
            text("error", model.error, color: :red)
          else
            if model.preview do
              model.preview
            else
              text("placeholder", "Press Save to compile and preview")
            end
          end
        end
      end

      row padding: 4, spacing: 8 do
        button("save", "Save")
        checkbox("auto-save", model.auto_save)
        text("auto-label", "Auto-save")
      end

      scrollable "log", height: 120 do
        column spacing: 2, padding: 4 do
          for {entry, i} <- Enum.with_index(model.event_log) do
            text("log-#{i}", entry, size: 12, font: :monospace)
          end
        end
      end
    end
  end
end
```

The sidebar, editor, and preview sit side by side in a `row`. The sidebar
has a fixed width of 180 pixels; the editor and preview share the remaining
space equally via `{:fill_portion, 1}`.

## The complete pad

Here is the full module with file management. This is a substantial update
from chapter 5. If anything is not working, compare against this listing:

```elixir
defmodule PlushiePad do
  use Plushie.App

  import Plushie.UI

  alias Plushie.Command
  alias Plushie.Event.WidgetEvent

  @experiments_dir "priv/experiments"

  @starter_code """
  defmodule Pad.Experiments.Hello do
    import Plushie.UI

    def view do
      column padding: 16, spacing: 8 do
        text("greeting", "Hello, Plushie!", size: 24)
        button("btn", "Click Me")
      end
    end
  end
  """

  def init(_opts) do
    files = list_experiments()

    {source, active} =
      case files do
        [first | _] -> {load_experiment(first), first}
        [] -> {@starter_code, nil}
      end

    model = %{
      source: source,
      preview: nil,
      error: nil,
      event_log: [],
      files: files,
      active_file: active,
      new_name: "",
      auto_save: false
    }

    case compile_preview(source) do
      {:ok, tree} -> %{model | preview: tree}
      {:error, msg} -> %{model | error: msg}
    end
  end

  def update(model, %WidgetEvent{type: :input, id: "editor", value: source}) do
    %{model | source: source}
  end

  def update(model, %WidgetEvent{type: :click, id: "save"}) do
    case compile_preview(model.source) do
      {:ok, tree} ->
        if model.active_file, do: save_experiment(model.active_file, model.source)
        %{model | preview: tree, error: nil}
      {:error, msg} ->
        %{model | error: msg, preview: nil}
    end
  end

  def update(model, %WidgetEvent{type: :input, id: "new-name", value: text}) do
    %{model | new_name: text}
  end

  def update(model, %WidgetEvent{type: :submit, id: "new-name"}) do
    create_new_experiment(model)
  end

  def update(model, %WidgetEvent{type: :toggle, id: "auto-save", value: checked}) do
    %{model | auto_save: checked}
  end

  def update(model, %WidgetEvent{type: :click, id: "select", scope: [file | _]}) do
    if model.active_file, do: save_experiment(model.active_file, model.source)
    source = load_experiment(file)
    model = %{model | active_file: file, source: source}

    case compile_preview(source) do
      {:ok, tree} -> %{model | preview: tree, error: nil}
      {:error, msg} -> %{model | error: msg, preview: nil}
    end
  end

  def update(model, %WidgetEvent{type: :click, id: "delete", scope: [file | _]}) do
    delete_experiment(file)
    files = list_experiments()

    if file == model.active_file do
      case files do
        [first | _] ->
          source = load_experiment(first)
          model = %{model | files: files, active_file: first, source: source}
          case compile_preview(source) do
            {:ok, tree} -> %{model | preview: tree, error: nil}
            {:error, msg} -> %{model | error: msg, preview: nil}
          end
        [] ->
          %{model | files: [], active_file: nil, source: @starter_code, preview: nil, error: nil}
      end
    else
      %{model | files: files}
    end
  end

  # Log everything else
  def update(model, event) do
    entry = format_event(event)
    %{model | event_log: Enum.take([entry | model.event_log], 20)}
  end

  def view(model) do
    window "main", title: "Plushie Pad" do
      column width: :fill, height: :fill do
        row width: :fill, height: :fill do
          file_list(model)

          text_editor "editor", model.source do
            width {:fill_portion, 1}
            height :fill
            highlight_syntax "ex"
            font :monospace
          end

          container "preview", width: {:fill_portion, 1}, height: :fill, padding: 8 do
            if model.error do
              text("error", model.error, color: :red)
            else
              if model.preview do
                model.preview
              else
                text("placeholder", "Press Save to compile and preview")
              end
            end
          end
        end

        row padding: 4, spacing: 8 do
          button("save", "Save")
          checkbox("auto-save", model.auto_save)
          text("auto-label", "Auto-save")
        end

        scrollable "log", height: 120 do
          column spacing: 2, padding: 4 do
            for {entry, i} <- Enum.with_index(model.event_log) do
              text("log-#{i}", entry, size: 12, font: :monospace)
            end
          end
        end
      end
    end
  end

  defp file_list(model) do
    column width: 180, height: :fill, padding: 8, spacing: 8 do
      text("sidebar-title", "Experiments", size: 14)

      scrollable "file-scroll", height: :fill do
        keyed_column spacing: 2 do
          for file <- model.files do
            container file do
              row spacing: 4 do
                button("select", file,
                  width: :fill,
                  style: if(file == model.active_file, do: :primary, else: :text)
                )
                button("delete", "x")
              end
            end
          end
        end
      end

      row spacing: 4 do
        text_input("new-name", model.new_name,
          placeholder: "name.ex",
          on_submit: true
        )
      end
    end
  end

  defp create_new_experiment(model) do
    name = String.trim(model.new_name)

    if name == "" or not String.ends_with?(name, ".ex") do
      model
    else
      template = """
      defmodule Pad.Experiments.#{name |> Path.rootname() |> Macro.camelize()} do
        import Plushie.UI

        def view do
          column padding: 16 do
            text("hello", "New experiment")
          end
        end
      end
      """

      save_experiment(name, template)
      files = list_experiments()
      model = %{model | files: files, active_file: name, source: template, new_name: ""}

      case compile_preview(template) do
        {:ok, tree} -> {%{model | preview: tree, error: nil}, Command.focus("editor")}
        {:error, msg} -> {%{model | error: msg, preview: nil}, Command.focus("editor")}
      end
    end
  end

  defp compile_preview(source) do
    case Code.string_to_quoted(source) do
      {:error, {meta, message, token}} ->
        line = Keyword.get(meta, :line, "?")
        {:error, "Line #{line}: #{message}#{token}"}

      {:ok, _ast} ->
        try do
          Code.put_compiler_option(:ignore_module_conflict, true)
          [{module, _}] = Code.compile_string(source)

          if function_exported?(module, :view, 0) do
            {:ok, module.view()}
          else
            {:error, "Module must export a view/0 function"}
          end
        rescue
          e -> {:error, Exception.message(e)}
        after
          Code.put_compiler_option(:ignore_module_conflict, false)
        end
    end
  end

  defp format_event(%mod{} = event) do
    name = mod |> Module.split() |> List.last()

    fields =
      event
      |> Map.from_struct()
      |> Enum.map(fn {k, v} -> "#{k}: #{inspect(v)}" end)
      |> Enum.join(", ")

    "%#{name}{#{fields}}"
  end

  defp list_experiments do
    File.mkdir_p!(@experiments_dir)
    @experiments_dir |> File.ls!() |> Enum.filter(&String.ends_with?(&1, ".ex")) |> Enum.sort()
  end

  defp save_experiment(name, source) do
    File.mkdir_p!(@experiments_dir)
    File.write!(Path.join(@experiments_dir, name), source)
  end

  defp load_experiment(name), do: Path.join(@experiments_dir, name) |> File.read!()
  defp delete_experiment(name), do: Path.join(@experiments_dir, name) |> File.rm!()
end
```

## Verify it

Test the file management flow: create an experiment and switch between
files:

```elixir
test "create experiment and switch back" do
  type_text("#new-name", "test.ex")
  submit("#new-name")

  # New experiment appears in the sidebar
  assert_exists("#test.ex/select")

  # Switch back to the starter experiment
  click("#hello.ex/select")
  assert_text("#preview/greeting", "Hello, Plushie!")
end
```

This exercises scoped IDs, the create flow, file switching, and
compilation. That is the core of what this chapter builds.

## Try it

With the updated pad running:

- Create a few experiments with different names. Each gets a starter
  template and appears in the sidebar.
- Switch between them. Notice the editor content changes and the preview
  updates.
- Delete an experiment. The sidebar updates and the next experiment loads
  automatically.
- Write a gallery experiment from chapter 5 and interact with the widgets.
  Switch to another experiment and back. Your content is preserved because
  we save on switch.
- Try the auto-save checkbox. It toggles in the model but does not save yet
  (that comes in [chapter 10](10-subscriptions.md) when we learn about
  subscriptions).

Your pad now manages a library of experiments. Each one is a plain `.ex`
file in `priv/experiments/` that you can also open in your code editor.
In the next chapter, we will improve the layout so the panes are properly
sized and the spacing is consistent.

---

Next: [Layout](07-layout.md)
