Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: todo example to use a class for state #667

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 38 additions & 83 deletions solara/website/pages/documentation/examples/utilities/todo.py
Original file line number Diff line number Diff line change
@@ -1,91 +1,46 @@
"""# Todo application

Demonstrates the use of reactive variables in combinations with dataclasses.

With solara we can get a type safe view onto a field in a dataclass, pydantic model, or
attr object.

This is using the experimental `solara.lab.Ref` class, which is not yet part of the
official API.

Demonstrates the use of reactive variables in an externally defined state class.

```python
import dataclasses
import solara
from solara.lab import Ref

@dataclasses.dataclass(frozen=True)
class TodoItem:
text: str
done: bool

todo_item = solara.reactive(TodoItem("Buy milk", False))
def __init__(self, text='', done=False):
self.text = solara.reactive(text)
self.done = solara.reactive(done)

# now text is a reactive variable that is always in sync with todo_item.text
text = Ref(todo_item.fields.text)
todo_item = TodoItem("Buy milk", False)


# we can now modify the reactive text variable
# and see its value reflect in the todo_item
text.value = "Buy skimmed milk"
assert todo_item.value.text == "Buy skimmed milk"

# Or, the other way around
todo_item.value = TodoItem("Buy whole milk", False)
assert text.value == "Buy whole milk"
todo_items = solara.reactive([TodoItem("Buy milk", False),
TodoItem("Buy whole milk", False)])
```

Apart from dataclasses, pydantic models etc, we also supports dictionaries and lists.

```python
todo_items = solara.reactive([TodoItem("Buy milk", False), TodoItem("Buy eggs", False)])
todo_item_eggs = Ref(todo_items.fields[1])
todo_item_eggs.value = TodoItem("Buy eggs", True)
assert todo_items.value[1].done == True

# However, if a list becomes shorter, and the valid index is now out of range, the
# reactive variables will act as if it is "not connected", and will not trigger
# updates anymore. Accessing the value will raise an IndexError.

todo_items.value = [TodoItem("Buy milk", False)]
# anyone listening to todo_item_eggs will *not* be notified.
try:
value = todo_item_eggs.value
except IndexError:
print("this is expected")
else:
raise AssertionError("Expected an IndexError")
```

"""

import dataclasses
from typing import Callable

import reacton.ipyvuetify as v

import solara
from solara.lab.toestand import Ref


# our model for a todo item, immutable/frozen avoids common bugs
@dataclasses.dataclass(frozen=True)
class TodoItem:
text: str
done: bool
def __init__(self, text='', done=False):
self.text = solara.reactive(text)
self.done = solara.reactive(done)


@solara.component
def TodoEdit(todo_item: solara.Reactive[TodoItem], on_delete: Callable[[], None], on_close: Callable[[], None]):
def TodoEdit(todo_item: TodoItem, on_delete: Callable[[], None], on_close: Callable[[], None]):
"""Takes a reactive todo item and allows editing it. Will not modify the original item until 'save' is clicked."""
copy = solara.use_reactive(todo_item.value)
copy = TodoItem(todo_item.text, todo_item.done)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears to me that this does not create a "deep" copy, but uses the same reactive objects in the todo_item, so that any change to the "copy" will actually modify also the todo_item.


def save():
todo_item.value = copy.value
todo_item.text.value = copy.text.value
on_close()

with solara.Card("Edit", margin=0):
solara.InputText(label="", value=Ref(copy.fields.text))
solara.InputText(label="", value=copy.text)
with solara.CardActions():
v.Spacer()
solara.Button("Save", icon_name="mdi-content-save", on_click=save, outlined=True, text=True)
Expand All @@ -94,23 +49,23 @@ def save():


@solara.component
def TodoListItem(todo_item: solara.Reactive[TodoItem], on_delete: Callable[[TodoItem], None]):
def TodoListItem(todo_item: TodoItem, on_delete: Callable[[TodoItem], None]):
"""Displays a single todo item, modifications are done 'in place'.

For demonstration purposes, we allow editing the item in a dialog as well.
This will not modify the original item until 'save' is clicked.
"""
edit, set_edit = solara.use_state(False)
with v.ListItem():
solara.Button(icon_name="mdi-delete", icon=True, on_click=lambda: on_delete(todo_item.value))
solara.Checkbox(value=Ref(todo_item.fields.done)) # , color="success")
solara.InputText(label="", value=Ref(todo_item.fields.text))
solara.Button(icon_name="mdi-delete", icon=True, on_click=lambda: on_delete(todo_item))
solara.Checkbox(value=todo_item.done) # , color="success")
solara.InputText(label="", value=todo_item.text)
solara.Button(icon_name="mdi-pencil", icon=True, on_click=lambda: set_edit(True))
with v.Dialog(v_model=edit, persistent=True, max_width="500px", on_v_model=set_edit):
if edit: # 'reset' the component state on open/close

def on_delete_in_edit():
on_delete(todo_item.value)
on_delete(todo_item)
set_edit(False)

TodoEdit(todo_item, on_delete=on_delete_in_edit, on_close=lambda: set_edit(False))
Expand Down Expand Up @@ -141,7 +96,7 @@ def create_new_item(*ignore_args):
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment here would be helpful explaining why this is globally declared and not created as a class attribute inside State or inside State.init().



# We store out reactive state, and our logic in a class for organization
# We store our reactive state, and our logic in a class for organization
# purposes, but this is not required.
# Note that all the above components do not refer to this class, but only
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these in-line comments may need updating?

# to do the Todo items.
Comment on lines 101 to 102
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use further justification and explanation.

Expand All @@ -151,25 +106,23 @@ def create_new_item(*ignore_args):

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is an example of how to manage state, it would be even more valuable to show how the todos can be reused. Now the Page component is effectively a Todo component, but it's not reusable as State can only manage a single list of Todos.


class State:
todos = solara.reactive(initial_items)
def __init__(self, initial_items):
self.todos = solara.reactive(initial_items)

@staticmethod
def on_new(item: TodoItem):
State.todos.value = [item] + State.todos.value
def on_new(self, item: TodoItem):
self.todos.value = [item] + self.todos.value

@staticmethod
def on_delete(item: TodoItem):
new_items = list(State.todos.value)
def on_delete(self, item: TodoItem):
new_items = list(self.todos.value)
new_items.remove(item)
State.todos.value = new_items
self.todos.value = new_items


@solara.component
def TodoStatus():
def TodoStatus(items):
"""Status of our todo list"""
items = State.todos.value
count = len(items)
items_done = [item for item in items if item.done]
items_done = [item for item in items if item.done.value]
count_done = len(items_done)

if count != count_done:
Expand All @@ -183,14 +136,16 @@ def TodoStatus():
solara.Success("All done, awesome!", dense=True)


state = State(initial_items)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice with a comment explaining how the state object can still work for multiple clients (virtual kernels), despite it not being declared a solara.reactive.



@solara.component
def Page():
with solara.Card("Todo list", style="min-width: 500px"):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider doing a separate Todo component card and show how it can be reused. Again, this may be another example (a dashboard of TODO lists for different people/tasks where you can add or remove entire TODOs?)

TodoNew(on_new=State.on_new)
if State.todos.value:
TodoStatus()
for index, item in enumerate(State.todos.value):
todo_item = Ref(State.todos.fields[index])
TodoListItem(todo_item, on_delete=State.on_delete)
TodoNew(on_new=state.on_new)
if state.todos.value:
TodoStatus(state.todos.value)
for item in state.todos.value:
TodoListItem(item, on_delete=state.on_delete)
else:
solara.Info("No todo items, enter some text above, and hit enter")
solara.Info("No todo items, enter some text above, and hit enter")
Loading