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 name: :counter do
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: keyword argument:
screen name: :my_screen do
# components go here
endLayout Containers
Container components support nested children via do...end blocks. Props are set as function calls 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
horizontal false
show_indicator true
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"
endCard
card variant: :elevated, elevation: 2.0, corner_radius: 12 do
text "Card content"
endBadge
badge count: 5, color: :error do
icon "notifications"
endBottomSheet
bottom_sheet visible: true, on_dismiss: :dismissed, drag_indicator: true do
text "Sheet content"
endTooltip
tooltip text: "Helpful info", position: :top do
icon "help"
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 MyApp.ChartComponent, id: :revenue_chart— platform-native componenttab_bar tabs: [%{id: "home", label: "Home"}]— tab navigationlist :my_list, data: @items— data-driven listcheckbox value: true, on_change: :agree_toggled, label: "I agree"— checkbox inputradio selected: true, on_select: :option_a, label: "Option A", group: "choices"— radio buttonchip label: "Filter", variant: :filter, selected: true, on_tap: :chip_tapped— chip/tagsnackbar message: "Item deleted", action_label: "Undo", on_action: :undo— snackbar/toastfab icon: "edit", text: "Compose", on_tap: :compose— floating action buttonicon_button icon: "favorite", on_tap: :favorite_tapped— icon-only buttonsegmented_button segments: [%{id: "day", label: "Day"}, %{id: "week", label: "Week"}], selected: "week", on_select: :range_changed— segmented controlapp_bar title: "My App", leading_icon: "back", on_leading: :back_pressed— top app barnav_bar items: [%{id: "home", label: "Home", icon: "home"}], active: "home", on_select: :tab_changed— bottom nav barnav_drawer visible: true, on_dismiss: :dismissed, items: [...], active: "home", on_select: :nav_changed— nav drawernav_rail items: [%{id: "home", label: "Home", icon: "home"}], active: "home", on_select: :rail_changed— nav railmenu items: [%{label: "Edit", action: :edit}], visible: true, on_select: :menu_selected— dropdown menudate_picker visible: true, on_select: :date_picked, selected_date: "2025-01-15"— date pickertime_picker visible: true, on_select: :time_picked, selected_time: "09:30"— time pickersearch_bar placeholder: "Search...", on_change: :search_changed, on_submit: :search_submitted— search barcarousel :my_carousel, items: @slides, on_page_change: :page_changed— carousel/slideshow
@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}
endPubSub Subscriptions
The Spark DSL supports declarative PubSub subscriptions via the pubsub section. Topics are subscribed when the screen mounts and automatically unsubscribed when the screen terminates.
defmodule MyApp.ChatScreen do
use Dala.Spark.Dsl
attributes do
attribute :messages, :list, default: []
end
pubsub do
subscribe "chat:room:123", on_message: :handle_chat
end
screen name: :chat do
column do
text "Messages: @messages"
end
end
def handle_chat({:message, text}, socket) do
messages = socket.assigns.messages ++ [text]
{:noreply, Dala.Socket.assign(socket, :messages, messages)}
end
endUse Dala.PubSub to set up a PubSub instance and broadcast messages:
# In your app supervision tree:
children = [
{Dala.PubSub, name: MyApp.PubSub}
]
# Broadcast a message:
Dala.PubSub.broadcast(MyApp.PubSub, "chat:room:123", {:message, "Hello!"})Integration 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