patera-desktop-ui 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- patera_desktop_ui-0.2.0/LICENSE +21 -0
- patera_desktop_ui-0.2.0/PKG-INFO +457 -0
- patera_desktop_ui-0.2.0/README.md +444 -0
- patera_desktop_ui-0.2.0/docs/runtime.md +241 -0
- patera_desktop_ui-0.2.0/pyproject.toml +20 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/__init__.py +74 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/application.py +640 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/assets/adapter.js +180 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/assets/bootstrap.py +21 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/browser.py +941 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/build.py +291 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/cache.py +97 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/components.py +410 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/controller.py +72 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/discovery.py +45 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/exceptions.py +46 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/lifecycle.py +130 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/lists.py +214 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/metadata.py +55 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/properties.py +84 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/py.typed +0 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/routing.py +166 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/server.py +424 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/LICENSE-MPL-2.0 +373 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/NOTICE.md +25 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/README.md +54 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/__init__.py +5 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/__init__.pyi +18879 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/_ecma.pyi +214 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/updates.py +106 -0
- patera_desktop_ui-0.2.0/src/patera/desktop/ui/view.py +335 -0
- patera_desktop_ui-0.2.0/tools/generate_browser_types.py +130 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marko Šterk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: patera-desktop-ui
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python browser UI for Patera desktop applications
|
|
5
|
+
License-Expression: MIT AND MPL-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: src/patera/desktop/ui/types/LICENSE-MPL-2.0
|
|
8
|
+
Requires-Dist: patera-desktop>=0.11.2
|
|
9
|
+
Requires-Dist: jinja2>=3.1.6,<4
|
|
10
|
+
Requires-Dist: markupsafe>=3.0,<4
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# patera-desktop-ui
|
|
15
|
+
|
|
16
|
+
Experimental Python UI for `patera-desktop`, using a locally bundled PyScript /
|
|
17
|
+
Pyodide interpreter. Install with `pip install patera-desktop-ui`.
|
|
18
|
+
Requires `patera-desktop>=0.11.2` for namespace and packaging integration.
|
|
19
|
+
|
|
20
|
+
The backend stays native Python. Browser controllers, components and models run
|
|
21
|
+
in a separate interpreter and communicate with Patera HTTP endpoints. Native
|
|
22
|
+
database sessions, files and services do not cross that boundary.
|
|
23
|
+
|
|
24
|
+
## Application structure
|
|
25
|
+
|
|
26
|
+
Keep browser sources outside the backend package, so backend discovery and
|
|
27
|
+
PyInstaller do not import browser modules:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
project/
|
|
31
|
+
app/__init__.py
|
|
32
|
+
app/configs.py
|
|
33
|
+
app/api/notes_api.py
|
|
34
|
+
frontend/
|
|
35
|
+
frontend/__init__.py
|
|
36
|
+
templates/note.html
|
|
37
|
+
ui-runtime/ complete local runtime, including runtime.json
|
|
38
|
+
build/ui/ generated, do not commit
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from patera import Patera, app
|
|
43
|
+
from patera.desktop import Desktop
|
|
44
|
+
from patera.desktop.ui import DesktopUI
|
|
45
|
+
from .configs import Config
|
|
46
|
+
|
|
47
|
+
@app(__name__, configs=Config)
|
|
48
|
+
class App(Patera[Config]):
|
|
49
|
+
app_extensions = [Desktop, DesktopUI]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from patera import BaseConfig
|
|
54
|
+
from patera.desktop import DesktopConfig
|
|
55
|
+
from patera.desktop.ui import DesktopUIConfig
|
|
56
|
+
|
|
57
|
+
class Config(BaseConfig):
|
|
58
|
+
DESKTOP: DesktopConfig = DesktopConfig(INDEX_PAGE="/app/")
|
|
59
|
+
DESKTOP_UI: DesktopUIConfig = DesktopUIConfig(TITLE="Notes")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Paths in `DesktopUIConfig` are relative to the backend application's root:
|
|
63
|
+
|
|
64
|
+
| Setting | Default | Meaning |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `SOURCE` | `../frontend` | Dedicated browser source root |
|
|
67
|
+
| `ENTRYPOINT` | `frontend:app` | An `Application` instance; never imported during build |
|
|
68
|
+
| `RUNTIME` | `../ui-runtime` | Complete offline runtime and hash manifest |
|
|
69
|
+
| `OUTPUT` | `../build/ui` | Build artifacts, outside source/runtime folders |
|
|
70
|
+
| `MOUNT` | `/app` | Shell and page URLs |
|
|
71
|
+
| `ASSETS` | `/__ui__` | Immutable asset URLs |
|
|
72
|
+
| `TITLE` | `Patera` | Initial window document title |
|
|
73
|
+
| `PUBLIC` | `{}` | Explicitly public browser configuration |
|
|
74
|
+
| `API_PATHS` | `['/api']` | API prefixes requiring the UI session token |
|
|
75
|
+
|
|
76
|
+
`URL_PREFIX` applies to shell, assets and API prefixes. Mounts must be disjoint,
|
|
77
|
+
non-root paths. Ordinary Patera endpoints outside these prefixes are unchanged.
|
|
78
|
+
|
|
79
|
+
## Pages
|
|
80
|
+
|
|
81
|
+
`frontend/frontend/__init__.py`:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from patera.desktop.ui import Application, Controller, PageContext, View, path, page
|
|
85
|
+
|
|
86
|
+
@path("/notes")
|
|
87
|
+
class Notes(Controller):
|
|
88
|
+
@page("/<int:note_id>")
|
|
89
|
+
async def detail(self, ctx: PageContext, note_id: int) -> View:
|
|
90
|
+
note = await ctx.api.get_json(f"/api/notes/{note_id}")
|
|
91
|
+
return View("templates/note.html", note=note, title=note["title"])
|
|
92
|
+
|
|
93
|
+
app = Application(controllers=[Notes])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Templates use Jinja2 with autoescaping and strict undefined values. Inheritance,
|
|
97
|
+
includes, macros, expressions, loops, filters and globals are supported.
|
|
98
|
+
Use `View(markup=html("<h1>{{ title }}</h1>", title=value))` for inline markup.
|
|
99
|
+
Use `trusted_html()` only for application-authored markup. Interpolation belongs
|
|
100
|
+
in HTML text or quoted ordinary attributes, never scripts, styles, event-handler
|
|
101
|
+
attributes or unvalidated URLs. Jinja2 and MarkupSafe's portable Python sources
|
|
102
|
+
and licenses are bundled automatically; no browser-side installation is needed.
|
|
103
|
+
|
|
104
|
+
Component pages, persistent layouts, reactive fragments, lifecycle hooks,
|
|
105
|
+
authorization, lazy dependencies and server-rendered components are described
|
|
106
|
+
in the [runtime guide](docs/runtime.md). Controller pages remain supported and
|
|
107
|
+
can also use layouts.
|
|
108
|
+
|
|
109
|
+
Routes support `string` (default), nonnegative `int`, and `path` converters.
|
|
110
|
+
`app.url_for("Notes.detail", note_id=3, query={"tab": "details"})` includes the
|
|
111
|
+
mount after startup. Multiple `@page` decorators may alias one method; the first
|
|
112
|
+
registered route is used for URL generation. Distinct handlers cannot share a
|
|
113
|
+
route or endpoint. Trailing slashes are ignored by default.
|
|
114
|
+
|
|
115
|
+
`ctx.query` maps keys to lists. `ctx.spawn(coroutine)` tracks a task for the page;
|
|
116
|
+
navigation cancels it. `ctx.on_cleanup(callback)` registers resource cleanup.
|
|
117
|
+
Navigation cancels superseded work and checks a generation number before
|
|
118
|
+
committing a result. Controllers live for the application lifetime; page-local
|
|
119
|
+
state belongs in the context or components.
|
|
120
|
+
|
|
121
|
+
`ExceptionHandler` with `@handles(ValueError)` methods `(self, ctx, error)` handles
|
|
122
|
+
page and event errors; the most specific exception type wins. Return a `View`
|
|
123
|
+
for an error page, or `None` from an event handler to retain the current page.
|
|
124
|
+
Unhandled failures appear in an alert and the browser console.
|
|
125
|
+
|
|
126
|
+
## Components
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from __future__ import annotations
|
|
130
|
+
from typing import TYPE_CHECKING
|
|
131
|
+
|
|
132
|
+
from patera.desktop.ui import Component, State, event, html, tag
|
|
133
|
+
|
|
134
|
+
if TYPE_CHECKING:
|
|
135
|
+
from patera.desktop.ui.types import MouseEvent
|
|
136
|
+
|
|
137
|
+
@tag("note-counter")
|
|
138
|
+
class Counter(Component):
|
|
139
|
+
|
|
140
|
+
def __init__(self, app, element):
|
|
141
|
+
super().__init__(app, element)
|
|
142
|
+
self.state = State(count=0)
|
|
143
|
+
|
|
144
|
+
def render(self):
|
|
145
|
+
return html('<button data-ui-on-click="increment">Count: '
|
|
146
|
+
'<span data-ui-text="count"></span></button>')
|
|
147
|
+
|
|
148
|
+
@event("click")
|
|
149
|
+
def increment(self, event: MouseEvent) -> None:
|
|
150
|
+
self.state["count"] += 1
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Register with `Application(..., components=[Counter])`; use
|
|
154
|
+
`<note-counter></note-counter>` in a view. `mounted()` and `unmounted()` may be
|
|
155
|
+
async. Event names are explicit; `data-ui-on-input="change"` requires
|
|
156
|
+
`@event("input")`. `data-ui-value` updates a control's value; use an input handler
|
|
157
|
+
to write back to state. Bindings update nodes without replacing the component,
|
|
158
|
+
preserving input focus. Assign to state to notify subscribers; in-place mutation
|
|
159
|
+
of nested values does not notify. `State.subscribe()` returns an unsubscribe
|
|
160
|
+
function. The runtime owns and releases DOM listener proxies.
|
|
161
|
+
|
|
162
|
+
## Offline runtime and builds
|
|
163
|
+
|
|
164
|
+
Provision a **complete** pinned PyScript/Pyodide offline distribution in
|
|
165
|
+
`ui-runtime`. Follow the official [offline guide](https://docs.pyscript.net/2026.7.3/user-guide/offline/).
|
|
166
|
+
Include PyScript's supporting modules, the interpreter's `.mjs`, `.js`, `.wasm`,
|
|
167
|
+
`python_stdlib.zip`, `pyodide-lock.json`, and packages required by that runtime.
|
|
168
|
+
Preserve its directory layout and licenses. Nothing downloads at application
|
|
169
|
+
startup or as a side effect of extension registration.
|
|
170
|
+
|
|
171
|
+
Record the actual entry paths and versions (these arguments describe your
|
|
172
|
+
chosen runtime; do not substitute a different version's files):
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
patera cli ui:lock-runtime pyscript/core.js pyscript/pyodide/pyodide.mjs 2026.2.1 0.28.0.dev0 3.13
|
|
176
|
+
patera cli ui:build
|
|
177
|
+
patera cli desktop:run
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The tested PyScript **2026.2.1 offline archive** uses entry paths
|
|
181
|
+
`pyscript/core.js` and `pyscript/pyodide/pyodide.mjs`, with Pyodide
|
|
182
|
+
`0.28.0.dev0` and Python `3.13`. For that archive, extract its `offline/`
|
|
183
|
+
contents into `ui-runtime/` and use the command above.
|
|
184
|
+
|
|
185
|
+
Other releases can require a newer host Python for syntax validation. The
|
|
186
|
+
runtime is an explicit build input rather than a floating CDN dependency.
|
|
187
|
+
|
|
188
|
+
Commit `runtime.json` or retain it with your build inputs. Every build checks
|
|
189
|
+
the recorded SHA-256 hashes. The builder validates Python syntax against the
|
|
190
|
+
recorded browser Python version; build with a host Python at least that new.
|
|
191
|
+
This does not validate third-party package compatibility or ensure an incomplete
|
|
192
|
+
runtime has all of its transitive assets: test a fresh browser profile offline.
|
|
193
|
+
|
|
194
|
+
The browser archive contains minimal `patera`/`patera.desktop` parent packages,
|
|
195
|
+
portable UI modules, and only allowlisted frontend files. It never installs the
|
|
196
|
+
native Patera wheel in Pyodide. Thus public UI imports stay the same without
|
|
197
|
+
changing Patera core imports. Frontend packages need `__init__.py`; their parent
|
|
198
|
+
initializers must be browser-safe. Keep shared models in a separate lightweight
|
|
199
|
+
package rather than importing through the backend application's initializer.
|
|
200
|
+
|
|
201
|
+
The builder excludes hidden files, caches, logs, databases, bytecode and unknown
|
|
202
|
+
file extensions. Frontend Python, HTML, JS, CSS, fonts and images are public.
|
|
203
|
+
Do not put secrets in this source root. `.html` files become named templates;
|
|
204
|
+
other non-Python assets are served below the build's `public/` URL.
|
|
205
|
+
|
|
206
|
+
Builds are content-addressed and switch `current.json` atomically. Re-run
|
|
207
|
+
`ui:build` after edits, or run `patera cli ui:watch` in another terminal.
|
|
208
|
+
For a single-command desktop workflow, use `patera cli ui:run`: it builds first,
|
|
209
|
+
watches in a background thread, and runs the desktop window on the main thread.
|
|
210
|
+
Closing the window or Ctrl+C stops the watcher; any active build finishes before
|
|
211
|
+
exit. The desktop extension handles backend shutdown. Initial build failures
|
|
212
|
+
prevent launch; subsequent failures retain the working build. `ui:run prod`
|
|
213
|
+
builds and launches without watching. Both modes require the Desktop extension.
|
|
214
|
+
Development windows (`DEBUG=True`) poll the build version and reload after a
|
|
215
|
+
successful build. Failed builds preserve the working version. Previous builds
|
|
216
|
+
remain available to already-open windows. Frozen applications never poll or
|
|
217
|
+
rebuild. Third-party dependency installation is not included in this version.
|
|
218
|
+
|
|
219
|
+
Use `app.config` for `PUBLIC` settings and `app.asset_url("styles.css")` for
|
|
220
|
+
bundled non-template resources after startup. Build with the deployment's
|
|
221
|
+
`URL_PREFIX`; the generated interpreter configuration contains that prefix.
|
|
222
|
+
|
|
223
|
+
## Desktop packaging
|
|
224
|
+
|
|
225
|
+
New `desktop:spec` files include extension asset preparation. Both
|
|
226
|
+
`desktop:package` and direct `pyinstaller desktop.spec` builds prepare the UI.
|
|
227
|
+
Existing specs need the `DESKTOP_ASSETS` setting and `prepare_desktop_assets`
|
|
228
|
+
call from a newly generated spec. The package command rejects old specs when
|
|
229
|
+
the UI extension is registered. Custom specs must retain this call.
|
|
230
|
+
|
|
231
|
+
Artifacts are installed under `patera_ui` in PyInstaller's resource directory.
|
|
232
|
+
Frozen applications read those artifacts, never compile sources or write to
|
|
233
|
+
the installation directory. Runtime configuration is separate from the bundle;
|
|
234
|
+
the API session token is generated per backend process and never persisted.
|
|
235
|
+
|
|
236
|
+
## Local API boundary
|
|
237
|
+
|
|
238
|
+
Protected API prefixes require `X-Patera-UI-Token` on every HTTP method. The
|
|
239
|
+
built-in API client adds it, checks status codes (`APIError`), and aborts fetches
|
|
240
|
+
when cancelled. Shell/API requests require a loopback Host and reject foreign
|
|
241
|
+
Origin and cross-site fetch metadata. This works with both the development
|
|
242
|
+
subprocess and packaged server thread because the shell obtains the token from
|
|
243
|
+
the running server. Do not enable permissive CORS for these routes.
|
|
244
|
+
|
|
245
|
+
This is a browser cross-origin boundary, not authentication against other local
|
|
246
|
+
processes. Backend authorization is still required where applicable. WebSocket
|
|
247
|
+
routes and native bridges are not provided or protected by this HTTP middleware;
|
|
248
|
+
secure them independently. Keep heavy computation on the backend. The initial
|
|
249
|
+
browser adapter targets Pyodide on the main thread, not MicroPython or workers.
|
|
250
|
+
|
|
251
|
+
## Verification
|
|
252
|
+
|
|
253
|
+
`pytest tests/extensions/test_desktop_ui.py` covers portable routing, escaping,
|
|
254
|
+
cancellation, deterministic builds, runtime hashes, native-import isolation,
|
|
255
|
+
HTTP serving, API protection, frozen paths, and standalone spec execution.
|
|
256
|
+
|
|
257
|
+
The optional `tests/browser/desktop_ui.cjs` Playwright test exercises the notes
|
|
258
|
+
example in a fresh browser context with all non-local requests blocked. It
|
|
259
|
+
checks saving, escaped text, navigation, Back/Forward and deep-link refresh.
|
|
260
|
+
Install Playwright separately; set `NODE_PATH` to its node_modules folder and
|
|
261
|
+
`UI_TEST_URL` to the running example's `/app/` URL. `UI_TEST_BROWSER=msedge`
|
|
262
|
+
uses installed Edge instead of Playwright's Chromium. Run with
|
|
263
|
+
`node tests/browser/desktop_ui.cjs`. No browser automation dependency is added
|
|
264
|
+
to the framework's runtime dependencies.
|
|
265
|
+
|
|
266
|
+
### Folder discovery and type hints
|
|
267
|
+
|
|
268
|
+
Use `app = Application(__name__)` in your frontend package's `__init__.py`.
|
|
269
|
+
The application recursively imports public modules under `controllers/`,
|
|
270
|
+
`components/`, `pages/`, `layouts/`, and `exceptions/` (or `exception_handlers/`). Give each folder
|
|
271
|
+
an `__init__.py`; nested packages work too, including inside the browser ZIP.
|
|
272
|
+
Concrete subclasses defined in those modules are registered automatically.
|
|
273
|
+
Imported/re-exported classes are ignored, abstract classes are skipped, and
|
|
274
|
+
import errors are surfaced. Modules are visited in deterministic name order.
|
|
275
|
+
Only these frontend folders are scanned; backend discovery is unchanged.
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
frontend/
|
|
279
|
+
__init__.py # app = Application(__name__)
|
|
280
|
+
controllers/
|
|
281
|
+
__init__.py
|
|
282
|
+
pages.py # @path class Pages(Controller), @page methods
|
|
283
|
+
components/
|
|
284
|
+
__init__.py
|
|
285
|
+
counter.py # @tag("note-counter") class Counter(Component)
|
|
286
|
+
exceptions/
|
|
287
|
+
__init__.py
|
|
288
|
+
errors.py # class Errors(ExceptionHandler), @handles methods
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Explicit `controllers=`, `components=`, `pages=`, `layouts=`, and `exception_handlers=` lists may
|
|
292
|
+
supplement discovery; the same class is registered once. Omit the package name
|
|
293
|
+
to use only explicit registration. Use `@tag("note-counter")` on each component.
|
|
294
|
+
|
|
295
|
+
Import `PageContext` to annotate page handlers as
|
|
296
|
+
`async def index(self, ctx: PageContext) -> View`. Error handlers may receive
|
|
297
|
+
`PageContext | None` when no page is active. For event handlers, import `Event`,
|
|
298
|
+
`MouseEvent`, `KeyboardEvent`, `InputEvent`, or `SubmitEvent` from
|
|
299
|
+
`patera.desktop.ui.types` under `TYPE_CHECKING`, with postponed annotations.
|
|
300
|
+
These stubs describe the original browser event proxy; they do not wrap or
|
|
301
|
+
convert events. Narrow or cast event targets to access element-specific fields.
|
|
302
|
+
|
|
303
|
+
## Reactive update scheduling and diagnostics
|
|
304
|
+
|
|
305
|
+
Existing applications keep synchronous binding updates. Opt into frame batching
|
|
306
|
+
when constructing the frontend application:
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
app = Application(__name__, batch_updates=True, diagnostics=True)
|
|
310
|
+
|
|
311
|
+
# Inside an async component handler:
|
|
312
|
+
self.count += 1 # Python reads see the new value immediately
|
|
313
|
+
self.count += 1
|
|
314
|
+
await self.flush() # or await app.flush()
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
With batching, repeated invalidations of a binding are combined. A selected
|
|
318
|
+
parent fragment also covers its selected descendants. Normal commits run on the
|
|
319
|
+
next animation frame; explicit `flush()` commits without waiting for a frame,
|
|
320
|
+
including when the window is hidden. Updates caused during rendering are handled
|
|
321
|
+
in subsequent batches, with a 100-batch limit to surface update loops. Disposing a
|
|
322
|
+
component removes its queued work. Full `rerender()` remains an explicit async
|
|
323
|
+
operation; batching applies to reactive binding/text/value updates.
|
|
324
|
+
|
|
325
|
+
`flush()` is a DOM commit barrier for queued bindings, not a paint, network,
|
|
326
|
+
initialization, or event-task barrier. After inserting components through a
|
|
327
|
+
binding, use `await self.wait_children()` if their initialization must
|
|
328
|
+
finish before continuing. `insert_html()` and `hydrate()` already wait for
|
|
329
|
+
non-deferred children. Initial rendering resolves `init_complete` after those
|
|
330
|
+
children and `BEFORE_INIT_RESOLVE`; `AFTER_INIT` and `mounted()` run afterward.
|
|
331
|
+
Explicit `rerender()` waits for children before `AFTER_RERENDER`. Fragment updates
|
|
332
|
+
do not invoke full-render lifecycle hooks. A newer render or disposal prevents
|
|
333
|
+
obsolete asynchronous results from committing.
|
|
334
|
+
|
|
335
|
+
Fragment commits retain listeners outside the replaced subtree, hydrate only
|
|
336
|
+
inserted nodes, and skip identical output. Binding identifiers remain stable
|
|
337
|
+
when an unchanged parent recreates its nested bindings. Form submission and
|
|
338
|
+
explicit event modifiers still cancel events during synchronous DOM dispatch.
|
|
339
|
+
Ordinary list bindings still replace their rendered range. Use the opt-in list
|
|
340
|
+
blocks below for keyed reconciliation or virtualization.
|
|
341
|
+
|
|
342
|
+
Diagnostics are disabled by default. With `diagnostics=True`, inspect
|
|
343
|
+
`app.diagnostics_snapshot()` for aggregate counts and timings in milliseconds:
|
|
344
|
+
template evaluation, DOM replacement, hydration, listener addition/removal,
|
|
345
|
+
initialization, and cleanup. Initialization/cleanup timings include awaited work;
|
|
346
|
+
nested measurements overlap and must not be summed as a total. The snapshot also
|
|
347
|
+
counts registered components, component/browser tasks, listeners/proxies,
|
|
348
|
+
subscriptions, fragments, and queued components. These are framework resource
|
|
349
|
+
counts, not total Python/WebAssembly memory or arbitrary user-created tasks.
|
|
350
|
+
`app.diagnostics.reset()` resets measurements without changing live resources.
|
|
351
|
+
Snapshots contain copied numeric data, not retained component/event references.
|
|
352
|
+
|
|
353
|
+
See `tests/browser/framework_updates.cjs` for isolated framework workloads and
|
|
354
|
+
`tests/browser/BENCHMARKS.md` for running the browser checks and comparisons.
|
|
355
|
+
|
|
356
|
+
## Keyed lists
|
|
357
|
+
|
|
358
|
+
Use a Jinja `keyed` block where a `data-bind` loop would otherwise replace every
|
|
359
|
+
row. The block name is the invalidation name, just like a normal binding:
|
|
360
|
+
|
|
361
|
+
```html
|
|
362
|
+
<ul>
|
|
363
|
+
{% keyed "items" %}
|
|
364
|
+
{% for item in component.items %}
|
|
365
|
+
<li data-ui-key="{{ item.id }}">
|
|
366
|
+
<input value="{{ item.title }}">
|
|
367
|
+
<button @click="remove" :item="{{ item.id }}">Remove</button>
|
|
368
|
+
</li>
|
|
369
|
+
{% endfor %}
|
|
370
|
+
{% endkeyed %}
|
|
371
|
+
</ul>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
For example, an `items = react_factory(list)` field invalidates this block on
|
|
375
|
+
assignment. Every direct row needs a stable, unique, non-empty `data-ui-key`.
|
|
376
|
+
Keys describe item identity, so avoid array positions when items can move.
|
|
377
|
+
Missing/duplicate keys and malformed row HTML fail before DOM reconciliation.
|
|
378
|
+
Use explicitly balanced HTML; for table rows put the block inside a `<tbody>`.
|
|
379
|
+
|
|
380
|
+
Matching keys and element types retain their row nodes. The framework patches
|
|
381
|
+
changed text and declared attributes, creates new rows, removes obsolete rows,
|
|
382
|
+
and moves existing rows without remounting their Patera child components. Input
|
|
383
|
+
edits remain intact when their declared value did not change; an explicitly
|
|
384
|
+
changed value updates the field. Focus and selection survive moves of retained
|
|
385
|
+
elements. Event handlers are rebound only where their declarations changed.
|
|
386
|
+
|
|
387
|
+
A mounted child component owns its interior. Its host and local Python state
|
|
388
|
+
survive a move; changed `:argument`/`data-ui-arg-*` values update `args` and schedule
|
|
389
|
+
its normal `rerender()` lifecycle. After `await self.flush()`, use
|
|
390
|
+
`await self.wait_children()` to await new child initialization and pending list
|
|
391
|
+
argument updates. Removed children are disposed. Changing a row's key or element
|
|
392
|
+
type deliberately replaces it. Deferred children receive new arguments without
|
|
393
|
+
being initialized automatically. These guarantees cover framework-controlled moves
|
|
394
|
+
within the list, not arbitrary DOM moves across component/application owners.
|
|
395
|
+
|
|
396
|
+
Nested ordinary `bind`/`data-bind` regions inside rows are supported. Place a
|
|
397
|
+
nested keyed or virtual list in a child Component so it has its own renderer and
|
|
398
|
+
lifetime. Keyed rendering still evaluates the entire loop; it reduces DOM and
|
|
399
|
+
lifecycle churn, rather than the amount of row-template evaluation.
|
|
400
|
+
|
|
401
|
+
## Fixed-height virtual lists
|
|
402
|
+
|
|
403
|
+
For a large sequence, a `virtual` block evaluates and mounts only a visible
|
|
404
|
+
window plus overscan. Its body receives `item` and zero-based `index`:
|
|
405
|
+
|
|
406
|
+
```html
|
|
407
|
+
{% virtual "items", component.items, key="id", row_height=40, height=400, overscan=3 %}
|
|
408
|
+
<span>{{ item.title }}</span>
|
|
409
|
+
<button @click="open" :item="{{ item.id }}">Open</button>
|
|
410
|
+
{% endvirtual %}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`key` is a mapping key, object attribute name, or callable such as
|
|
414
|
+
`key=component.item_key`. Key values must be non-empty strings or integers and
|
|
415
|
+
must be unique across the sequence (integer `1` and string `"1"` collide).
|
|
416
|
+
`row_height` and viewport `height` are positive pixel sizes; `overscan` is a
|
|
417
|
+
non-negative integer. Defaults are 40, 400, and 3 respectively. The framework
|
|
418
|
+
provides a scroll viewport, spacer elements, fixed-height row wrappers, and
|
|
419
|
+
list/listitem roles with position and total-size attributes. Keep row content
|
|
420
|
+
within the configured height; variable-height rows and automatic measurement
|
|
421
|
+
are not supported. Use a normal block container, not a table or select element.
|
|
422
|
+
|
|
423
|
+
Scroll updates are frame-coalesced in either application update mode, and a
|
|
424
|
+
scroll that stays within the same rendered window does not reevaluate templates.
|
|
425
|
+
The viewport and overlapping keyed rows retain identity. Filtering clamps the
|
|
426
|
+
scroll offset when the sequence shrinks; empty sequences remove all rows.
|
|
427
|
+
Off-screen rows are disposed, so keep persistent edits/selection in application
|
|
428
|
+
state rather than solely in a row component. Keyboard focus on a row removed
|
|
429
|
+
from the window is not pinned or restored automatically.
|
|
430
|
+
|
|
431
|
+
The items expression and keys are reevaluated on invalidation; key validation
|
|
432
|
+
still scans the sequence. Only row-template evaluation, DOM size, and mounted
|
|
433
|
+
row components are bounded by the viewport. These APIs add no frontend runtime
|
|
434
|
+
dependencies and keep Jinja2 as the template engine.
|
|
435
|
+
|
|
436
|
+
## Browser type library
|
|
437
|
+
|
|
438
|
+
The package includes a broad browser API stub library under
|
|
439
|
+
`patera.desktop.ui.types`, covering DOM elements, events, networking, storage,
|
|
440
|
+
canvas, media and common JavaScript built-ins. No webtypy dependency is needed.
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
from __future__ import annotations
|
|
444
|
+
from typing import TYPE_CHECKING
|
|
445
|
+
|
|
446
|
+
if TYPE_CHECKING:
|
|
447
|
+
from patera.desktop.ui.types import MouseEvent, HTMLInputElement
|
|
448
|
+
|
|
449
|
+
def clicked(event: MouseEvent) -> None:
|
|
450
|
+
event.preventDefault()
|
|
451
|
+
button: int = event.button
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
These declarations are for static analysis; actual browser objects still come
|
|
455
|
+
from `js` or `pyscript`. See the [type library guide](src/patera/desktop/ui/types/README.md)
|
|
456
|
+
for coverage and limitations and [provenance](src/patera/desktop/ui/types/NOTICE.md)
|
|
457
|
+
for the generated definitions' MPL-2.0 license.
|