Components & Embedding¶
Note
This page covers tesserae.instantiate -- embedding a whole,
independently-stateful *_View.yaml + *_ViewModel.py pair. For
reusing one of Tesserae's built-in MD3 components
(a button, a card, a checkbox, ...) declaratively inside a
*_View.yaml, see
Declarative Component Fragments and
Components instead.
tesserae.instantiate(parent, path, viewmodel_cls, into, *args,
**kwargs) (see the Python API) embeds another view's own YAML as a real, independent
Component with its own ViewModel, built by Tesserae in the host's
window, with the same *_View.yaml/*_ViewModel.py naming check
App.load makes.
from tesserae import instantiate
component, viewmodel = instantiate(view, "Card_View.yaml", CardViewModel, container)
# in a project, a name is enough: Views/Card_View.yaml, and CardViewModel found by the view's name
component, viewmodel = instantiate(view, "Card", into=container)
parentis aViewor anotherComponent-- they nest, so a component can itself hold further nested components the same way.intois theNodeto embed under (e.g.view.node("item_list")).pathis a file or a name found in the app's project;viewmodel_clsleft out is found by the view's name. A name with no app to look in is an error saying so.- Extra positional and keyword args are forwarded to
viewmodel_cls(component, *args, **kwargs): for a component that needs its own data, or a callback to notify its parent when it removes itself.
Call instantiate once per instance for multiple simultaneous
instances (a list where each row is its own independent component) --
each call is fully independent, even reusing the same path
repeatedly. Every instance gets its own NodeIds, even when widget ids
repeat across instances.
A component is built in its host's window with the host's theme and
stylesheet, so the host's styles: rules and theme roles style it too.
When the host is re-themed or re-styled, including by hot reload, the
component follows. Its own *_View.yaml is hot-reloaded too: editing it
reloads every live instance in place, including ones added while the app
runs (see Hot Reload).
In YAML: view:¶
A view: node shows another view where it is, with no Python. It is the YAML form of instantiate, for a view that is part
of the layout and not made from data:
id: root
kind: Container
style: {flex_direction: horizontal, width: 480, height: 200}
children:
- {id: sidebar, view: Sidebar_View.yaml, style: {width: 160}}
- {id: counter, view: Counter, with: {start: 5}} # a name, found in the project
- The view is a view of its own, so its ids don't clash with the host's:
Sidebar_View.yamland the host can both haveid: root. - It has its own ViewModel when there is one,
Sidebar_ViewModel.pybeside it or in the project'sViewModels/, and it needs none: a view with no ViewModel is static (a layout, a header, a panel of text). A static view can't havebindings:,handlers:ortwo_way:, and it can't be givenwith:: the error names the file and the node. with:is given to the ViewModel's constructor as keyword arguments:CounterViewModel(view, start=5).style:on the node places the embedded view in its parent, as on any node. A relative file is relative to the file that names it.- It takes the host's theme and follows it, and a view embedded in a view works too.
- From Python,
view.embedded("counter")is the embedded view (.node(...),.viewmodel) andview.viewmodelis a view's own ViewModel. - Hot reload watches the embedded file. Editing the host keeps an embedded view whose
view:andwith:didn't change, with its state, and builds a new one when they did.
Routed views¶
Give a view: node a route: and the view is a screen of the app, not just a part of the layout. This is for the
window view (the kind: Window root), where the screens are:
id: root
kind: Window
style: {width: 900, height: 600}
children:
- id: nav
component: NavigationRailScreens
with:
items:
- {label: Tasks, icon: home, screen: Main}
- {label: Settings, icon: settings, screen: Settings}
- id: screens
kind: Container
style: {flex: fill}
children:
- {id: main, view: Main_View.yaml, route: ""}
- {id: settings, view: Settings_View.yaml, route: settings}
- The screen's name is the view's file name without
_View.yaml(Settings):app.show("Settings"),app.navigate("Settings"),app.navigate_to("settings")andapp.screen("Settings")all work, and the view keeps its ViewModel and its state while another screen shows (it is hidden, out of the layout, not rebuilt). - Nothing shows until a route is current: call
app.navigate_to("")(orapp.show(...)) after loading the window. handlers: {on_click: navigate.Settings}on any node goes to a screen;navigate.backandnavigate.forwardmove in the history.app.current_screenis a value a binding can read:{{ app.current_screen.get() == 'Settings' }}.NavigationRailScreensis a navigation rail whose destinations navigate and are filled while their screen is current.- A
route:is an error outside the window view; a view can be routed once; editing the window view while the app runs adds or removes screens.
Tearing a component down¶
component.remove()
Removes the instance, unsubscribing its own Signals first, so a later
write to a Signal it read from can't reach a NodeId that no longer
exists.
An example¶
# TodoItem_View.yaml
id: root
kind: Container
style: {flex_direction: horizontal, width: 260, height: 36, gap: 8}
children:
- id: label
kind: Text
text: {content: "", font_family: Roboto, font_size: 16}
style: {width: 180, height: 32, foreground: "#FFFFFF"}
bindings: {text: "{{ label.get() }}"}
- id: remove_button
kind: Rect
style: {width: 60, height: 32, background: "#B3261E", corner_radius: 4}
handlers: {on_click: "remove_self"}
# TodoItem_ViewModel.py
from tesserae import Signal, ViewModel
class TodoItemViewModel(ViewModel):
def __init__(self, component, item_id, items_signal):
self.label = Signal(item_id)
self._item_id = item_id
self._items_signal = items_signal
super().__init__(component)
def remove_self(self):
self._items_signal.update(lambda items: [i for i in items if i != self._item_id])
component, vm = instantiate(
parent_view, "TodoItem_View.yaml", TodoItemViewModel, container,
item_id, items_signal, # forwarded to TodoItemViewModel(component, ...)
)
For a dynamic list, with items added and removed over time and driven by
one Signal, see Repeater, which does this bookkeeping
for you.