Skip to content

Commit dc3a6bc

Browse files
committed
Restructure the example to describe creating a form
1 parent 419308f commit dc3a6bc

File tree

9 files changed

+154
-122
lines changed

9 files changed

+154
-122
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ We use the MVVM framework for NOVA applications. With this in mind, each example
1818
1. view.py - Sets up a Trame GUI for the example.
1919
2. model.py - Sets up a Pydantic model and any business logic needed by the application.
2020
3. view_model.py - Sets up a view model that binds the model and view.
21-
4. mvvm_factory.py - Entrypoint for creating the model and view model.
22-
5. main.py - Runs the Trame GUI.
21+
4. main.py - Entrypoint for the Trame GUI.
2322

2423
## List of Examples
2524

26-
1. [Binding Trame parameters](examples/trame_bindings)
25+
1. [Creating a form for a Pydantic model](examples/pydantic_form)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
"""Runs the Trame bindings example."""
1+
"""Runs the Pydantic form example."""
22

3-
from examples.trame_bindings.view import App
3+
from examples.pydantic_form.view import App
44

55

66
def main() -> None:

examples/pydantic_form/model.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Model implementation for Pydantic form example."""
2+
3+
from pydantic import BaseModel, Field, computed_field, field_validator
4+
5+
6+
class FormData(BaseModel):
7+
"""Pydantic model."""
8+
9+
user_name: str = Field(default="", title="User Name")
10+
domain_name: str = Field(default="", title="Domain Name")
11+
submitted: bool = Field(default=False)
12+
13+
@computed_field
14+
@property
15+
def submit_disabled(self) -> bool:
16+
if "@" not in self.email:
17+
return True
18+
return False
19+
20+
@computed_field
21+
@property
22+
def email(self) -> str:
23+
if not self.user_name or not self.domain_name:
24+
return ""
25+
return f"{self.user_name}@{self.domain_name}"
26+
27+
@field_validator("domain_name", mode="after")
28+
@classmethod
29+
def validate_domain_name(cls, value: str) -> str:
30+
if value and "." not in value:
31+
raise ValueError("Invalid domain name.")
32+
33+
return value
34+
35+
36+
class Model:
37+
"""Model implementation for Pydantic form example."""
38+
39+
def __init__(self) -> None:
40+
self.form = FormData()
41+
42+
def submit(self) -> None:
43+
self.form.submitted = True
44+
45+
def unsubmit(self) -> None:
46+
self.form.submitted = False

examples/pydantic_form/view.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""View for Pydantic form example."""
2+
3+
from nova.mvvm.trame_binding import TrameBinding
4+
from nova.trame import ThemedApp
5+
from nova.trame.view.components import InputField
6+
from nova.trame.view.layouts import GridLayout
7+
from trame.widgets import html
8+
from trame.widgets import vuetify3 as vuetify
9+
10+
from .model import Model
11+
from .view_model import ViewModel
12+
13+
14+
class App(ThemedApp):
15+
"""View for Pydantic form example."""
16+
17+
def __init__(self) -> None:
18+
super().__init__()
19+
20+
self.create_vm()
21+
# If you forget to call connect, then the application will crash when you attempt to update the view.
22+
self.view_model.form_bind.connect("form")
23+
# Generally, we want to initialize the view state before creating the UI for ease of use. If initialization
24+
# is expensive, then you can defer it. In this case, you must handle the view state potentially being
25+
# uninitialized in the UI via v_if statements.
26+
self.view_model.update_view()
27+
28+
self.create_ui()
29+
30+
def create_ui(self) -> None:
31+
with super().create_ui() as layout:
32+
with layout.content:
33+
with vuetify.VCard(classes="mx-auto my-4", max_width=600):
34+
with GridLayout(columns=2, gap="0.5em"):
35+
# The GUI must bind the Pydantic model fields to elements.
36+
# Most bindings use the tuple syntax: (parameter_name, default_value).
37+
# If you don't have a default value, then you can use: (parameter_name,).
38+
InputField(v_model=("form.user_name",))
39+
40+
# v_model allows you to just pass the parameter_name as a string.
41+
# This typically does not work when binding other parameters.
42+
InputField(v_model="form.domain_name")
43+
44+
# Try just passing a string to model_value, and see what happens here!
45+
InputField(
46+
model_value=("form.email",), column_span=2, label="Computed Email Address", readonly=True
47+
)
48+
49+
# If you need to trigger some Python code on an event, then you can call view model functions
50+
# directly.
51+
vuetify.VBtn(
52+
"Submit",
53+
column_span=2,
54+
disabled=("form.submit_disabled",),
55+
click=self.view_model.submit,
56+
)
57+
58+
# This is less common, but you can use handlebars expressions to display the form data inside of
59+
# a string.
60+
html.Span("You've submitted the following email address: {{ form.email }}", v_if="form.submitted")
61+
62+
def create_vm(self) -> None:
63+
binding = TrameBinding(self.state)
64+
65+
model = Model()
66+
self.view_model = ViewModel(model, binding)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""View model implementation for Pydantic form example."""
2+
3+
from typing import Any
4+
5+
from nova.mvvm.interface import BindingInterface
6+
7+
from .model import Model
8+
9+
10+
class ViewModel:
11+
"""View model implementation for Pydantic form example."""
12+
13+
def __init__(self, model: Model, binding: BindingInterface) -> None:
14+
self.model = model
15+
# self.on_update is called any time the view updates the binding.
16+
self.form_bind = binding.new_bind(self.model.form, callback_after_update=self.on_update)
17+
18+
def on_update(self, results: Any) -> None:
19+
print("The user has modified the form:", results)
20+
21+
# If you need to detect that a specific field in the Pydantic model was updated, you can do so with:
22+
updated = results.get("updated", [])
23+
for update in updated:
24+
match update:
25+
case "user_name" | "domain_name":
26+
self.model.unsubmit()
27+
# In these cases, our computed field will update, and we need to communicate that update to the
28+
# front-end. Try commenting this out and see what happens!
29+
self.update_view()
30+
31+
def submit(self) -> None:
32+
self.model.submit()
33+
# Now, the Pydantic model has the new value, but the view doesn't until we call update_view.
34+
self.update_view()
35+
36+
def update_view(self) -> None:
37+
# This will fail if you haven't called connect on the binding!
38+
self.form_bind.update_in_view(self.model.form)

examples/trame_bindings/model.py

Lines changed: 0 additions & 23 deletions
This file was deleted.

examples/trame_bindings/mvvm_factory.py

Lines changed: 0 additions & 13 deletions
This file was deleted.

examples/trame_bindings/view.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

examples/trame_bindings/view_model.py

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)