This guide explains how to design UI components in Dala using two approaches:
- Sigil Style — Phoenix-style
~dalasigil (imported automatically withuse Dala.Screen) - DSL Style — Declarative
Dala.Spark.Dsl(Spark-based DSL)
Both styles produce identical output and are accepted by Dala.Renderer. Choose the style that fits your workflow.
Quick Comparison
| Aspect | Sigil Style | DSL Style |
|---|---|---|
| Syntax | ~dala"""...""" | dala do...end block |
| Learning curve | Familiar to Phoenix/LiveView devs | More declarative, less Elixir boilerplate |
| State management | Manual mount/3 + assigns | attribute declarations, auto-generated mount/3 |
| Render function | Manual render/1 | Auto-generated from screen block |
| @ref syntax | Not available | @count expands to assigns.count |
| Compile-time checks | Tag whitelist validation | Attribute types, handler verification |
| Best for | Complex rendering logic, dynamic UIs | Standard screens, rapid prototyping |
Sigil Style (Phoenix-style)
The sigil style uses the ~dala sigil to write declarative UI templates that compile to plain Elixir maps at compile time.
Basic Structure
defmodule MyApp.CounterScreen do
use Dala.Screen
def mount(_params, _session, socket) do
socket = Dala.Socket.assign(socket, :count, 0)
{:ok, socket}
end
def render(assigns) do
increment_tap = {self(), :increment}
~dala"""
<Column padding={:space_md}>
<Text text={"Count: #{assigns.count}"} text_size={:xl} />
<Spacer size={16} />
<Button text="Increment" on_tap={increment_tap} />
</Column>
"""
end
def handle_info({:tap, :increment}, socket) do
new_count = Dala.Socket.get_assign(socket, :count) + 1
socket = Dala.Socket.assign(socket, :count, new_count)
{:noreply, socket}
end
endKey Points
- Import:
use Dala.Screenautomatically importsDala.Sigil - Handler tuples: Pre-compute
{self(), :tag}tuples before the sigil - Expressions: Use
{...}for Elixir expressions in attributes or children - Assigns: Access via
assigns.keyorDala.Socket.get_assign(socket, :key)
Expression Slots
~dala"""
<Column>
{Enum.map(assigns.items, fn item ->
~dala(<Text text={item} padding={:space_sm} />)
end)}
</Column>
"""When to Use Sigil Style
- You're comfortable with Phoenix/LiveView syntax
- You need complex rendering logic with conditionals or loops
- You want full control over
mount/3andrender/1 - You're migrating from Phoenix LiveView
DSL Style (Spark DSL)
The DSL style uses Dala.Spark.Dsl to define screens declaratively with automatic boilerplate generation.
Basic Structure
defmodule MyApp.CounterScreen do
use Dala.Spark.Dsl
dala do
attribute :count, :integer, default: 0
screen name: :counter do
text "Count: @count", text_size: :xl
spacer size: 16
button "Increment", on_tap: :increment
end
end
def handle_event(:increment, _params, socket) do
new_count = Dala.Socket.get_assign(socket, :count) + 1
socket = Dala.Socket.assign(socket, :count, new_count)
{:noreply, socket}
end
endKey Points
- Attributes: Declare state with
attribute :name, :type, default: value - @ref syntax: Use
@countin strings to reference assigns (expanded at compile time) - Auto-generation:
mount/3andrender/1are generated automatically - Handlers: Reference as atoms (
:increment) instead of{self(), :tag}tuples - Events: Use
handle_event/3instead ofhandle_info/2for component events
@ref Syntax
The @ref syntax is processed at compile time and replaced with assigns.key access:
dala do
attribute :message, :string, default: "Hello"
screen do
text "@message" # Becomes: "Hello"
text "Value: @count" # Becomes: "Value: " <> assigns.count
end
endWhen to Use DSL Style
- You want less boilerplate (no manual
mount/3orrender/1) - You're building standard screens with declarative UI
- You want compile-time verification of attributes and handlers
- You prefer a more concise, DSL-driven approach
- You're rapidly prototyping
Component Design Patterns
Shared Components with Sigil Style
Create reusable functions that return node maps:
defmodule MyApp.Components do
import Dala.Sigil
def card(title, content) do
~dala"""
<Box background={:surface} padding={:space_md} corner_radius={:radius_md}>
<Text text={title} text_size={:lg} font_weight="bold" />
<Spacer size={8} />
<Text text={content} text_color={:muted} />
</Box>
"""
end
end
# Usage in screen:
def render(assigns) do
~dala"""
<Column>
{MyApp.Components.card("Welcome", "Hello there!")}
</Column>
"""
endShared Components with DSL Style
Use helper functions within the DSL:
defmodule MyApp.CounterScreen do
use Dala.Spark.Dsl
dala do
attribute :count, :integer, default: 0
screen name: :counter do
text "Count: @count"
my_button "Increment", on_tap: :increment
end
end
# Helper function for DSL
def my_button(label, opts) do
button label, Keyword.merge([background: :primary, text_color: :on_primary], opts)
end
def handle_event(:increment, _params, socket) do
new_count = Dala.Socket.get_assign(socket, :count) + 1
socket = Dala.Socket.assign(socket, :count, new_count)
{:noreply, socket}
end
endConditional Rendering
Sigil Style:
def render(assigns) do
~dala"""
<Column>
<Text text="Always visible" />
{if assigns.show_details do
~dala(<Text text="Details here" />)
else
~dala(<Text text="Hidden" />)
end}
</Column>
"""
endDSL Style:
dala do
attribute :show_details, :boolean, default: false
screen do
text "Always visible"
if @show_details do
text "Details here"
else
text "Hidden"
end
end
endLists and Iteration
Sigil Style:
def render(assigns) do
~dala"""
<Column>
{Enum.map(assigns.items, fn item ->
~dala(<Text text={item} padding={:space_sm} />)
end)}
</Column>
"""
endDSL Style:
dala do
attribute :items, :list, default: []
screen do
# Use expression child slot
{Enum.map(@items, fn item ->
text item, padding: :space_sm
end)}
end
endEvent Handling
Sigil Style Events
Uses handle_info/2 with tagged tuples:
def render(assigns) do
save_tap = {self(), :save}
~dala(<Button text="Save" on_tap={save_tap} />)
end
def handle_info({:tap, :save}, socket) do
# Handle save
{:noreply, socket}
endDSL Style Events
Uses handle_event/3 with atom references:
dala do
screen do
button "Save", on_tap: :save
end
end
def handle_event(:save, _params, socket) do
# Handle save
{:noreply, socket}
endStyling and Theming
Both styles support Dala.Style for reusable styles:
@card_style %Dala.Style{props: %{background: :surface, padding: :space_md}}
# Sigil style
~dala(<Box style={@card_style}>...</Box>)
# DSL style (using Dala.Ui.Widgets.box or expression)
{[:box, style: @card_style, children: [...]]}Migration Between Styles
Sigil → DSL
- Add
use Dala.Spark.Dslto your module - Move state to
attributedeclarations - Move render logic to
screenblock - Remove manual
mount/3andrender/1 - Convert
{self(), :tag}to:tagatoms - Convert
handle_info/2tohandle_event/3 - Use
@refsyntax for assign references
DSL → Sigil
- Replace
use Dala.Spark.Dslwithuse Dala.Screen - Add manual
mount/3withDala.Socket.assign/3calls - Add manual
render/1returning~dalasigil - Convert
:tagatoms to{self(), :tag}tuples - Convert
handle_event/3tohandle_info/2 - Replace
@refwithassigns.keyor string interpolation
Best Practices
Sigil Style
- Pre-compute handler tuples before the sigil
- Use
Dala.Ui.Widgetshelper functions for complex nodes - Extract reusable UI into component functions
- Keep
render/1pure — no side effects
DSL Style
- Use descriptive attribute names
- Leverage
@refsyntax for cleaner templates - Keep
handle_event/3functions focused - Use the
screenblock for static layouts, expression slots for dynamic content
Mixing Styles
You can mix both styles in the same project, but not in the same screen module. Choose one style per screen for consistency.
For shared utilities, Dala.Ui.Widgets functions work with both styles since they return plain maps:
# Works in both sigil and DSL screens
Dala.Ui.Widgets.text(text: "Hello")Further Reading
- Components — Detailed sigil syntax and component reference
- Spark DSL — In-depth DSL documentation
- Theming — Colors, spacing, and typography tokens
- Events — Event system and message passing