Search documentation of Plushie

# The Development Loop

Copy Markdown View Source

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:

    # config/config.exs
    import Config

    import_config "#{config_env()}.exs"

    # 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:

    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`:

    @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:

    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:

    iex -S mix

    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:

    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:

    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:

    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:

    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

← Previous Page Your First App
Next Page → Events