Py.Cafe

kecnry/

todo

Dropdown component with user API

DocsPricing
  • app.py
  • requirements.txt
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""# 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.


```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))

# now text is a reactive variable that is always in sync with todo_item.text
text = Ref(todo_item.fields.text)


# 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"
```

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")
```

"""

from typing import Callable

import reacton.ipyvuetify as v

import solara
from solara.lab.toestand import Ref


class TodoItem:
    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]):
    """Takes a reactive todo item and allows editing it. Will not modify the original item until 'save' is clicked."""
    copy = TodoItem(todo_item.text, todo_item.done)

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

    with solara.Card("Edit", margin=0):
        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)
            solara.Button("Close", icon_name="mdi-window-close", on_click=on_close, outlined=True, text=True)
            solara.Button("Delete", icon_name="mdi-delete", on_click=on_delete, outlined=True, text=True)


@solara.component
def TodoListItem(todo_item: solara.Reactive[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))
        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)
                    set_edit(False)

                TodoEdit(todo_item, on_delete=on_delete_in_edit, on_close=lambda: set_edit(False))


@solara.component
def TodoNew(on_new: Callable[[TodoItem], None]):
    """Component that managed entering new todo items"""
    new_text, set_new_text = solara.use_state("")
    text_field = v.TextField(v_model=new_text, on_v_model=set_new_text, label="Enter a new todo item")

    def create_new_item(*ignore_args):
        if not new_text:
            return
        new_item = TodoItem(text=new_text, done=False)
        on_new(new_item)
        # reset text
        set_new_text("")

    v.use_event(text_field, "keydown.enter", create_new_item)
    return text_field


initial_items = [
    TodoItem("Learn Solara", done=True),
    TodoItem("Write cool apps", done=False),
    TodoItem("Relax", done=False),
]


# We store out 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
# to do the Todo items.
# This means all above components are reusable, and can be used in other
# places, while the components below use 'application'/'global' state.
# They are not suited for reuse.


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

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

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


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

    if count != count_done:
        with solara.Row():
            percent = count_done / count * 100
            solara.ProgressLinear(value=percent)
        with solara.Row():
            solara.Text(f"Remaining: {count - count_done}")
            solara.Text(f"Completed: {count_done}")
    else:
        solara.Success("All done, awesome!", dense=True)


state = State(initial_items)


@solara.component
def Page():
    with solara.Card("Todo list", style="min-width: 500px"):
        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")