Search documentation of Plushie

# Your First App

Copy Markdown View Source

In the previous chapter we built a counter and learned the init/update/view cycle. Now we will start building Plushie Pad, a live widget editor that grows with you throughout this guide.

In this chapter we set up the pad's layout: a code editor on the left, a preview area on the right, and a save button. We will make the preview actually work in the next chapter. For now the focus is on the DSL and how views are composed.

## The DSL: three equivalent forms

Before we look at the code, a quick note on how the Plushie DSL works. There are three ways to set widget properties, and they all produce the same result.

Keyword arguments on the call line, inline declarations mixed with children in the do-block, and nested do-blocks for struct-typed properties like padding, all in one expression:

    column spacing: 8 do
      padding do
        top 16
        bottom 8
      end
      text("hello", "Hello")
    end

This is equivalent to:

    column spacing: 8, padding: %{top: 16, bottom: 8} do
      text("hello", "Hello")
    end

You can mix all three in the same expression. Block declarations override keyword arguments when they conflict. Use whichever form reads best for the situation. Throughout this guide we will use different forms depending on context.

Widgets also have a programmatic struct API that you can use instead of the macro-based DSL. The same column from above:

    alias Plushie.Widget.{Column, Text}

    Column.new("my_col", spacing: 8, padding: %{top: 16, bottom: 8})
    |> Column.push(Text.new("hello", "Hello"))

The struct API produces the same output as the DSL. It is useful for helper functions, dynamic generation, or anywhere you prefer working with data structures directly. We will use the DSL throughout this guide.

## The complete pad

Here is the full module for this chapter. Save it as `lib/plushie_pad.ex` and we will walk through the key parts below.

    defmodule PlushiePad do
      use Plushie.App

      import Plushie.UI

      alias Plushie.Event.WidgetEvent

      def init(_opts) do
        %{
          source: "# Write some Plushie code here\n",
          preview: nil,
          error: nil
        }
      end

      def update(model, %WidgetEvent{type: :input, id: "editor", value: source}) do
        %{model | source: source}
      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
    end

Run it:

    mix plushie.gui PlushiePad --watch

You should see the editor on the left with syntax highlighting and a placeholder message on the right. The save button is there but does not do anything yet. We will fix that in the next chapter.

## Walking through the code

### The model

`init/1` receives a keyword list passed via the `app_opts:` option when starting the app (e.g. `Plushie.start_link(PlushiePad, app_opts: [key: val])`). We do not need it yet, so we ignore it. The returned map becomes the initial model. Here we track the editor source text and leave slots for a compiled preview and an error message.

### text_editor

`text_editor` is a multi-line editing widget with syntax highlighting support. The `content` argument seeds the initial text, and subsequent changes arrive as `:input` events with the full content as the value. The `highlight_syntax: "ex"` option enables Elixir syntax highlighting.

Some widgets hold renderer-side state (cursor position, scroll offset, text selection). `text_editor`, `text_input`, `combo_box`, `scrollable`, and `pane_grid` all fall into this category. These widgets need an explicit string ID so the renderer can match them to their state across renders. If the ID changes, the state resets. Layout widgets like `column` and `row` have no renderer-side state, so auto-generated IDs work fine for them.

### The view

The view is a `window` containing a `column` that splits vertically into a main content `row` and a toolbar `row` at the bottom.

- `{:fill_portion, 1}` gives the editor and preview equal width. Change one to `{:fill_portion, 2}` and it takes twice the space. We cover sizing in depth in chapter 7.
- The preview pane is wrapped in `container "preview"`. This named container will matter later when we need to distinguish events from preview widgets vs the pad's own widgets.
- `model.preview` will hold a compiled widget tree once we wire up compilation. For now it is `nil`, so the placeholder text shows.
- An `if` without an `else` returns `nil`, which the layout filters out automatically. This is a useful pattern throughout Plushie views for conditionally showing widgets.

### Events

The editor emits `:input` events on every keystroke. We pattern-match on the widget ID and update the model. The catch-all clause ignores everything else, including save button clicks. We will wire those up in the next chapter.

## Verify it

Add a test for the pad layout in `test/pad_test.exs`:

    defmodule PlushiePad.PadTest do
      use Plushie.Test.Case, app: PlushiePad

      test "pad has editor and preview panes" do
        assert_exists("#editor")
        assert_exists("#preview")
        assert_text("#preview/placeholder", "Press Save to compile and preview")
      end
    end

This verifies the split-pane layout is rendering correctly.

## Try it

With the pad running and hot reload active:

- Type some Elixir code in the editor. The syntax highlighting updates as you type.
- Change `{:fill_portion, 1}` to `{:fill_portion, 2}` on the editor pane. Save the file. The editor takes twice the width.
- Add a second button to the toolbar row: `button("clear", "Clear")`. Save and see it appear.

The pad is a shell right now, a text editor next to an empty preview. In the next chapter, we will bring it to life by wiring up hot reload and code compilation so you can write Plushie widgets and see them rendered instantly.

Next: The Development Loop

← Previous Page Getting Started
Next Page → The Development Loop