@gramlot/gramlot-examples 0.2.11 → 0.2.14

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.
Files changed (71) hide show
  1. package/README.md +10 -3
  2. package/package.json +3 -3
  3. package/src/gramlot_examples/catalog.json +36 -6
  4. package/src/gramlot_examples/gallery/dist/frame.js +1742 -5
  5. package/src/gramlot_examples/gallery/dist/gallery.js +4073 -17144
  6. package/src/gramlot_examples/gallery/dist/inspector.js +1801 -0
  7. package/src/gramlot_examples/gallery/dist/notices.json +2 -22
  8. package/src/gramlot_examples/gallery/dist/theme-styles.js +2 -0
  9. package/src/gramlot_examples/gallery/gallery-page.js +35 -67
  10. package/src/gramlot_examples/gallery/gallery.css +23 -94
  11. package/src/gramlot_examples/index.js +2 -1
  12. package/src/gramlot_examples/pages/README.md +84 -1
  13. package/src/gramlot_examples/pages/binding/01_pointers.js +9 -0
  14. package/src/gramlot_examples/pages/binding/01_pointers.md +56 -9
  15. package/src/gramlot_examples/pages/binding/02_variable_datapath.md +50 -4
  16. package/src/gramlot_examples/pages/binding/03_setters_and_defaults.md +45 -6
  17. package/src/gramlot_examples/pages/binding/04_native_editing.md +48 -6
  18. package/src/gramlot_examples/pages/binding/05_checkbox_radio.md +44 -4
  19. package/src/gramlot_examples/pages/binding/06_style_and_visibility.md +40 -6
  20. package/src/gramlot_examples/pages/binding/07_bound_svg.md +48 -3
  21. package/src/gramlot_examples/pages/binding/08_freeze.js +1 -1
  22. package/src/gramlot_examples/pages/binding/08_freeze.md +44 -6
  23. package/src/gramlot_examples/pages/binding/09_forms.js +2 -1
  24. package/src/gramlot_examples/pages/binding/09_forms.md +53 -7
  25. package/src/gramlot_examples/pages/binding/10_svg_shapes.md +50 -5
  26. package/src/gramlot_examples/pages/binding/11_complete_page.md +53 -7
  27. package/src/gramlot_examples/pages/binding/README.md +34 -0
  28. package/src/gramlot_examples/pages/controllers/01_formula.md +48 -7
  29. package/src/gramlot_examples/pages/controllers/02_controller.js +24 -2
  30. package/src/gramlot_examples/pages/controllers/02_controller.md +61 -6
  31. package/src/gramlot_examples/pages/controllers/03_named_logic.md +63 -7
  32. package/src/gramlot_examples/pages/controllers/04_inline_expressions.md +50 -7
  33. package/src/gramlot_examples/pages/controllers/05_node_methods.md +41 -7
  34. package/src/gramlot_examples/pages/controllers/06_button_controller.md +49 -6
  35. package/src/gramlot_examples/pages/controllers/07_events.md +45 -6
  36. package/src/gramlot_examples/pages/controllers/08_end_to_end.js +26 -1
  37. package/src/gramlot_examples/pages/controllers/08_end_to_end.md +138 -9
  38. package/src/gramlot_examples/pages/controllers/09_endpoint.md +47 -7
  39. package/src/gramlot_examples/pages/controllers/10_call_on_change.md +48 -8
  40. package/src/gramlot_examples/pages/controllers/11_call_on_click.js +6 -0
  41. package/src/gramlot_examples/pages/controllers/11_call_on_click.md +58 -6
  42. package/src/gramlot_examples/pages/controllers/12_remote.md +55 -8
  43. package/src/gramlot_examples/pages/controllers/13_cached_remote.js +2 -2
  44. package/src/gramlot_examples/pages/controllers/13_cached_remote.md +62 -7
  45. package/src/gramlot_examples/pages/controllers/14_data_remote.js +10 -2
  46. package/src/gramlot_examples/pages/controllers/14_data_remote.md +65 -6
  47. package/src/gramlot_examples/pages/controllers/15_lifecycle.js +51 -0
  48. package/src/gramlot_examples/pages/controllers/15_lifecycle.md +59 -0
  49. package/src/gramlot_examples/pages/controllers/16_attribute_projection.js +25 -0
  50. package/src/gramlot_examples/pages/controllers/16_attribute_projection.md +58 -0
  51. package/src/gramlot_examples/pages/controllers/17_data_exchange.js +51 -0
  52. package/src/gramlot_examples/pages/controllers/17_data_exchange.md +61 -0
  53. package/src/gramlot_examples/pages/controllers/18_publish_subscribe.js +36 -0
  54. package/src/gramlot_examples/pages/controllers/18_publish_subscribe.md +20 -0
  55. package/src/gramlot_examples/pages/controllers/19_frame_messages.js +39 -0
  56. package/src/gramlot_examples/pages/controllers/19_frame_messages.md +20 -0
  57. package/src/gramlot_examples/pages/controllers/README.md +57 -1
  58. package/src/gramlot_examples/pages/html_svg/01_hello_world.md +48 -4
  59. package/src/gramlot_examples/pages/html_svg/02_text_and_links.md +42 -4
  60. package/src/gramlot_examples/pages/html_svg/03_lists.md +45 -4
  61. package/src/gramlot_examples/pages/html_svg/04_semantic_page.md +44 -4
  62. package/src/gramlot_examples/pages/html_svg/05_tables.md +46 -4
  63. package/src/gramlot_examples/pages/html_svg/06_forms.md +48 -4
  64. package/src/gramlot_examples/pages/html_svg/07_native_disclosure.md +41 -4
  65. package/src/gramlot_examples/pages/html_svg/08_svg_shapes.md +45 -4
  66. package/src/gramlot_examples/pages/html_svg/09_svg_composition.md +44 -4
  67. package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.md +42 -15
  68. package/src/gramlot_examples/pages/html_svg/11_static_report.md +37 -4
  69. package/src/gramlot_examples/pages/html_svg/12_complete_page.md +41 -4
  70. package/src/gramlot_examples/pages/html_svg/13_live_source.md +47 -16
  71. package/src/gramlot_examples/pages/html_svg/README.md +36 -0
@@ -19,6 +19,10 @@ export class Page extends BasePage {
19
19
  result.h2('Menu, loaded by the server when it is read');
20
20
  result.p('^.menu', {id: 'menu'});
21
21
 
22
+ result.button('Read current value', {id: 'read-menu'}).dataController({func: 'readMenu'});
23
+ result.p('Snapshot returned by the explicit read:', {class: 'muted'});
24
+ result.p('^.read_value', {id: 'read-value'});
25
+
22
26
  pane.dataRemote({destination_path: '.menu', method: 'load_menu', cacheTime: 5, table: '^.table'});
23
27
  pane.dataSetter({destination_path: '.table', value: 'dishes'});
24
28
  }
@@ -27,5 +31,9 @@ export class Page extends BasePage {
27
31
  }
28
32
  Page.registerEndpoint('load_menu');
29
33
 
30
- /** The page logic: this page names no methods. */
31
- export class Logic {}
34
+ /** The explicit read returns the stored value and may start a resolver refresh. */
35
+ export class Logic {
36
+ readMenu(node) {
37
+ node.SET('.read_value', node.GET('.menu'));
38
+ }
39
+ }
@@ -1,10 +1,69 @@
1
1
  # 14 · Data remote
2
2
 
3
- `dataRemote(destination_path, method, cacheTime=None, **params)` makes a Data path a value computed by an endpoint of the page. The endpoint is called when the path is read, not when the page starts.
3
+ A menu loads when its Data path is read. You learn how `dataRemote` supplies values on demand. The read button copies the stored value; a fresh response can arrive afterwards. After the five-second cache expires, another read starts a refresh; expiry alone makes no call.
4
4
 
5
- - **Declare the endpoint** as for `dataRpc`: Python `@endpoint`, with `**kwargs`; JavaScript `Page.registerEndpoint('load_menu')`.
6
- - **A read never waits.** The paragraph `^.menu` reads the path: the read returns the current value, empty before the first call, and starts the call. The answer is written with the Data event, so the paragraph shows the menu.
7
- - **`cacheTime`** is in seconds. `5` serves the stored value for five seconds, then the next read calls again. `None` or a negative value calls once; `0` calls at every read and writes the answer without the Data event.
8
- - **`table='^.table'`** is an argument of the endpoint. After the first call, a change of a `^` argument loads the value again at once and the paragraph receives it.
5
+ **Uses:** Data binding and an endpoint-backed resolver — core 0.2.16. An in-memory example, not a database service.
9
6
 
10
- **Try it:** Choose Drinks: the menu changes to the drinks that the server returns.
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+
11
+ **Release stage:** Remote Source and lazy Data (core 0.2.16), after the 0.2.14 endpoint lessons.
12
+
13
+ **Goal:** Distinguish resolver installation, first read, response and later refresh.
14
+
15
+ **Before this lesson:** [c09 · Endpoint](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/09_endpoint.md), [c10 · Call on change](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/10_call_on_change.md), [c13 · Cached remote](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/13_cached_remote.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c14 · Data remote** in your environment’s gallery. For source changes, copy/edit the matching page in your own project using the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md); the gallery is a source viewer. Save, then reload/reopen as that guide explains. The snippets below are excerpts/edits within that Page, not standalone launch commands.
18
+
19
+ ## Read the page
20
+
21
+ `dataRemote` installs a resolver at `.menu`. Installation alone sends no call. The paragraph’s `^.menu` immediately reads it, which starts `load_menu`; the first read returns the stored value (initially null) without waiting.
22
+
23
+ ```python
24
+ pane.dataRemote(".menu", "load_menu", cacheTime=5, table="^.table")
25
+ ```
26
+
27
+ After the response, the resolver writes Data and notifies the paragraph for a nonzero cacheTime. The Read current value button calls named Logic: `node.GET('.menu')` reads synchronously and `node.SET('.read_value', ...)` displays that snapshot. A later response updates Menu; it does not retroactively change an already copied snapshot. Read `readMenu` in the gallery’s **logic.js** tab (the Logic class from `14_data_remote.js`) beside the Python source.
28
+
29
+ Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Endpoint/fragment methods execute there too; bindings and any Logic execute in the browser. A Worker is not a private server.
30
+
31
+ ## Try it and predict the result
32
+
33
+ 1. On opening, the paragraph reads and then shows “Soup, Risotto, Fish”. The read snapshot is empty until you press the button.
34
+ 2. Press Read current value within five seconds of loading: the snapshot shows the stored menu; no additional call is needed.
35
+ 3. Choose Drinks: since the old resolver had already loaded, its replacement loads immediately and Menu becomes “Water, Lemonade, Coffee”. The earlier snapshot stays unchanged until another explicit read.
36
+
37
+ ## Change it and check yourself
38
+
39
+ To isolate **installation from first read**, temporarily remove the `result.p("^.menu", id="menu")` declaration in your editable copy. Keep the button and `.read_value` display.
40
+
41
+ **Answer:** opening installs the resolver but does not call `load_menu`. The first button press returns the current empty value and starts a call. After the response, a second press displays the loaded menu. No explicit fetch or await was added to the page.
42
+
43
+ ## The same example across stages
44
+
45
+ 0.2.14’s `dataRpc` explicitly triggers a Page computation and places its result in Data. In 0.2.16, a read of a declared Data path can own that loading through a resolver: you no longer author “if absent or expired, call and store” coordination. The endpoint method remains the same kind of Page method.
46
+
47
+ This is a variation of server-backed Data, not an automatic replacement for every RPC. Choose dataRpc for a commanded action/error callback, remote for Source content, and dataRemote for read-driven values. The advanced exercises modify this same menu page rather than introducing a parallel request/state system.
48
+
49
+ ## Advanced practice
50
+
51
+ Start each trial from a reload and inspect only `load_menu` calls in Network on HTTP. In a Worker environment the equivalent calls are messages.
52
+
53
+ - **Expiry then read (5).** Wait for the first response, then wait more than five seconds without changing anything. Expect no timer-driven call. Press Read current value: it immediately displays the old value and starts a refresh. Wait for the response and read again within its cache window: no extra refresh is needed. Read twice while a deliberately delayed response is pending: only one call is in flight.
54
+ - **Zero (0).** In the no-automatic-consumer variant from the exercise above, change `cacheTime=5` to 0. First click starts a call and returns empty. After it finishes, the next click displays the previous result **and starts another call**. Zero-cache writes do not emit a Data event, so a permanently bound Menu paragraph would not announce the completed response; do not mistake that for failed transport.
55
+ - **Once (`None`, JS `null`, or negative).** Repeat reads: one call per installed resolver. Change Table after it has loaded: the reactive argument replaces the resolver and loads a new value; “once” is not forever across all parameter combinations.
56
+ - **Failure.** Change the Drinks option’s authored value to a missing key, reload, first load Dishes, then choose it. Expect a console error while the last successful menu remains. Restore the option. No custom error callback is invented.
57
+ - **Condition and lifetime.** In an editable copy, add an enabled checkbox/setter and `enabled="^.enabled", _if="enabled"`. Disabling it removes the resolver, retains the last value and cancels its pending call. Re-enabling after it previously loaded starts a replacement load. Removing the declaration’s owning Source branch or disposing the page also aborts its pending call; an aborted response must not overwrite the Data. These conditional declarations use inline code and require the permitted CSP profile.
58
+
59
+ These are separate advanced trials, not prerequisites for the first menu read. The release sequence continues with future agreed features; the complete-page extension and environment-wide qualification are separate planned work.
60
+
61
+ ## Limits and next step
62
+
63
+ A resolver read is not a promise for the fresh response. `cacheTime` uses **seconds**, unlike controller delays in milliseconds. `None` (JS null) or a negative value means once per resolver instance; 0 means every explicit read, with concurrent reads coalesced. Failures retain the stored value and reach the console naming the declaration.
64
+
65
+ Do not copy dataRpc’s `_onCalling`, `_onResult` or `_onError` onto dataRemote, or use `_init`, `_onBuilt`, `_onStart` there. An auth-aware environment is still needed for protected methods. This in-memory menu is not a database service.
66
+
67
+ Continue with [setup and release sequence](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
68
+
69
+ </details>
@@ -0,0 +1,51 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Lifecycle';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+ main(root) {
7
+ const pane = root.div({class: 'example-page stack', datapath: 'lifecycle'});
8
+ pane.h1('Lifecycle');
9
+ pane.p('Follow initialization, building, starting and removal of one branch.');
10
+ const actions = pane.div({class: 'actions'});
11
+ for (const [label, method] of [['Freeze', 'freeze'], ['Thaw', 'thaw'],
12
+ ['Inspect Source and DOM', 'inspect'], ['Remove branch', 'remove']]) {
13
+ actions.button(label, {id: method, type: 'button'}).dataController({func: method});
14
+ }
15
+ for (const [label, key] of [['Hook trace', 'trace'], ['Data ticks', 'ticks'], ['Lookup', 'lookup']]) {
16
+ pane.h2(label); pane.p(`^.${key}`, {id: key});
17
+ }
18
+ const observed = pane.section({node_id: 'observed', id: 'observed', class: 'card stack'});
19
+ observed.h2('Observed branch');
20
+ observed.p('^.ticks', {id: 'branch-ticks'});
21
+ for (const [hook, stage] of [['_init', 'init'], ['_onBuilt', 'built'], ['_onStart', 'start']]) {
22
+ observed.dataController({func: 'record', stage, [hook]: true});
23
+ }
24
+ observed.dataController({func: 'tick', _timing: 1, _onStart: true});
25
+ pane.dataSetter({destination_path: '.trace', value: ''});
26
+ pane.dataSetter({destination_path: '.ticks', value: 0});
27
+ pane.dataSetter({destination_path: '.lookup', value: 'Press Inspect'});
28
+ }
29
+ }
30
+
31
+ export class Logic {
32
+ branch() { return this.page.src.source.getNodeByAttr('node_id', 'observed'); }
33
+ record(node, {stage}) {
34
+ const previous = node.GET('.trace');
35
+ node.SET('.trace', previous ? `${previous} → ${stage}` : stage);
36
+ }
37
+ tick(node) { node.SET('.ticks', node.GET('.ticks') + 1); }
38
+ freeze() { const branch = this.branch(); if (branch) this.page.src.renderer.freeze(branch); }
39
+ thaw() { const branch = this.branch(); if (branch) this.page.src.renderer.unfreeze(branch); }
40
+ inspect(node) {
41
+ const branch = this.branch();
42
+ if (!branch) { node.SET('.lookup', 'Source branch absent'); return; }
43
+ const element = this.page.dom.getDomNode(branch);
44
+ node.SET('.lookup', element && this.page.dom.getBaseSourceNode(element) === branch
45
+ ? 'Source → DOM → same Source' : 'Source present; DOM absent');
46
+ }
47
+ remove() {
48
+ const branch = this.branch();
49
+ if (branch) branch.parentBag.popNode(branch.label);
50
+ }
51
+ }
@@ -0,0 +1,59 @@
1
+ # 15 · Lifecycle
2
+
3
+ Start and stop a branch, then remove it. You learn when its lifecycle actions run and which work ends with that branch.
4
+
5
+ **Uses:** Source, Data, controllers and lifecycle (0.2.0); module layout from 0.2.5.
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+
11
+ **Release stage:** core 0.2.0; the maintained Page/Logic file layout is from 0.2.5.
12
+
13
+ **Goal:** Predict when a branch starts and which work ends when it is removed.
14
+
15
+ **Before this lesson:** [c03 · Named logic](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/03_named_logic.md), [c06 · Button controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/06_button_controller.md), [b08 · Freeze](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/08_freeze.md).
16
+
17
+ **Open and edit:** open this page in your environment's gallery. Read Python first; Python builds Source on the host and named JavaScript Logic runs in the browser. The JavaScript Page is the equivalent host/Worker declaration. The gallery shows source; to change it, copy the page into your editable project following the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md). Read the **logic.js** tab (the Logic class from `15_lifecycle.js`) when using Python. Reload after source edits.
18
+
19
+ ## Read the page
20
+
21
+ The same branch contains three named controllers. Each labels a different hook:
22
+
23
+ ```python
24
+ observed.dataController(func="record", stage="init", _init=True)
25
+ observed.dataController(func="record", stage="built", _onBuilt=True)
26
+ observed.dataController(func="record", stage="start", _onStart=True)
27
+ ```
28
+
29
+ Data setters run before these hooks. `_init` installs initial behavior, `_onBuilt` follows the first successful build, and `_onStart` follows readiness. A separate timer controller uses `_timing=1` (seconds). Hook delays, if numeric, are milliseconds. The trace and tick count are ordinary Data, displayed outside the removable branch.
30
+
31
+ Inspect uses `gramlot.dom.getDomNode(sourceNode)` and `getBaseSourceNode(domNode)` for read-only identity lookup. It does not mutate the DOM. `node_id` locates Source; the separate HTML `id` locates the rendered element.
32
+
33
+ ## Try it and predict the result
34
+
35
+ 1. Open the page: Hook trace becomes `init → built → start`. Data ticks begins at 1 because the timer also has `_onStart=True`.
36
+ 2. Press Inspect: expect `Source → DOM → same Source`.
37
+ 3. Freeze; wait two seconds. Both Data ticks and the number already built inside the branch continue increasing. Freeze defers structural Source projection, not updates to existing bound text.
38
+ 4. Thaw: the three hooks are not appended again. Compare b08, where pending structural removals appear at thaw.
39
+ 5. Remove branch and wait two seconds: its DOM disappears, the hook trace remains unchanged and ticks stop. Timer cancellation is observed from the stopped counter; the page does not invent a disposal hook. Inspect now reports `Source branch absent`. Reload to restore the original branch.
40
+
41
+ ## Change it and check yourself
42
+
43
+ Change `_timing=1` to 2 in the Python timer declaration (or `_timing: 2` in JS), then reload.
44
+
45
+ **Answer:** the initial start still adds one tick; subsequent ticks are about two seconds apart. Do not change `_onStart` to 2 expecting seconds: its numeric delay uses milliseconds.
46
+
47
+ Advanced: freeze, then remove the branch before thawing. The Data ticks stop even though the frozen DOM remains visible. Inspect reports that Source is absent. The buttons no longer find that removed Source branch, so reload to reset this trial. Use the Data count as the cleanup evidence, not whether old pixels are still visible.
48
+
49
+ ## The same example across stages
50
+
51
+ b08 introduced a frozen visual branch. This lesson revisits it with explicit lifecycle observations. Compared with e13's manually owned animation timer, `_timing` owns repeated controller scheduling and cancellation: no page timer handle or `clearInterval` is needed. Source/DOM lookup is diagnostic; it does not introduce another state system.
52
+
53
+ ## Limits and next step
54
+
55
+ Freeze defers structural projection and insertion/rebuild work; existing nodes still follow Data. Removing a branch is different from hiding it. This page uses named Logic; no new lifecycle hook or timer implementation is invented. Full page disposal also ends its providers. Late startup and insertion while frozen are advanced contract cases, not extra steps needed for the basic trace.
56
+
57
+ Continue with [c16 · Attribute projection](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/16_attribute_projection.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) provides the full contract.
58
+
59
+ </details>
@@ -0,0 +1,25 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Attribute projection';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+ main(root) {
7
+ const pane = root.div({class: 'example-page stack', datapath: 'projection'});
8
+ pane.h1('Attribute projection');
9
+ pane.h2('0.2.0: one value, several projections');
10
+ pane.html_label('Name', {for: 'name'});
11
+ pane.input({value: '^.name', live: true, id: 'name'});
12
+ pane.p('^.name', {mask: 'Hello, %s', id: 'greeting'});
13
+ pane.div('Hover to read the title', {customer: '^.name', title: 'Customer: ${customer}', id: 'card'});
14
+ pane.p('The template consumes customer; it is not a DOM attribute.');
15
+ pane.h2('0.2.3: a separate HTML preview');
16
+ pane.html_label('Preview HTML', {for: 'html'});
17
+ pane.textarea({value: '^.html', live: true, id: 'html'});
18
+ pane.iframe({srcdoc: '^.html', title: 'Sandboxed preview', id: 'preview'});
19
+ pane.p('Data-bound srcdoc receives an empty sandbox; this preview only displays content.');
20
+ pane.dataSetter({destination_path: '.name', value: 'Ada'});
21
+ pane.dataSetter({destination_path: '.html', value: '<h2>Hello, Ada</h2><p>A separate document.</p>'});
22
+ }
23
+ }
24
+
25
+ export class Logic {}
@@ -0,0 +1,58 @@
1
+ # 16 · Attribute projection
2
+
3
+ One name appears in a greeting and a tooltip with different formatting. You learn how templates and masks reuse a bound value without copying text. The later **0.2.3** section previews HTML in a separate sandboxed frame.
4
+
5
+ **Uses:** Data binding, attribute templates and text masks (0.2.0); sandboxed Data `srcdoc` only in the later part (0.2.3).
6
+
7
+ <details>
8
+ <summary>More detail</summary>
9
+
10
+
11
+ **Release stage:** core 0.2.0 templates/masks, followed by the separate 0.2.3 Data-srcdoc sandbox step.
12
+
13
+ **Goal:** Reuse one Data value in text and attributes, then distinguish text from a sandboxed HTML document.
14
+
15
+ **Before this lesson:** [b01 · Pointers](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/01_pointers.md), [b06 · Style and visibility](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/06_style_and_visibility.md), [c04 · Inline expressions](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/04_inline_expressions.md).
16
+
17
+ **Open and edit:** open this page in your environment's gallery. Read Python first; Python builds Source on the host and named JavaScript Logic runs in the browser. The JavaScript Page is the equivalent host/Worker declaration. The gallery shows source; to change it, copy the page into your editable project following the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md). This page needs no Logic methods; compare the Python and JavaScript Page declarations. Reload after source edits.
18
+
19
+ ## Read the page
20
+
21
+ A template reads another attribute of its own Source node **after pointer resolution**:
22
+
23
+ ```python
24
+ pane.div("Hover to read the title", customer="^.name",
25
+ title="Customer: ${customer}")
26
+ pane.p("^.name", mask="Hello, %s")
27
+ ```
28
+
29
+ `${customer}` is Gramlot syntax, not Python interpolation. `customer` supplies the template and is consumed instead of becoming a DOM attribute. `mask` formats the projected text; it does not enable HTML.
30
+
31
+ The later section uses `pane.iframe(srcdoc="^.html", ...)`. Data-provided HTML gets `sandbox=""`: it is an isolated document, with scripts/forms/popups blocked by default. Ordinary `pane.p("^.html")` would show literal markup instead.
32
+
33
+ ## Try it and predict the result
34
+
35
+ 1. Initially the greeting is “Hello, Ada”; hover the card and expect title “Customer: Ada”.
36
+ 2. Type Grace: both follow the same Data value. Inspect the card: it has `title`, but no `customer` attribute.
37
+ 3. Clear Name: the greeting becomes empty; the title retains its authored prefix.
38
+ 4. Replace Preview HTML with `<h2>Preview two</h2><p>Only this frame changes.</p>`. The heading appears inside the frame; the outer page stays intact. Inspect the iframe's `sandbox` attribute: it is present with an empty value.
39
+
40
+ ## Change it and check yourself
41
+
42
+ Change the mask to `Welcome, %s` and the title to `Guest: ${customer}`. Leave the Data pointer alone.
43
+
44
+ **Answer:** one Name edit still updates both destinations with different formatting; no extra provider or shared formatted Data value is needed.
45
+
46
+ Separate invalid-edit exercise: rename only the template reference to `${missing}`, then reload. Expect an unknown-template-parameter error, not a blank substitute. Restore it. A preceding backslash escapes `${`; an `==` attribute cannot supply a template parameter, and `==` expressions cannot contain a template. See the reference for the permitted expression/template dependency order.
47
+
48
+ ## The same example across stages
49
+
50
+ e02 taught literal text; b01 added a pointer. The 0.2.0 stage now adds presentation around that same value without a controller copying strings into multiple outputs. The 0.2.3 section is explicitly later: an iframe preview is not a replacement for safe literal text, and the sandbox does not imply a widget or raw-HTML injection API.
51
+
52
+ ## Limits and next step
53
+
54
+ Templates apply to visual attributes, not node text or data-element declarations. A static Python f-string runs on the host once. A mask never interprets HTML. Keep the default empty sandbox for this exercise; do not add script permissions. A literal authored srcdoc has a different contract. JavaScript counterparts use quoted template text here, not JS backtick interpolation.
55
+
56
+ Continue with [c17 · Data exchange](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/17_data_exchange.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) provides the full contract.
57
+
58
+ </details>
@@ -0,0 +1,51 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Data exchange';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+ main(root) {
7
+ const pane = root.div({class: 'example-page stack', datapath: 'exchange'});
8
+ pane.h1('Data exchange');
9
+ const form = pane.section({class: 'card stack', datapath: '.record'});
10
+ form.html_label('Name', {for: 'name'});
11
+ form.input({value: '^.name', id: 'name'});
12
+ form.html_label('Seats', {for: 'seats'});
13
+ form.input({type: 'number', value: '^.seats', id: 'seats'});
14
+ form.html_label('Confirmed', {for: 'confirmed'});
15
+ form.input({type: 'checkbox', value: '^.confirmed', id: 'confirmed'});
16
+ form.p('The record also contains a null note and a nested address.');
17
+ const actions = pane.div({class: 'actions'});
18
+ for (const [label, method] of [['Inspect types', 'inspect'], ['Save TYTX', 'save'],
19
+ ['Restore TYTX', 'restore'], ['Export JSON', 'exportJson'], ['Export XML', 'exportXml'],
20
+ ['Preview email text', 'previewMail']]) {
21
+ actions.button(label, {type: 'button', id: method}).dataController({func: method});
22
+ }
23
+ pane.p('^.status', {id: 'status'});
24
+ pane.pre('^.types', {id: 'types'});
25
+ pane.pre('^.mail', {id: 'mail'});
26
+ pane.dataSetter({destination_path: '.record', value: {name: 'Ada', seats: 2, confirmed: true,
27
+ note: null, address: {city: 'Rome'}}});
28
+ pane.dataSetter({destination_path: '.status', value: 'Choose an action; files stay under your control.'});
29
+ }
30
+ }
31
+
32
+ export class Logic {
33
+ inspect(node) {
34
+ const record = node.GET('.record');
35
+ node.SET('.types', `name: ${typeof record.getItem('name')}
36
+ seats: ${typeof record.getItem('seats')}
37
+ confirmed: ${typeof record.getItem('confirmed')}
38
+ note: ${record.getItem('note') === null ? 'null' : typeof record.getItem('note')}
39
+ city: ${record.getItem('address.city')}`);
40
+ }
41
+ save(node) { this.page.utl.inout.save('exchange.record', 'registration.tytx.json'); node.SET('.status', 'TYTX download requested'); }
42
+ async restore(node) {
43
+ try {
44
+ await this.page.utl.inout.restore('exchange.record');
45
+ this.inspect(node); node.SET('.status', 'Restored typed Data');
46
+ } catch (error) { node.SET('.status', error.message); }
47
+ }
48
+ exportJson() { this.page.utl.inout.download('exchange.record', 'registration.json', 'json'); }
49
+ exportXml() { this.page.utl.inout.download('exchange.record', 'registration.xml', 'xml'); }
50
+ previewMail(node) { node.SET('.mail', this.page.utl.inout.mailText('exchange.record')); }
51
+ }
@@ -0,0 +1,61 @@
1
+ # 17 · Data exchange
2
+
3
+ Edit a record, save it as TYTX, change it again and restore the file. You learn how to save a whole Data Bag with its supported types. JSON/XML exports are for interchange, not Restore TYTX.
4
+
5
+ The record contains text, numbers, a boolean, a null value and a nested address. The email button previews text only.
6
+
7
+ **Uses:** Data binding, named Logic and local InOut utilities (0.2.5), with the utility name from 0.2.10. No HTTP service or email delivery.
8
+
9
+ <details>
10
+ <summary>More detail</summary>
11
+
12
+
13
+ **Release stage:** core 0.2.5; the public `gramlot.utl.inout` spelling is from 0.2.10.
14
+
15
+ **Goal:** Save and restore a typed Data Bag, and distinguish round-trip files from interchange exports.
16
+
17
+ **Before this lesson:** [b09 · Forms with binding](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/09_forms.md), [c03 · Named logic](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/03_named_logic.md), [c06 · Button controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/06_button_controller.md).
18
+
19
+ **Open and edit:** open this page in your environment's gallery. Read Python first; Python builds Source on the host and named JavaScript Logic runs in the browser. The JavaScript Page is the equivalent host/Worker declaration. The gallery shows source; to change it, copy the page into your editable project following the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md). Read the **logic.js** tab (the Logic class from `17_data_exchange.js`) when using Python. Reload after source edits.
20
+
21
+ ## Read the page
22
+
23
+ Python initializes one record with strings, a number, a boolean, a null note and a nested address. Three fields are editable.
24
+
25
+ Each Python button declares one named method, for example:
26
+
27
+ ```python
28
+ actions.button("Save TYTX", type="button").dataController(func="save")
29
+ ```
30
+
31
+ Its Logic calls `this.page.utl.inout.save('exchange.record', 'registration.tytx.json')`. Restore opens the framework's file picker and waits for `restore(...)`; after it resolves, the fields follow the replaced Bag. Errors are written to the status. The application authors no file input, Blob, request envelope or DOM listener.
32
+
33
+ ## Try it and predict the result
34
+
35
+ 1. Press Inspect types: expect string, number, boolean, null, and city Rome.
36
+ 2. Edit Name to Grace and leave the field; set Seats to 3. Press Save TYTX and retain the downloaded file.
37
+ 3. Change Name again, then Restore TYTX and select that saved file: Name returns to Grace, Seats to 3, and Inspect still reports number, boolean and null for their respective fields.
38
+ 4. Export JSON and Export XML: inspect these files as interchange data, not as a Gramlot restore file. JSON is Bag’s list of `label`/`value`/`attr` nodes, not the plain-object JSON used by `sendHttp`; XML has a `record` root. Try restoring exported JSON; expect an error and the existing record to remain intact.
39
+ 5. Preview email text: inspect the leaf values, including the nested `address.city` line. This button only renders `mailText`; it does not open a mail program.
40
+
41
+ ## Change it and check yourself
42
+
43
+ Add `"department": "Research"` to the initial Python record and a bound paragraph `form.p("^.department")` (the JS equivalent belongs in its object and Source).
44
+
45
+ **Answer:** Save/Restore includes the new field without another serialization handler, because the utility operates on the whole Bag. Reloading resets the authored defaults; restoring reintroduces the saved values.
46
+
47
+ Advanced transport study: `sendMail(path, email)` opens the user's mail program and has a 2000-character limit; it does not itself submit an email. `sendHttp(path, url)` posts JSON and rejects non-2xx responses; it is not a page endpoint call. Exercise these only with an explicitly chosen mail action or an environment-owned local receiving fixture. The common gallery supplies no HTTP receiver. The core's `inout.test.js` and `scripts/verify_inout_browser.mjs` show intercepted mail links and local HTTP checks; they are verification fixtures, not application networking code to copy. No real messages or external service requests are part of this trial.
48
+
49
+ ## The same example across stages
50
+
51
+ b09 made a form share one Data record with its display. This later release lets the same record cross the file boundary: two utility calls replace bespoke serialization, file-choice and restore coordination. Earlier form examples remain valid without file operations. The next release stage introduces endpoints; a download is not server persistence.
52
+
53
+ ## Limits and next step
54
+
55
+ **Scope of this lesson:** this example uses strings, numbers, booleans, null and nested Bags. Dates and decimals are deferred while [Bag issue #69](https://github.com/genro-org/genro-bag/issues/69) addresses typed object initialization in JavaScript and Python/JS API consistency. The owner accepted this reduced scope for now. The verified defect affects object initialization; it does not establish a failure of TYTX Save/Restore. No alternate initialization path is needed for this lesson.
56
+
57
+ TYTX Save/Restore preserves supported types. JSON/XML exports are for other programs; the `.json` suffix alone does not make a file restorable. File dialogs require a user gesture. If you cancel a picker, the existing Data remains; start a new Restore action when ready. Browser download settings may prompt or save automatically. Named Logic handles these actions without an inline-code CSP requirement. The catalogue entry is a local file lesson; mail and HTTP environment checks are separately classified.
58
+
59
+ Continue with [c09 · Endpoint](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/09_endpoint.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) provides the full contract.
60
+
61
+ </details>
@@ -0,0 +1,36 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Publish and subscribe';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+ main(root) {
7
+ const pane = root.main({class: 'example-page stack', datapath: 'messages'});
8
+ pane.h1('The same action, two entry points');
9
+ pane.p('Compare the direct controller with a page topic. Both update the same summary.');
10
+ pane.dataSetter({destination_path: '.text', value: 'Hello'});
11
+ pane.html_label('Message', {for: 'message'});
12
+ pane.input({id: 'message', value: '^.text', live: true});
13
+ pane.button('Direct controller', {id: 'direct'}).dataController({func: 'save', text: '=.text'});
14
+ pane.button('Publish', {id: 'publish'}).dataController({func: 'publishMessage', text: '=.text'});
15
+ pane.dataController({func: 'save', subscribe_saved: true});
16
+ pane.p('^.summary', {id: 'summary'});
17
+ pane.button('Ask, then publish', {id: 'ask'}).dataController({func: 'publishMessage',
18
+ _ask: {title: 'Message', fields: [{name: 'text', lbl: 'Message', required: true}]}, text: '=.text'});
19
+ pane.h2('Two independent receivers');
20
+ for (const name of ['left', 'right']) {
21
+ const box = pane.section({class: 'card stack', datapath: `.${name}`});
22
+ box.h3(name[0].toUpperCase() + name.slice(1));
23
+ const target = box.dataController({func: 'receive', selfsubscribe_refresh: true});
24
+ box.button(`Send to ${name}`, {id: `send-${name}`}).dataController({func: 'sendToNode', target, text: '=.text'});
25
+ box.p('^.received', {id: `received-${name}`});
26
+ box.dataSetter({destination_path: '.text', value: `Hello ${name}`});
27
+ }
28
+ }
29
+ }
30
+
31
+ export class Logic {
32
+ save(node, {text, _reason}) { node.SET('.summary', `${text} (${_reason})`); }
33
+ publishMessage(node, {text}) { this.page.publish('saved', {text}); }
34
+ sendToNode(node, {target, text}) { target.publish('refresh', {text}); }
35
+ receive(node, {text}) { node.SET('.received', text); }
36
+ }
@@ -0,0 +1,20 @@
1
+ # 18 · Publish and subscribe
2
+
3
+ Revisit **Button controller**: one button calls a controller directly, another publishes a message to the same behavior. Then address two independent receivers without looking up their IDs, and collect a message in a dialog.
4
+
5
+ **Uses:** Source, Data, named Logic, page/self subscriptions, authored references and native `ask` — core 0.2.18. No endpoint calls. Earlier lessons remain the 0.2.0 stage.
6
+
7
+ **Try it:** edit Message, press Direct controller, then Publish. The summary is the same except for the trigger reason. Send to left: only the left receiver changes. Ask, then publish lets you edit the outgoing message; Cancel leaves the summary unchanged.
8
+
9
+ <details>
10
+ <summary>More detail</summary>
11
+
12
+ The direct controller keeps the c06 click mechanism. The publishing controller knows the topic `saved`; `subscribe_saved=True` connects the receiver. The publisher does not need a Data trigger path to activate that receiver. Repeating the same message still delivers it.
13
+
14
+ Each receiver declares `selfsubscribe_refresh=True`. Python passes that controller as `target`; Gramlot resolves the authored reference in the browser. `target.publish('refresh', {text})` addresses only that node. The JavaScript Page makes the same declaration, and both use the shared Logic class.
15
+
16
+ `_ask` adds native field collection to the existing controller. It runs the controller once after confirmation and never after cancellation. This example uses named methods throughout; it does not need inline JavaScript evaluation. For a conditional confirmation use a string `_ask` with `_ask_if`; those inline conditions need the corresponding CSP policy.
17
+
18
+ **Exercise:** subscribe a second controller to `saved`, writing another Data path. Predict whether both run when Publish is pressed. **Answer:** both run, in registration order; each should write its own result. A self-subscription does not also subscribe to the plain page topic.
19
+
20
+ </details>
@@ -0,0 +1,39 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Parent and child messages';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+ main(root) {
7
+ const pane = root.main({class: 'example-page stack', datapath: 'frames'});
8
+ pane.h1('Parent and child messages');
9
+ pane.dataSetter({destination_path: '.child', value: false});
10
+ pane.dataSetter({destination_path: '.text', value: 'Hello'});
11
+ pane.html_label('Message', {for: 'message'});
12
+ pane.input({id: 'message', value: '^.text', live: true});
13
+ const parent = pane.section({hidden: '^.child'});
14
+ const frame = parent.iframe({id: 'detail', title: 'Child page', style: 'width:100%;height:260px;border:1px solid #aab4c2'});
15
+ parent.button('Send to child', {id: 'send-child'}).dataController({func: 'sendChild', target: frame, text: '=.text'});
16
+ pane.button('Reply to parent', {id: 'reply'}).dataController({func: 'reply', text: '=.text', child: '=.child'});
17
+ pane.p('^.received', {id: 'received'});
18
+ pane.dataController({func: 'receive', subscribe_message: true});
19
+ pane.dataController({func: 'setup', frame, _onStart: true});
20
+ }
21
+ }
22
+
23
+ export class Logic {
24
+ setup(node, {frame}) {
25
+ const url = new URL(this.page.dom.getDomNode(node.parentNode).ownerDocument.location.href);
26
+ const child = url.hash === '#child';
27
+ node.SET('.child', child);
28
+ if (!child) { url.hash = 'child'; frame.setAttr({src: url.href}); }
29
+ }
30
+ sendChild(node, {target, text}) {
31
+ try { this.page.publish({topic: 'message', iframe: target}, {text}); }
32
+ catch (error) { node.SET('.received', error.message); }
33
+ }
34
+ reply(node, {text, child}) {
35
+ if (child) this.page.publish({topic: 'message', parent: true}, {text});
36
+ else node.SET('.received', 'Use Reply inside the child page.');
37
+ }
38
+ receive(node, {text}) { node.SET('.received', text); }
39
+ }
@@ -0,0 +1,20 @@
1
+ # 19 · Parent and child messages
2
+
3
+ Send a message to the embedded page, then reply from that page. The example reuses the page-topic receiver from **Publish and subscribe** and adds a destination to `publish`.
4
+
5
+ **Uses:** Source, Data, named Logic, authored iframe references and direct parent/child `postMessage` — core 0.2.18. No endpoint calls or server messaging.
6
+
7
+ **Try it:** wait for the child field to appear, edit the outer Message and press Send to child. Both summaries change: publication is local first, then reaches the selected child. Edit the child's Message and press its Reply to parent. The child publishes locally first and the parent receives the reply.
8
+
9
+ <details>
10
+ <summary>More detail</summary>
11
+
12
+ The same page opens with `#child` inside the iframe. Its setup controller hides the parent section and does not load another iframe, so nesting stops after one child. The URL hash is only this example's mode selector; it is not a messaging address.
13
+
14
+ Python passes the iframe Source as `target`. Logic uses `{topic: 'message', iframe: target}`. The child uses `{topic: 'message', parent: true}`. All cross-frame calls use postMessage, including same-origin pages; each receiving page activates its ordinary subscribe_message controller.
15
+
16
+ The child must be connected and ready. An early click reports the readiness error; retry after the child appears. There is no automatic queue or broadcast. Source/DOM objects stay local: the iframe reference selects a local destination, while only the message data crosses the connection. Reloading the child establishes a new connection.
17
+
18
+ **Exercise:** add another owned child and send only to its reference. **Answer:** local delivery still occurs, but only the selected child receives the remote copy. There is no recursive broadcast in this release.
19
+
20
+ </details>
@@ -1,11 +1,67 @@
1
1
  # Controllers
2
2
 
3
+ Add calculations and actions to bound pages, then calls to their host. Each stage uses the earlier building blocks plus only the features listed here.
4
+
5
+ - **0.2.0:** local formulas, controllers, Logic, events and lifecycle. Begin with **Formula**; the first part of **End-to-end story** combines them without a host call.
6
+ - **0.2.3:** the later part of **Attribute projection** adds a sandboxed HTML preview.
7
+ - **0.2.5:** revisit **Named logic** for the shared module layout; **Data exchange** adds local file save/restore, using the utility name introduced in 0.2.10.
8
+ - **0.2.14:** **Endpoint**, **Call on change** and **Call on click** add `dataRpc`. Then revisit the quote section of **End-to-end story** with the same inputs.
9
+ - **0.2.15:** the later part of **Call on click** adds Source authorization.
10
+ - **0.2.16:** **Remote**, **Cached remote** and **Data remote** add Source fragments and values loaded on demand.
11
+
12
+ - **0.2.18:** revisit **Button controller** in **Publish and subscribe**, then use **Parent and child messages** for direct iframe exchange. These pages require core 0.2.18 or later.
13
+
14
+ The host is Python, Node/Bun or a standalone browser Worker. Local Logic runs in the browser. Stable example numbers identify pages; the menu follows release order.
15
+
16
+ <details>
17
+ <summary>More detail</summary>
18
+
19
+
3
20
  The pages of this family run code when the Data changes.
4
21
 
5
22
  `dataFormula` computes a value and writes it to its result path. `dataController` runs code for its effects. `dataRpc` calls an endpoint of the page on the server and writes the answer to its result path. `remote` makes the content of an element a fragment built by the page on the server; `dataRemote` makes a Data path a value computed by an endpoint when it is read. A button runs a nested controller at every click; `connect_on<event>` attributes connect other DOM events. The code is inline in the page, or a method of the `Logic` class that the page module `NN_name.js` exports; the Python page takes it from there.
6
23
 
7
24
  Inline code needs a Content Security Policy that allows it. Named logic in the `Logic` class also runs under a strict policy.
8
25
 
9
- Start with **Formula** and **Controller**, then move the code to the `Logic` class with **Named logic**. **End-to-end story** puts every piece of the two families on one page. Six pages call the server. **Endpoint**, **Call on change** and **Call on click** use `dataRpc`; **Remote** and **Cached remote** mount a fragment of the page with `remote`; **Data remote** computes a Data value with `dataRemote`.
26
+ Start with **Formula** and **Controller**, then move the code to the `Logic` class with **Named logic**. **End-to-end story** preserves its local binding/lifecycle stage and adds a separately labelled endpoint trial to revisit later. Seven pages call the server. **End-to-end story** (later section), **Endpoint**, **Call on change** and **Call on click** use `dataRpc`; **Remote** and **Cached remote** mount a fragment of the page with `remote`; **Data remote** computes a Data value with `dataRemote`.
10
27
 
11
28
  Each example has a Python page and its JavaScript equivalent. Both use the `Logic` class of the JavaScript module. An endpoint is a method of the page in each language: Python `@endpoint`, JavaScript `Page.registerEndpoint`. A fragment method is declared with Python `@source`, JavaScript `Page.registerSource`.
29
+
30
+ ## Prerequisites within the release sequence
31
+
32
+ The 0.2.0 stage starts with c01/c02's calculation/effect distinction, then c03 for the
33
+ recommended named-Logic form. c06/c09/c11 teach click, server value and errors;
34
+ c12 teaches Source replacement in the later 0.2.16 stage.
35
+ The Page/Logic module arrangement is a 0.2.5 addition to the named-logic lesson. c08's first pass combines local binding/lifecycle. Return after c09/c11 for its
36
+ 0.2.14 quote/result/error section; it does not run automatically or add remote/lazy Data. After b02 and c12, use c13
37
+ for a first cache trial; c14 needs the endpoint concepts of c09/c10. Advanced
38
+ cache/lifecycle exercises are separate in c13/c14; their corrected cache behavior
39
+ is available from core 0.2.17. See the [release-by-release route](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md#gc-025-015). Prerequisites support the
40
+ chronological sequence; they do not replace it. The maintained files may use
41
+ later APIs, so they are not snapshots of the earliest release.
42
+
43
+ _userChanges filters exact-path changes, not user versus programmatic input.
44
+ Named connect_on handlers require dotted resource groups, unlike a root func
45
+ method in c03; the common FileServer gallery has no such resource host.
46
+
47
+ ## How to work through a lesson
48
+
49
+ Read its core release stage first, then its goal and prerequisites. Open the
50
+ existing preview, predict the guided result, and only then try the source-change
51
+ exercise in your own editable page. Each exercise includes an answer. Follow
52
+ "The same example across stages" links to compare earlier/later declarations;
53
+ keep both stages visible. A newer interactive page can contain more total code
54
+ while requiring less explicit state synchronization or request coordination.
55
+
56
+
57
+ ## Added depth in release order
58
+
59
+ After the local c08 stage, use c15 for lifecycle (0.2.0), c16 for attribute
60
+ projection (0.2.0) and its separately labelled sandbox step (0.2.3), then c17 for
61
+ Data exchange (0.2.5, utility path since 0.2.10). Continue with c09–c11 (0.2.14),
62
+ return to c08's 0.2.14 quote extension, then c11's later Source-auth section (0.2.15), and c12–c14 (0.2.16). Stable keys are
63
+ identifiers, not release numbers: their order in this menu follows these stages.
64
+ The date initialization issue in c17 and named-resource-host prerequisites remain
65
+ explicit gaps; the local file lesson does not imply HTTP service or mail delivery.
66
+
67
+ </details>