A Breeze View is a process that handles events, updates its states and renders to the terminal. Breeze Views are inspired by Phoenix LiveView, but not 100% compatible.
Prior art
Breeze's template/component ergonomics are inspired by Phoenix LiveView. Relevant upstream modules:
- https://github.com/phoenixframework/phoenix_live_view/blob/main/lib/phoenix_component.ex
- https://github.com/phoenixframework/phoenix_live_view/blob/main/lib/phoenix_live_view/tag_engine.ex
Usage
The module can be used by including use Breeze.View:
defmodule Demo do
use Breeze.View
enduse Breeze.View declares the Breeze.View behaviour. The callbacks are
optional because the same module is also used to define component-only
modules, but a root view must implement render/1.
Initial state
The initial state can be set in the mount callback:
def mount(_opts, term), do: {:ok, assign(term, counter: 0)}Rendering
Rendering is performed using Breeze's ~H template sigil.
def render(assigns) do
~H"<box>Counter: <%= @counter %></box>"
endThere are a handful of supported attributes:
id- the id of the element. This is required for focusables and implicitsfocusable- if the element should be added to the focus tree. These are added in the order they appear, and can be toggled using tab/shift-tab. Thefocusstyle state can be used to style these. E.g. class="border focus:border-3"default-focus- marks the preferred focus target when a view or focus scope becomes activefocus-scope- defines a focus region. Setfocus-scope="trap"to keep tab traversal inside itclass- token-based styling for the box. This is covered in the Style section.style- inline style maps or%BackBreeze.Style{}values for the box. Passing a binary remains backwards compatible.implicit- this is a module that will be used for implicit state. This is covered in the Implicits section.
Handling events
Events that come from the terminal or an implicit are handled in the
optional handle_event/3 callback:
def handle_event(_, %{"key" => "ArrowUp"}, term) do
{:noreply, assign(term, counter: term.assigns.counter + 1)}
end
def handle_event(_, %{"key" => "ArrowDown"}, term) do
{:noreply, assign(term, counter: term.assigns.counter - 1)}
end
def handle_event(_, %{"key" => "q"}, term) do
{:stop, term}
end
def handle_event(_, _, term) do
{:noreply, term}
endFor convenience, keys are converted to a more friendly representation for example, instead of sending "A" which is provided by the terminal, we convert it to "ArrowUp".
If handle_event/3 is not implemented, events that reach the view are
ignored. If it is implemented, normal Elixir function clause matching
applies.
Any other messages sent to the process are handled using the optional
handle_info/2 callback:
def handle_info(:some_message, term), do: {:noreply, term}If handle_info/2 is not implemented, those messages are ignored.
Style
Breeze supports two styling inputs:
class- string tokens such asborder,width-15,text-3style- a%BackBreeze.Style{}struct or a map for inline values
Passing a binary to style remains supported for backwards compatibility.
A box can be styled similar to CSS using the class attribute:
<box class="bold text-3 border width-15">Hello World</box>Inline maps can be used when you want direct BackBreeze values:
<box style={%{border: :rounded, border_color: 3, width: 15}}>Hello World</box>The following styles are supported:
border- add a line border to the boxborder-square- add a square block border to the boxbold- make the text bolditalic- make the text italicinverse- reverse the foreground-backgroundreverse- reverse the foreground-backgroundinline- display the elements inline (join horizontally)grid- lay out child elements in a gridgrid-cols-n- set the number of grid columnsgrid-rows-n- set the number of grid rowsgap-x-n- set the horizontal gap between grid cellsgap-y-n- set the vertical gap between grid cellswidth-x- set the width of the elementheight-x- set the height of the elementoverflow-hidden- clip child content to the viewportoffset-top-x- vertically scroll content by x rowsoffset-left-x- horizontally scroll content by x columnsabsolute- position the elements absolute relative to the parent
Grid layout
Grid children flow from left to right and then onto the next row. Use
grid-cols-n and, when a fixed row count is useful, grid-rows-n to define
the tracks. gap-x-n and gap-y-n add horizontal and vertical spacing in
terminal cells.
<box class="grid grid-cols-2 grid-rows-2 gap-x-1 gap-y-1 width-full">
<box>One</box>
<box>Two</box>
<box>Three</box>
<box>Four</box>
</box>Colors
text- set the foreground to the theme's default text colorbg- set the background to the theme's default background colortext-x- set the foreground colorbg-x- set the background colorborder-x- set the border colorscrollbar-x- set the scrollbar colorplaceholder-text-x- set an input placeholder's foreground color
For the color classes above, x can be a numeric ANSI color index or a
theme variable such as primary, muted, or panel. See
Breeze.Theme for the complete variable reference and
custom theme configuration.
<box class="bg-panel border-primary text-muted">Status</box>Implicits
Implicits add stateful event handling outside the view. Implement the Breeze.Implicit behaviour to package that logic as a reusable renderer extension.
For example, consider a list component:
def render(assigns) do
~H"""
<.list id="my-list" br-change="my_custom_event">
<:item value="hello">Hello</:item>
<:item value="world">World</:item>
<:item value="foo">Foo</:item>
</.list>
"""
end
def handle_event("my_custom_event", %{value: value}, term), do: ...Ideally, we don't want to have to keep track of the selected value, handle key events, scroll position, viewport overflow, etc. within our view. We might only care about the selected value. In this case, we can define the list component to use an implicit state module.
attr :id, :string, required: true
attr :rest, :global
slot :item do
attr :value, :string, required: true
end
def list(assigns) do
~H"""
<box
id={@id}
focusable
class="border focus:border-primary"
implicit={MyAppList}
{@rest}
>
<box
:for={item <- @item}
value={item.value}
class="selected:bg-primary selected:text-background focus:selected:bg-accent focus:selected:text-background"
>
{render_slot(item)}
</box>
</box>
"""
endThe implicit module is first called with an init/2 callback (or optional init/3).
It receives all child element attributes and the previous state. The init/3 form
also receives root attributes for the implicit container.
init can either return the implicit state directly, or {:ok, state, options}.
The options form is used for renderer-driven animation behavior such as periodic rerenders.
defmodule MyAppList do
@behaviour Breeze.Implicit
def init(children, root_attrs, last_state) do
%{values: Enum.map(children, &(&1.value)), selected: last_state[:selected], root: root_attrs}
end
enddef init(_children, _root_attrs, last_state) do
{:ok, last_state, rerender_every: 500}
endIf rerender_every is set, Breeze will periodically call animate/5 when it is
implemented. The final argument includes timing context such as :now,
:frame, :last_render_at, :last_interaction_at, :pending?, and :focused?.
There is also a handle_event/3 callback. This is similar to the callback for a view, but
returns different values. Here we handle key events and return a :change event along
with the new state. The :change will be used by br-change to pass through to the handle_event
callback of the Breeze.View.
def handle_event(_, %{"key" => "ArrowDown"}, %{values: values} = state) do
index = Enum.find_index(values, &(&1 == state.selected))
value = if index, do: Enum.at(values, index + 1) || hd(values), else: hd(values)
{{:change, %{value: value}}, %{state | selected: value}}
end
def handle_event(_, %{"key" => "ArrowUp"}, %{values: values} = state) do
index = Enum.find_index(values, &(&1 == state.selected))
first = hd(Enum.reverse(values))
value = if index, do: Enum.at(values, index - 1) || first, else: first
{{:change, %{value: value}}, %{state | selected: value}}
end
def handle_event(_, _, state), do: {:noreply, state}There are two final handlers used during rendering.
animate/5 can transform the rendered BackBreeze.Box for lightweight
renderer-driven animation and other presentation changes. It can return either
the updated box directly or {:ok, box, overlays: overlays} to request
terminal overlays during async animation passes.
handle_modifiers/3 receives :root or :child as the first argument and can be
used to tell the renderer things about the state.
Return values can include style flags (for example selected: true) and structured
scroll modifiers (scroll_y, scroll_x, or scroll: {top, left}).
Root implicit modifiers can also influence focus handling:
default_focus: true- mark the root element as the preferred focus targetfocus_scope: :trap- constrain tab/shift-tab navigation to this implicit subtree
def handle_modifiers(:child, attributes, state) do
if state.selected == Keyword.get(attributes, :value) do
[selected: true]
else
[]
end
end
def handle_modifiers(:root, _attributes, state) do
[scroll_y: state.offset]
end
Summary
Types
Assigns passed to a view or function component.
A decoded terminal or implicit event payload.
A view event name.
Compiled template output returned by the ~H sigil.
A valid return value from a view event or message callback.
An option returned alongside an event or message reply.
Runtime slot entry generated by Breeze.Template
Callbacks
Handles a terminal or implicit event.
Handles a message sent to the view process.
Initializes a view with its startup options and term state.
Renders a view or component from its assigns.
Functions
Return the currently active keybinding hints for the term.
Merge values into term.assigns, or into a plain assigns map inside a component function.
Declares an attribute for the next function component.
Declares an attribute for the next function component.
Clear flash messages from assigns.breeze.flash.
Cycles a term through Breeze's standard theme set.
Cycles a term through a configurable theme set, or handles a global keybinding event using the standard theme set.
Handles a global keybinding event using a configurable theme set.
Sets the focused element ID, or clears focus when value is nil.
Append a flash message to assigns.breeze.flash.
Set keybindings that are only active when the given local focus target is focused.
Set the implicit state for the given element ID.
Set keybindings that are active anywhere inside the current view subtree.
Set the active Breeze theme for the current term.
Render a slot value using empty assigns.
Render a slot value with assigns passed to the slot render function.
Reset the implicit state for the given element ID, causing it to reinitialise on the next render.
Compiles a Breeze template from an ~H sigil.
Declares a slot for the next function component.
Declares a slot for the next function component.
Set a named theme and update assigns.breeze.theme metadata.
Update the implicit state for the given element ID in place.
Types
@type assigns() :: map()
Assigns passed to a view or function component.
@type event() :: map()
A decoded terminal or implicit event payload.
@type event_name() :: term()
A view event name.
@type rendered() :: term()
Compiled template output returned by the ~H sigil.
@type reply() :: {:noreply, Breeze.Term.t()} | {:noreply, Breeze.Term.t(), [reply_option()]} | {:stop, Breeze.Term.t()} | {:stop, Breeze.Term.t(), [reply_option()]}
A valid return value from a view event or message callback.
@type reply_option() :: {:invalidate, boolean()}
An option returned alongside an event or message reply.
Runtime slot entry generated by Breeze.Template
Callbacks
@callback handle_event(event_name(), event(), Breeze.Term.t()) :: reply()
Handles a terminal or implicit event.
@callback handle_info(term(), Breeze.Term.t()) :: reply()
Handles a message sent to the view process.
@callback mount(keyword(), Breeze.Term.t()) :: {:ok, Breeze.Term.t()}
Initializes a view with its startup options and term state.
Renders a view or component from its assigns.
Functions
Return the currently active keybinding hints for the term.
@spec assign(map(), Enumerable.t()) :: map()
Merge values into term.assigns, or into a plain assigns map inside a component function.
Declares an attribute for the next function component.
Declares an attribute for the next function component.
Options include :required, :default, and :doc.
Clear flash messages from assigns.breeze.flash.
Without a second argument all flash messages are removed. With a second argument, messages matching that kind or id are removed.
Cycles a term through Breeze's standard theme set.
Cycles a term through a configurable theme set, or handles a global keybinding event using the standard theme set.
Pass :themes with a list of built-in names or {name, theme} tuples to
customize the cycle. The event-handler form can be used directly in a global
keybinding:
global_keybindings: [{"F3", "Cycle theme", &Breeze.View.cycle_theme/2}]
Handles a global keybinding event using a configurable theme set.
@spec focus(Breeze.Term.t(), String.t() | nil) :: Breeze.Term.t()
Sets the focused element ID, or clears focus when value is nil.
Append a flash message to assigns.breeze.flash.
The resulting flash assign is a stack-friendly list consumed by
Breeze.Blocks.flash_group/1.
term
|> put_flash(:info, "Saved", max: 3, duration: 5_000)
|> put_flash(:error, "Publish failed", id: "publish-error", highlight: "error")The left highlight strip can be customized with :highlight or :color.
It accepts Breeze semantic color names, ANSI color indexes, RGB tuples, and
"#rgb"/"#rrggbb" hex strings.
Set keybindings that are only active when the given local focus target is focused.
Set the implicit state for the given element ID.
Set keybindings that are active anywhere inside the current view subtree.
Set the active Breeze theme for the current term.
@spec render_slot(nil | slot_entry() | [slot_entry()]) :: binary()
Render a slot value using empty assigns.
Accepts nil, a single slot entry, or a list of slot entries.
@spec render_slot(nil | slot_entry() | [slot_entry()], map() | keyword() | nil) :: binary()
Render a slot value with assigns passed to the slot render function.
Reset the implicit state for the given element ID, causing it to reinitialise on the next render.
Compiles a Breeze template from an ~H sigil.
Declares a slot for the next function component.
Declares a slot for the next function component.
Use a do block to declare attributes accepted by each slot entry. Options
include :required and :doc.
Set a named theme and update assigns.breeze.theme metadata.
The theme can be one of Breeze's built-in cycle names (:system16, :system,
:nebula, :catppuccin, :dracula, :gruvbox, :nord,
:solarized_light, or :solarized_dark) or a {name, theme} tuple.
Update the implicit state for the given element ID in place.