In this tutorial, we'll build something a bit more substantial and useful: a to-do list with realtime sync, in under 75 lines of (well-formatted, well-commented) code.
More importantly, this tutorial will not introduce any new concepts. Everything you need to know about authoring interactive apps using Wave is already covered in the previous tutorial. From this point on, it's mostly a matter of abstraction, which is a fancy term for how you solve the problem at hand using short, simple, clear, elegant, modular functions that do one thing and do it well.
We'll start with a basic skeleton, and then work our way up from there.
The first step is to define an @app function. Also, we want the landing page to show a list of to-dos, so we'll throw in an empty show_todos() function for now, and call it from serve().
A to-do item has some basic attributes: an ID, some text content, and whether it's completed or not. Let's define a class for that, with a global one-up id.
Next, we turn each incomplete to-do item into a checkbox (using ui.checkbox()), and display it in a form card (using ui.form_card()).
Also, we want each checkbox to raise an event immediately when checked, so we set its trigger attribute to True.
tip
Several components have a trigger attribute. Normally, an event is triggered only when a command-like component (a button, menu, or tab) is clicked. If you want a component to immediately trigger an event when changed, set trigger to True.
$HOME/wave-apps/todo.py
from typing import List
from h2o_wave import Q, main, app, ui
_id =0
# A simple class that represents a to-do item.
classTodoItem:
def__init__(self, text):
global _id
_id +=1
self.id=f'todo_{_id}'
self.text = text
self.done =False
@app('/todo')
asyncdefserve(q: Q):
show_todos(q)
await q.page.save()
defshow_todos(q: Q):
# Get items for this user.
todos: List[TodoItem]= q.user.todos
# Create a sample list if we don't have any.
if todos isNone:
q.user.todos = todos =[TodoItem('Do this'), TodoItem('Do that'), TodoItem('Do something else')]
# Create checkboxes.
not_done =[ui.checkbox(name=todo.id, label=todo.text, trigger=True)for todo in todos ifnot todo.done]
We also turn each completed to-do item into another list of checkboxes, checked by default (using its value attribute). We append this to the form card, and put a separator in between (using ui.separator()) to distinguish the completed items from the incomplete ones.
$HOME/wave-apps/todo.py
from typing import List
from h2o_wave import Q, main, app, ui
_id =0
# A simple class that represents a to-do item.
classTodoItem:
def__init__(self, text):
global _id
_id +=1
self.id=f'todo_{_id}'
self.text = text
self.done =False
@app('/todo')
asyncdefserve(q: Q):
show_todos(q)
await q.page.save()
defshow_todos(q: Q):
# Get items for this user.
todos: List[TodoItem]= q.user.todos
# Create a sample list if we don't have any.
if todos isNone:
q.user.todos = todos =[TodoItem('Do this'), TodoItem('Do that'), TodoItem('Do something else')]
# Create done/not-done checkboxes.
done =[ui.checkbox(name=todo.id, label=todo.text, value=True, trigger=True)for todo in todos if todo.done]
not_done =[ui.checkbox(name=todo.id, label=todo.text, trigger=True)for todo in todos ifnot todo.done]
Next, let's display a form to add new items to our list. For that, we'll add a new button to our existing form, named new_todo, and direct the serve() function to the new_todo() function if the button is clicked. Recall that when buttons are clicked, q.args.button_name will be True, so we check if q.args.new_todo is True.
In the new_todo() function, we display a new form containing a textbox (using ui.textbox()) and a set of buttons to add the item or return to to-do list (a ui.buttons() helps us display buttons side-by-side).
$HOME/wave-apps/todo.py
from typing import List
from h2o_wave import Q, main, app, ui
_id =0
# A simple class that represents a to-do item.
classTodoItem:
def__init__(self, text):
global _id
_id +=1
self.id=f'todo_{_id}'
self.text = text
self.done =False
@app('/todo')
asyncdefserve(q: Q):
if q.args.new_todo:# Display an input form.
new_todo(q)
else:# Show all items.
show_todos(q)
await q.page.save()
defshow_todos(q: Q):
# Get items for this user.
todos: List[TodoItem]= q.user.todos
# Create a sample list if we don't have any.
if todos isNone:
q.user.todos = todos =[TodoItem('Do this'), TodoItem('Do that'), TodoItem('Do something else')]
# If the user checked/unchecked an item, update our list.
for todo in todos:
if todo.idin q.args:
todo.done = q.args[todo.id]
# Create done/not-done checkboxes.
done =[ui.checkbox(name=todo.id, label=todo.text, value=True, trigger=True)for todo in todos if todo.done]
not_done =[ui.checkbox(name=todo.id, label=todo.text, trigger=True)for todo in todos ifnot todo.done]
Finally, we handle the add_todo button-click, redirecting serve() to a new add_todo() function, which simply inserts a the new to-do item into our user-level todo list and calls show_todos() to redraw the to-do list.
note
In this example, for clarity, we named the both the buttons and their corresponding functions new_todo and add_todo, but this is not necessary.
$HOME/wave-apps/todo.py
from typing import List
from h2o_wave import Q, main, app, ui
_id =0
# A simple class that represents a to-do item.
classTodoItem:
def__init__(self, text):
global _id
_id +=1
self.id=f'todo_{_id}'
self.text = text
self.done =False
@app('/todo')
asyncdefserve(q: Q):
if q.args.new_todo:# Display an input form.
new_todo(q)
elif q.args.add_todo:# Add an item.
add_todo(q)
else:# Show all items.
show_todos(q)
await q.page.save()
defshow_todos(q: Q):
# Get items for this user.
todos: List[TodoItem]= q.user.todos
# Create a sample list if we don't have any.
if todos isNone:
q.user.todos = todos =[TodoItem('Do this'), TodoItem('Do that'), TodoItem('Do something else')]
# If the user checked/unchecked an item, update our list.
for todo in todos:
if todo.idin q.args:
todo.done = q.args[todo.id]
# Create done/not-done checkboxes.
done =[ui.checkbox(name=todo.id, label=todo.text, value=True, trigger=True)for todo in todos if todo.done]
not_done =[ui.checkbox(name=todo.id, label=todo.text, trigger=True)for todo in todos ifnot todo.done]