ToDo
The ToDo example is one of the best Ruflet tutorials because it looks like a real product feature.
Instead of reading the finished file as one block, build it in layers:
- create the app and state
- render the input
- store tasks
- render task rows
- add filters
- wire task actions
The production example lives here:
examples/todo.rbStep 1: create the app and initial state
The real example uses a class-based app with a few instance variables.
require TOKPLACEHOLDER0TOKEN
class TodoApp < Ruflet::App
FILTERS = %w[all active completed].freeze
def initialize
super
@tasks = []
@next_id = 1
@filter = TOKPLACEHOLDER1TOKEN
@draft = TOKPLACEHOLDER2TOKEN
end
endWhy this structure is good:
@tasksstores the list@next_idgives each task a stable identifier@filtercontrols which tasks are visible@draftmirrors the current input field value
Step 2: build the page shell
Start with a simple centered card.
def view(page)
page.title = TOKPLACEHOLDER0TOKEN
page.vertical_alignment = TOKPLACEHOLDER1TOKEN
page.horizontal_alignment = TOKPLACEHOLDER2TOKEN
render(page)
endUse a dedicated render(page) method just like the example does. That makes full rerenders easy after each action.
Step 3: add the draft input
The real app keeps the text field controlled by @draft.
input = text_field(
value: @draft,
hint_text: TOKPLACEHOLDER0TOKEN,
on_change: ->(e) { @draft = e.data.to_s },
on_submit: ->(e) { add_task(e.page) }
)This is an important Ruflet pattern:
- state lives on the app object
- the field updates that state on change
- submit actions call a method that mutates state and rerenders
Step 4: add the button row
Next, place the input beside an Add button.
def add_row(page, input)
add_button = elevated_button(
content: text(TOKPLACEHOLDER0TOKEN),
on_click: ->(e) { add_task(e.page) }
)
row(spacing: 10, children: [input, add_button])
endIn the full example this becomes responsive, switching to a column on small screens. Start simple first, then add responsiveness after the behavior works.
Step 5: create the add action
Now make the button do something.
def add_task(page)
task_text = @draft.to_s.strip
return if task_text.empty?
@tasks << { id: @next_id, text: task_text, done: false }
@next_id += 1
@draft = TOKPLACEHOLDER0TOKEN
render(page)
endThis small method teaches a lot:
- validate first
- append a new task hash
- reset the draft
- rerender the screen
Step 6: render the task list
The real example turns each task into its own row helper.
def task_row(page, task)
label = task[:done] ? TOKPLACEHOLDER0TOKEN : task[:text]
row(
alignment: TOKPLACEHOLDER1TOKEN,
vertical_alignment: TOKPLACEHOLDER2TOKEN,
children: [
checkbox(
value: task[:done],
label: label,
expand: true,
on_change: ->(e) { toggle_task(task[:id], e.page) }
),
text_button(
content: text(TOKPLACEHOLDER3TOKEN),
on_click: ->(e) { delete_task(task[:id], e.page) }
)
]
)
endThen in render(page):
task_controls = if filtered_tasks.empty?
[
container(
padding: 16,
content: text(TOKPLACEHOLDER0TOKEN)
)
]
else
filtered_tasks.map { |task| task_row(page, task) }
endThis is a very Ruflet-friendly pattern:
- Ruby data in
- control list out
Step 7: add filtering
The example supports all, active, and completed.
def filtered_tasks
case @filter
when TOKPLACEHOLDER0TOKEN
@tasks.reject { |task| task[:done] }
when TOKPLACEHOLDER1TOKEN
@tasks.select { |task| task[:done] }
else
@tasks
end
endNow build the filter buttons:
def filter_button(page, name)
selected = (@filter == name)
if selected
filled_button(
content: text(name.capitalize),
on_click: ->(e) { set_filter(name, e.page) }
)
else
text_button(
content: text(name.capitalize),
on_click: ->(e) { set_filter(name, e.page) }
)
end
endThis teaches a useful product pattern:
- the active filter changes both data and visual style
Step 8: wire task actions
The rest of the example is a set of small mutations:
def toggle_task(task_id, page)
task = @tasks.find { |item| item[:id] == task_id }
return unless task
task[:done] = !task[:done]
render(page)
end
def delete_task(task_id, page)
@tasks.reject! { |task| task[:id] == task_id }
render(page)
end
def set_filter(name, page)
@filter = name
render(page)
end
def clear_completed(page)
@tasks.reject! { |task| task[:done] }
render(page)
endThe key lesson here is consistency:
- every action changes Ruby state
- every action rerenders the page
That makes the whole app easy to reason about.
Step 9: make the layout feel complete
The real example adds:
- a card container
- an app bar
- a floating action button
- a footer with counts and filters
- responsive sizing based on
page.client_details
Those are polish layers you can add after the core flow works.
Run the real example
-function">cd examples
-function">bundle install
-function">bundle exec -function">ruflet run todoWhat you learned
- how to manage list state in Ruflet
- how to turn arrays into control trees
- how to build realistic add, toggle, delete, and filter flows
- how rerender-based UI can stay simple and productive