-
Notifications
You must be signed in to change notification settings - Fork 140
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
|
||
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) | ||
|
@@ -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)) | ||
|
@@ -141,7 +96,7 @@ def create_new_item(*ignore_args): | |
] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could use further justification and explanation. |
||
|
@@ -151,25 +106,23 @@ def create_new_item(*ignore_args): | |
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
@@ -183,14 +136,16 @@ def TodoStatus(): | |
solara.Success("All done, awesome!", dense=True) | ||
|
||
|
||
state = State(initial_items) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") |
There was a problem hiding this comment.
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.