# The Development Loop
The pad has a layout but the preview does not work yet. In this chapter we
bring it to life with two complementary techniques: **hot reload** for
editing the pad's own source code, and **runtime compilation** for compiling
widget code typed into the pad's editor.
Along the way we will learn how to inspect a running app from `iex`, a
useful debugging skill.
## Hot reload
In chapter 2 you used `--watch` to enable hot reload on a per-run basis.
Let us make it the default for development. Create a `config/` directory
with the following files:
```elixir
# config/config.exs
import Config
import_config "#{config_env()}.exs"
```
```elixir
# config/dev.exs
import Config
config :plushie, code_reloader: true
```
You also need stub files for the other environments so the import does
not fail. Create `config/test.exs` and `config/prod.exs` each containing
just `import Config`.
With this in place, `mix plushie.gui` enables hot reload automatically
in dev without the `--watch` flag. Plushie watches your `lib/` directory.
Edit any `.ex` file, save it, and the running app recompiles in place --
your model state is preserved.
This is how you develop the pad itself. Change the view, save, see the
result. You have been using this already if you tried the "Try it" exercises
in previous chapters.
Hot reload works because the runtime re-calls `view/1` with the current
model after recompilation. The new view function produces a new tree, the
runtime diffs it against the old one, and only the changes are sent to the
renderer.
## Making the preview work
The pad's editor holds Plushie widget code. We want to compile that code
and render the result in the preview pane. This requires three steps:
1. **Parse** - check that the code is valid Elixir syntax
2. **Compile** - compile it into a module with macro expansion
3. **Render** - call the module's `view/0` function and embed the result
Here is the helper that does all three:
```elixir
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
```
`Code.string_to_quoted/1` parses the source without evaluating it. If the
syntax is invalid, we get an error with a line number.
`Code.compile_string/1` compiles the source into a module. This runs the
full Elixir compilation pipeline including macro expansion, so
`import Plushie.UI` and the DSL macros work exactly as they would in a
normal `.ex` file. We set `ignore_module_conflict` to suppress the warning
that appears when saving the same experiment twice (redefining the module).
The compiled module's `view/0` function returns a widget tree. Because
widget structs compose naturally, we can place this tree directly in the
pad's view.
Errors at any stage are caught and displayed as text in the preview pane.
The pad never crashes from bad experiment code.
## Wiring up the save button
Update `init/1` to compile a starter experiment on startup, and add a save
handler to `update/2`:
```elixir
@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
model = %{
source: @starter_code,
preview: nil,
error: nil
}
case compile_preview(model.source) do
{:ok, tree} -> %{model | preview: tree}
{:error, msg} -> %{model | error: msg}
end
end
def update(model, %WidgetEvent{type: :click, id: "save"}) do
case compile_preview(model.source) do
{:ok, tree} -> %{model | preview: tree, error: nil}
{:error, msg} -> %{model | error: msg, preview: nil}
end
end
```
Now when you type experiment code in the editor and click Save, the
preview updates with the rendered widgets. Syntax errors, compile errors, and
runtime errors all show as red text in the preview pane.
## The experiment format
Experiments are modules with a `view/0` function:
```elixir
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
```
This is the same DSL code you write in an app's `view/1`. Experiments
just don't have a model, so the function takes no arguments.
## Inspecting a running app from iex
Sometimes you want to look at the model or tree of a running app. Start an
`iex` session and launch the app directly:
```bash
iex -S mix
```
```elixir
iex> {:ok, pid} = Plushie.start_link(PlushiePad, binary: Plushie.Binary.path!())
```
Now the app is running and you have the iex prompt. Query the runtime:
```elixir
iex> Plushie.Runtime.get_model(Plushie.Runtime)
%{source: "...", preview: ..., error: nil}
iex> Plushie.Runtime.get_tree(Plushie.Runtime)
%{id: "main", type: "window", children: [...]}
```
`get_model/1` returns the current model. `get_tree/1` returns the normalized
UI tree. These are the same values your `view/1` and `update/2` work with --
seeing them directly helps when debugging layout issues or unexpected state.
The default runtime name is `Plushie.Runtime`. If you started Plushie with
a custom `name:` option, the runtime name follows the convention
`{name}.Runtime`.
## The complete pad
Here is the full module with compilation wired up:
```elixir
defmodule PlushiePad do
use Plushie.App
import Plushie.UI
alias Plushie.Event.WidgetEvent
@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
model = %{
source: @starter_code,
preview: nil,
error: nil
}
case compile_preview(model.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} -> %{model | preview: tree, error: nil}
{:error, msg} -> %{model | error: msg, preview: nil}
end
end
def update(model, _event), do: model
def view(model) do
window "main", title: "Plushie Pad" do
column width: :fill, height: :fill do
row width: :fill, height: :fill do
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: 16 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: 8 do
button("save", "Save")
end
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
end
```
Run it:
```bash
mix plushie.gui PlushiePad
```
The starter experiment compiles on init, so you should see "Hello, Plushie!"
and a "Click Me" button in the preview pane immediately. Edit the code in
the editor, click Save, and the preview updates.
## Verify it
Update the test to verify compilation works:
```elixir
test "starter code compiles and renders on init" do
assert_text("#preview/greeting", "Hello, Plushie!")
assert_exists("#preview/btn")
end
```
The preview widgets have scoped IDs (`preview/greeting`, `preview/btn`)
because they are inside `container "preview"`. This is how scoped IDs
work. We will use them more in chapter 6.
## Try it
- Change the starter experiment: add a `checkbox`, a `slider`, or a
`text_input` to the column. Save and see them render.
- Deliberately break the syntax: delete a closing `end`. Save and see
the error message in the preview. Fix it and save again.
- Try writing a completely different experiment: a row of coloured buttons,
or a progress bar with a label.
- Start iex (`iex -S mix`), launch the pad with
`Plushie.start_link(PlushiePad, binary: Plushie.Binary.path!())`,
and inspect the model with `Plushie.Runtime.get_model(Plushie.Runtime)`.
In the next chapter, we will add an event log to the pad so you can see
exactly what events widgets produce when you interact with them.
---
Next: [Events](05-events.md)