Dala provides a Spark DSL for defining screens declaratively. This guide explains how to use it.
Overview
The Spark DSL allows you to define screens using a declarative syntax instead of writing render functions manually. It provides:
- Attribute declarations for screen state
- UI component entities for building the interface
- @ref syntax for referencing assign values in strings
- Compile-time verification for prop validation
- Automatic mount function generation
Getting Started
To use the Spark DSL, add use Dala.Spark.Dsl (or use Dala.Screen) to your screen module:
defmodule MyApp.CounterScreen do
use Dala.Spark.Dsl
attributes do
attribute :count, :integer, default: 0
end
screen do
name :counter
column do
gap :space_sm
text "Count: @count"
button "Increment", on_tap: :increment
end
end
def handle_event(:increment, _params, socket) do
{:noreply, Dala.Socket.assign(socket, :count, socket.assigns.count + 1)}
end
endAttributes
Attributes define the screen's state. They are automatically initialized in the generated mount/3 function.
Syntax
attribute :name, :type, default: valueSupported Types
:integer:string:boolean:float:atom:list:map
Example
attributes do
attribute :count, :integer, default: 0
attribute :message, :string, default: "Hello"
attribute :visible, :boolean, default: true
attribute :items, :list, default: []
endScreen Section
The screen section holds all UI components. It requires a name option:
screen do
name :my_screen
# components go here
endLayout Containers
Container components support nested children via do...end blocks. Props are set inside the block:
Column (VStack)
column do
padding :space_md
gap :space_sm
text "Title"
text "Subtitle"
endRow (HStack)
row do
gap :space_sm
icon "settings"
text "Settings"
endBox (ZStack)
Children overlap — useful for overlays:
box do
image "bg.jpg"
text "Overlay", text_color: :white
endScroll
scroll do
padding :space_md
text "Long content..."
endModal
modal do
visible true
on_dismiss :dismissed
text "Modal content"
endPressable
pressable do
on_press :card_tapped
text "Tap me"
endSafeArea
safe_area do
text "Safe content"
endLeaf Components
Leaf components have no children. They accept props as keyword arguments:
Text
text "Hello, world!"
text "Count: @count", text_size: :xl, text_color: :on_surface
text "Title", font_weight: "bold", text_align: :centerButton
button "Press me", on_tap: :button_pressed
button "Submit", on_tap: :submit, background: :primary, text_color: :on_primary
button "Disabled", on_tap: :action, disabled: trueIcon
icon "settings", text_size: 24, text_color: :on_surface
icon "chevron_right", on_tap: :navigateImage
image "https://example.com/photo.jpg"
image "logo.png", width: 100, height: 100, resize_mode: :containTextField
text_field placeholder: "Enter name", on_change: :name_changed
text_field keyboard_type: :email, return_key: :next, on_submit: :next_fieldToggle
toggle value: true, on_change: :notifications_toggled, text: "Notifications"Slider
slider value: 0.5, min_value: 0, max_value: 100, on_change: :volume_changedSwitch (legacy)
switch value: true, on_toggle: :toggledVideo
video "https://example.com/clip.mp4", autoplay: true, loop: trueOther Leaf Components
divider()— horizontal divider linespacer()— flexible space (orspacer size: 20for fixed)activity_indicator size: :large, color: :primary— loading spinnerprogress_bar progress: 0.7, color: :primary— progress barstatus_bar bar_style: :light_content— status bar controlrefresh_control on_refresh: :reload, refreshing: false— pull-to-refreshwebview "https://elixir-lang.org"— native web viewcamera_preview facing: :front— live camera feednative_view MyComponent, id: :my_view— platform-native componenttab_bar tabs: [%{id: "home", label: "Home"}]— tab navigationlist :my_list, data: @items— data-driven list
@ref Syntax
The @ref syntax allows you to reference assign values in strings. It's processed at compile time and replaced with runtime assign access.
Basic Usage
text "Count: @count" # Becomes: "Count: " <> to_string(assigns[:count])In Props
button "@message", on_tap: :press # Button text uses the @message assignCompile-time Verification
The DSL includes verifiers that check your declarations at compile time:
- Validates that all event handler props (
on_tap,on_change, etc.) are atoms - Validates that attribute types are supported
- Provides helpful error messages for misconfigurations
Generated Functions
The DSL transformers automatically generate:
mount/3
Initializes all attributes with their default values. Always generated, even without attributes:
def mount(_params, _session, socket) do
socket = Dala.Socket.assign(socket, :count, 0)
{:ok, socket}
endrender/1
Builds the component tree from your DSL declarations. Returns a list of top-level node maps:
def render(assigns) do
[
%{
type: :column,
props: %{gap: :space_sm},
children: [
%{type: :text, props: %{text: "Count: " <> to_string(assigns[:count])}, children: []}
]
}
]
endEvent Handling
Event handlers are defined as regular handle_event/3 functions. The on_tap, on_change, etc. props reference these handlers by atom name:
def handle_event(:increment, _params, socket) do
{:noreply, socket}
endIntegration with Dala.App
Register Spark DSL screens in your app's navigation/1 using Dala.App.screens/1:
defmodule MyApp do
use Dala.App
def navigation(_) do
screens([MyApp.HomeScreen, MyApp.CounterScreen, MyApp.SettingsScreen])
stack(:home, root: MyApp.HomeScreen)
end
endMigration from Manual Screens
To migrate an existing screen to the Spark DSL:
- Add
use Dala.Spark.Dsl(or keepuse Dala.Screen) to your module - Move state declarations to
attributes do ... end - Move render logic to the
screen do ... endblock - Remove the manual
mount/3andrender/1functions - Keep
handle_event/3functions as-is
Before
defmodule MyApp.Counter do
use Dala.Screen
def mount(_params, _session, socket) do
{:ok, Dala.Socket.assign(socket, :count, 0)}
end
def render(assigns) do
Dala.Ui.Widgets.column([padding: :space_md, gap: :space_sm], [
Dala.Ui.Widgets.text(text: "Count: #{assigns.count}"),
Dala.Ui.Widgets.button(text: "Increment", on_tap: :increment)
])
end
def handle_event(:increment, _params, socket) do
{:noreply, Dala.Socket.assign(socket, :count, socket.assigns.count + 1)}
end
endAfter
defmodule MyApp.Counter do
use Dala.Spark.Dsl
attributes do
attribute :count, :integer, default: 0
end
screen do
name :counter
column do
padding :space_md
gap :space_sm
text "Count: @count"
button "Increment", on_tap: :increment
end
end
def handle_event(:increment, _params, socket) do
{:noreply, Dala.Socket.assign(socket, :count, socket.assigns.count + 1)}
end
end