@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
@@ -7,6 +7,7 @@ export class Page extends BasePage {
7
7
  main(root) {
8
8
  const pane = root.div({class: 'example-page stack', datapath: 'story'});
9
9
  pane.h1('^.settings.caption', {id: 'caption'});
10
+ pane.h2('0.2.0 · Local page');
10
11
 
11
12
  const form = pane.form({class: 'card stack', node_id: 'form'});
12
13
  form.html_label('Caption', {for: 'caption-field'});
@@ -29,6 +30,7 @@ export class Page extends BasePage {
29
30
 
30
31
  const result = pane.section({class: 'card stack'});
31
32
  result.p('^.total', {id: 'total'});
33
+ // Optional: the same local total also drives an SVG projection.
32
34
  const drawing = result.svg({viewBox: '0 0 200 200', role: 'img', aria_label: 'Total as a circle', class: 'example-art'});
33
35
  drawing.circle({cx: 100, cy: 100, r: '^.radius', fill: '^.color', id: 'circle'});
34
36
  result.button('Press, also with Shift', {id: 'press'}).dataController({func: 'press'});
@@ -41,7 +43,7 @@ export class Page extends BasePage {
41
43
  pane.dataController({func: 'paint', total: '^.total', _init: true});
42
44
 
43
45
  const later = pane.section({class: 'card stack', node_id: 'later'});
44
- later.h2('A later branch');
46
+ later.h2('0.2.0 · Optional freeze exercise');
45
47
  const notes = later.ul({node_id: 'notes', id: 'notes'});
46
48
  for (const text of ['Setters live here too', 'This note can be removed under freeze']) notes.li(text);
47
49
  const actions = later.div({class: 'actions'});
@@ -52,8 +54,31 @@ export class Page extends BasePage {
52
54
  later.dataSetter({destination_path: '.size', value: {small: true, large: false}});
53
55
  later.dataSetter({destination_path: '.gift', value: false});
54
56
  later.dataSetter({destination_path: '.live', value: false});
57
+
58
+ // Return here after c09/c11: the original local stage above remains independent.
59
+ const quotes = pane.section({class: 'card stack'});
60
+ quotes.h2('0.2.14 · Ask the server for a quote');
61
+ quotes.p('The local total follows edits. The server quote changes only after a request.');
62
+ quotes.button('Request quote', {type: 'button', id: 'request-quote'}).dataRpc({
63
+ method: 'quote', result_path: '.quote', quantity: '=.quantity',
64
+ large: '=.size.large', gift: '=.gift',
65
+ _onResult: "this.SET('.quote_error', null)",
66
+ _onError: "this.SET('.quote_error', error.code + ': ' + error.message)"});
67
+ const facts = quotes.dl();
68
+ for (const [label, key, elementId] of [['Server quote', 'quote', 'quote'],
69
+ ['Request error', 'quote_error', 'quote-error']]) {
70
+ facts.dt(label); facts.dd(`^.${key}`, {id: elementId});
71
+ }
72
+ quotes.p('Try quantity 0: the server refuses it and the previous quote remains. Then use 3 and request again.',
73
+ {class: 'muted'});
74
+ }
75
+
76
+ quote({quantity, large, gift}) {
77
+ if (quantity === null || quantity < 1) throw new RangeError('Choose at least one item');
78
+ return quantity * (large ? 3 : 1) + (gift ? 5 : 0);
55
79
  }
56
80
  }
81
+ Page.registerEndpoint('quote');
57
82
 
58
83
  /** Logic of 08_end_to_end.py and 08_end_to_end.js: the named logic of the story. */
59
84
  export class Logic {
@@ -1,13 +1,142 @@
1
1
  # 08 · End-to-end story
2
2
 
3
- Every piece of the controllers family on one page, in Python and JavaScript with one `Logic` class in `08_end_to_end.js`.
3
+ A small order form combines fields, a total and an SVG preview. Start with the **0.2.0 local part**, where bindings and controllers update the page.
4
4
 
5
- 1. **First render.** The `dataSetter` nodes sit after some controls and in the later branch; `.settings.caption` is written twice and the later setter wins. The quantity has no setter and starts from its `default`. The first DOM already shows every final value.
6
- 2. **Editing.** The caption field writes at `change`; after checking `live` it writes at every keystroke.
7
- 3. **Formula and controller.** The formulas compute the total and the circle's radius; the named controller `paint` colors the circle.
8
- 4. **Radio.** Choosing Large writes `size.large` true and `size.small` false.
9
- 5. **Button.** The nested named controller `press` runs once per click, with `button_counter` and `button_shift`.
10
- 6. **Freeze.** Freeze the later branch, change the quantity and remove a note, then thaw once: the removal appears at the thaw.
11
- 7. **Close.** Closing the page disposes the Gramlot instance: providers, timers, listeners and pending requests stop.
5
+ After **Endpoint** and **Call on click**, return to the **0.2.14 quote section**. It sends the same inputs only when you request a quote; a failed request keeps the previous quote.
12
6
 
13
- **Try it:** Walk the seven steps in order, then reload and walk them in another order.
7
+ **Uses:** Local binding and controllers first; endpoint calls with `dataRpc` later. The module layout is from 0.2.5. No order persistence.
8
+
9
+ <details>
10
+ <summary>More detail</summary>
11
+
12
+
13
+ **Release stage:** first the local core 0.2.0 story, then a separately labelled core 0.2.14 endpoint extension. The maintained Page/Logic module arrangement is from 0.2.5; it does not move the original local behavior into that release.
14
+
15
+ **Goal:** Explain the local page first, then revisit the same inputs to request a server result and handle its failure.
16
+
17
+ **Before this lesson:** [b03 · Setters and defaults](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/03_setters_and_defaults.md), [b05 · Checkbox and radio](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/05_checkbox_radio.md), [b07 · Bound SVG](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/07_bound_svg.md), [b08 · Freeze](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/08_freeze.md), [c01 · Formula](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/01_formula.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). These prerequisites support the local stage. Return for the 0.2.14 section only after [c09 · Endpoint](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/09_endpoint.md) and [c11 · Call on click](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/11_call_on_click.md).
18
+
19
+ **Open and edit:** select **c08 · End-to-end story** 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. This lesson executes authored inline JavaScript and needs the gallery’s permissive CSP profile; a strict named-Logic starter needs the corresponding named form.
20
+
21
+ ## Read the page
22
+
23
+ This is the **local 0.2.0 story**, with the maintained 0.2.5 Logic-module arrangement. Setters initialize caption, radio flags and checkboxes; a default initializes quantity. Formulas derive total/radius and the named `paint` controller selects color. Named button controllers handle press and freeze.
24
+
25
+ ```python
26
+ pane.dataFormula(".total", "quantity * (large ? 3 : 1) + (gift ? 5 : 0)",
27
+ quantity="^.quantity", large="^.size.large", gift="^.gift", _init=True)
28
+ ```
29
+
30
+ The later caption setter wins before render. It does not arrive late over a network. The SVG and freeze sections are optional local depth: neither is required for the later server request. Leave the separately labelled quote section alone during this first pass; it sends no request at startup or when fields change.
31
+
32
+ Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Bindings, actions and any Logic execute in the browser; Python strings containing JavaScript are not executed by Python. Read the gallery’s **logic.js** tab (the Logic class from `08_end_to_end.js`) beside the Python source.
33
+
34
+ ## Try it and predict the result
35
+
36
+ First pass: core 0.2.0, using only the local sections.
37
+
38
+ 1. Initially expect “The story of a page”, quantity 2, Small, no gift, total 2 and radius 28.
39
+ 2. Choose Large, then Gift wrap: total becomes 11, radius 64, and paint makes the circle red because total is greater than 10.
40
+ 3. Press with Shift: “Pressed 1 times” and “with Shift”. Freeze, remove a note, then thaw: the list goes from two visible notes to one at thaw.
41
+
42
+ ## Change it and check yourself
43
+
44
+ Reload and change quantity to 3, leaving the field, while Small and no gift remain selected.
45
+
46
+ **Answer:** total 3 and radius 32. Then select Large: total 9 and radius 56. Explain the chain input → Data → formula → SVG pointer; there is no explicit circle-update routine.
47
+
48
+ ## Return after c09/c11: the 0.2.14 server step
49
+
50
+ Reload before this trial. The first stage remains above: the same form owns
51
+ quantity, size and gift, and the local formula still drives total/radius/color.
52
+ The new section declares a button and a separate result/error display:
53
+
54
+ ```python
55
+ quotes.button("Request quote", type="button", id="request-quote").dataRpc(
56
+ "quote", result_path=".quote", quantity="=.quantity",
57
+ large="=.size.large", gift="=.gift",
58
+ _onResult="this.SET('.quote_error', null)",
59
+ _onError="this.SET('.quote_error', error.code + ': ' + error.message)")
60
+ ```
61
+
62
+ `quote` is an endpoint method on Page, not on browser Logic. Python uses
63
+ `@endpoint`; JS uses `Page.registerEndpoint('quote')`. It rejects null or a
64
+ quantity below one, then returns a number. The host executes this method (in the
65
+ standalone JS environment, the Worker is that host). It does not save an order.
66
+
67
+ The `=` arguments are read when the button requests a quote. They do not trigger
68
+ requests on edits and are not frozen copies of the initial values. `result_path`
69
+ writes `.quote`; the display follows `^.quote`. A successful response clears the
70
+ previous error through `_onResult`. A failure writes `.quote_error` through
71
+ `_onError` and leaves the last successful quote unchanged. **Server quote is a
72
+ snapshot, not a live promise for the currently edited fields.**
73
+
74
+ ### Predict, request, fail and recover
75
+
76
+ 1. After reload, local total is 2 and Server quote/Request error are blank.
77
+ No `quote` endpoint call has run.
78
+ 2. Request quote: expect 2. Choose Large and Gift wrap: local total becomes 11,
79
+ but Server quote stays 2. Request again: it becomes 11.
80
+ 3. Set Quantity to 0, leave the field and request: local total is 5, the server
81
+ displays `application_error: Choose at least one item`, and its last quote
82
+ remains 11. Clearing Quantity also sends null and is refused by this endpoint.
83
+ 4. Set Quantity to 3, leave the field and request: expect quote 14 and an empty
84
+ error. The existing SVG and local controls continue working throughout.
85
+
86
+ If inspecting network traffic, count only `contentType: data`, `name: quote`:
87
+ initial Source loading is a different call. In a Worker environment the request
88
+ uses its transport, not necessarily HTTP.
89
+
90
+ ### Independent source-change exercise
91
+
92
+ Add a fixed delivery charge of 4 to the **endpoint's returned number only**:
93
+
94
+ ```python
95
+ return quantity * (3 if large else 1) + (5 if gift else 0) + 4
96
+ ```
97
+
98
+ In the JS equivalent, add `+ 4` to `quote`'s return expression. Keep the local
99
+ formula, the request declaration and the bound display unchanged. Reload, choose
100
+ Large and Gift wrap at quantity 2, then request.
101
+
102
+ **Answer:** local total stays 11; Server quote becomes 15. No new event listener,
103
+ request serialization or result-copying controller is needed. Quantity 0 is still
104
+ refused and preserves 15. Restore the original endpoint after the exercise.
105
+ The two calculations are deliberately retained to compare client preview and
106
+ host result, not offered as a shared production pricing implementation.
107
+
108
+ ## The same example across stages
109
+
110
+ | Stage | What runs | What the learner authors |
111
+ | --- | --- | --- |
112
+ | 0.2.0 local story | Browser bindings, formula, named paint/button Logic | One local calculation; pointers reuse it for text and SVG. No server quote exists at this stage. |
113
+ | 0.2.5 maintained module layout | Named browser Logic beside the Python Page | A shared Logic module for both language pages; this changes organization, not the earlier local result. |
114
+ | 0.2.14 extension | A Page endpoint when the new button runs | Endpoint rule, `dataRpc`, result pointer and small error/success callbacks. Gramlot owns transport and result assignment. |
115
+
116
+ Preserving the earlier stage makes the added responsibility visible. The later
117
+ page has more total code because it does more; the simplification is that calling
118
+ the host does not require application request plumbing or manual synchronization
119
+ of the result display. The endpoint does not replace a perfectly adequate local
120
+ formula just to make the example shorter.
121
+
122
+ At **0.2.16**, compare [c12 · Remote](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/12_remote.md)
123
+ when the host must construct a Source fragment, or [c14 · Data remote](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/14_data_remote.md)
124
+ when a Data value should load lazily when read. This explicit quote action needs
125
+ a value now, so it uses `dataRpc`; the page does not combine all three mechanisms.
126
+
127
+ ## Limits and next step
128
+
129
+ This is an arithmetic teaching example, not persistence, stock reservation or a
130
+ production order flow. A server Page is fresh per request; the request parameters
131
+ carry the values and the browser Data holds the returned snapshot. There is no
132
+ remote Source or lazy resolver in c08. Inline formulas/callbacks require the stated
133
+ CSP profile. Closing/disposal releases providers/listeners; the lesson does not
134
+ qualify every timing, transport or lifecycle edge case.
135
+
136
+ On the **first local pass**, continue with [c15 · Lifecycle](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/15_lifecycle.md), then projection and Data exchange before c09–c11.
137
+ After returning for the **0.2.14 extension**, continue with c11's separately
138
+ labelled 0.2.15 Source-auth section, then c12–c14 at 0.2.16. The
139
+ [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md)
140
+ provides the full contract. Do not install old packages to perform these stages.
141
+
142
+ </details>
@@ -1,12 +1,52 @@
1
1
  # 09 · Endpoint
2
2
 
3
- An endpoint is a method of the page that the browser calls for a value. `dataRpc(method, result_path, **parameters)` calls it and writes the answer at `result_path`.
3
+ An endpoint receives parameters and returns a value that appears in the page. You learn how `dataRpc` puts the result in Data without writing request and response handling yourself.
4
4
 
5
- - **Declare the endpoint.** Python marks the method with `@endpoint`; JavaScript calls `Page.registerEndpoint('with_vat')` after the class.
6
- - **Arguments.** The other attributes of the `dataRpc` are the arguments of the call: `price` and `rate`, read here from the Data with `=`. Python receives them as keywords, JavaScript as one object.
7
- - **`**kwargs`.** `dataRpc` sends every attribute the author writes, and Python rejects a keyword the method does not declare. A Python endpoint called from `dataRpc` declares `**kwargs`.
8
- - **`_init=True`** calls the endpoint once when the page starts, after the setters. The total is Data: the `dd` shows it through `^.gross`.
5
+ **Uses:** Data binding and `dataRpc` — core 0.2.14. The host runs the endpoint: Python, Node/Bun or the standalone Worker.
9
6
 
10
- The call travels as one envelope on `POST /gramlot/rpc`; the page writes no `fetch`.
7
+ <details>
8
+ <summary>More detail</summary>
11
9
 
12
- **Try it:** Open the browser network panel and reload: one call to `/gramlot/rpc` carries `with_vat` with price 12.5 and rate 22.
10
+
11
+ **Release stage:** Endpoint calls (core 0.2.14), after the local 0.2.0 lessons.
12
+
13
+ **Goal:** Follow a declarative endpoint call from parameters to Data.
14
+
15
+ **Before this lesson:** [c01 · Formula](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/01_formula.md), [c08 · End-to-end story](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/08_end_to_end.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c09 · Endpoint** 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
+ An endpoint computes a **value**, not Source. Python marks `with_vat` with `@endpoint`; JavaScript calls `Page.registerEndpoint('with_vat')`. `dataRpc` supplies the call and result placement.
22
+
23
+ ```python
24
+ pane.dataRpc("with_vat", result_path=".gross", price="=.price", rate="=.rate", _init=True)
25
+ ```
26
+
27
+ `=` reads values for the initial call without making them triggers. `_init` runs after setters. Python receives keyword arguments and declares `**kwargs` for additional authored arguments; JS receives one object. Each server call creates a fresh Page, so its fields are not session storage.
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. Expect Price 12.5, VAT rate 22 and, after the response, Total 15.25.
34
+ 2. On an HTTP environment, filter Network requests for RPC envelopes with `name: "with_vat"`; bootstrap/main is a separate call. In standalone this uses Worker messages, not HTTP.
35
+
36
+ ## Change it and check yourself
37
+
38
+ Change the price setter to 20 and reload.
39
+
40
+ **Answer:** total 24.4, from `round(20 * 122) / 100`. `result_path` directs the value into Data; the existing `^.gross` display then follows without a separate response-to-DOM handler.
41
+
42
+ ## The same example across stages
43
+
44
+ Revisit c01’s computed gross. There the browser owns the formula; here the Page endpoint owns it and `dataRpc` declares transport/result handling. This adds a server boundary, so it is not a claim of fewer total lines than a local formula. The simplification is removing request-envelope and result-routing code from the application.
45
+
46
+ ## Limits and next step
47
+
48
+ This lesson sends one initial call; it has no live price control or persistence. VAT is a percentage, unlike c01’s fractional rate. Do not infer login or database integration from an endpoint.
49
+
50
+ Continue with [c10 · Call on change](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/10_call_on_change.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
51
+
52
+ </details>
@@ -1,13 +1,53 @@
1
1
  # 10 · Call on change
2
2
 
3
- A `dataRpc` follows the Data like a `dataController`: a `^` argument calls the endpoint again at every change. The endpoint `quote` prices 10 per piece, 8 from ten pieces on.
3
+ A field requests a result when its value changes. You learn how a delay groups nearby edits and how the page distinguishes a pending request from the last received result.
4
4
 
5
- - **`quantity='^.quantity'`** triggers the call when the quantity changes and sends the new value.
6
- - **`_delay=500`** calls 500 ms after the last change, once: typing `12` makes one call, not two.
7
- - **`_onCalling`** is inline code that runs before the call, with the arguments: it writes the status.
8
- - **`_onResult`** runs after the value is written at `result_path`, with the arguments and `result`.
9
- - **Latest call wins.** A new trigger aborts the call in flight and its answer is dropped.
5
+ **Uses:** Data binding and delayed reactive `dataRpc` calls — core 0.2.14.
10
6
 
11
- `_init=True` makes the first call when the page starts. The callbacks are inline code: the page needs a Content Security Policy that allows it.
7
+ <details>
8
+ <summary>More detail</summary>
12
9
 
13
- **Try it:** Type 12 in the quantity and watch the status before and after the answer.
10
+
11
+ **Release stage:** Endpoint calls (core 0.2.14), after the local 0.2.0 lessons.
12
+
13
+ **Goal:** Debounce an endpoint call and distinguish pending from received values.
14
+
15
+ **Before this lesson:** [c02 · Controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/02_controller.md), [c09 · Endpoint](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/09_endpoint.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c10 · Call on change** 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. This lesson executes authored inline JavaScript and needs the gallery’s permissive CSP profile; a strict named-Logic starter needs the corresponding named form.
18
+
19
+ ## Read the page
20
+
21
+ The `quote` endpoint charges 10 each below quantity 10 and 8 each from 10 onward. `quantity="^.quantity"` triggers calls; `_delay=500` waits for a pause. `_onCalling` writes status before sending, `_onResult` after writing `.total`.
22
+
23
+ ```python
24
+ pane.dataRpc("quote", result_path=".total", quantity="^.quantity", _delay=500, _init=True,
25
+ _onCalling="this.SET('.status', 'Asking the server for ' + quantity)",
26
+ _onResult="this.SET('.status', 'Answered ' + result + ' for ' + quantity)")
27
+ ```
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. Wait for “Answered 10 for 1”.
34
+ 2. Replace Quantity with 12, then stop typing. After the pause and response expect total 96 and “Answered 96 for 12”.
35
+ 3. On HTTP inspect `quote` calls; multiple edits inside a single 500 ms quiet window are combined, but edits spaced farther apart can each call.
36
+
37
+ ## Change it and check yourself
38
+
39
+ Try quantities 9 and 10 separately, waiting for each answer.
40
+
41
+ **Answer:** totals 90 and 80. The endpoint’s threshold, not a client display trick, changes the unit price. A slow network can make the Asking status easier to see.
42
+
43
+ ## The same example across stages
44
+
45
+ c02 used `_delay` for a local search-status effect. c10 reuses that declaration for a server computation; no additional application timer, fetch call or separate result-to-Data write is needed. The endpoint calculation remains server code.
46
+
47
+ ## Limits and next step
48
+
49
+ Debounce is not a guarantee of one request for all typing patterns. A new executed call supersedes its in-flight predecessor; delayed scheduling still waits for its window. Callbacks here are inline and need the permitted CSP profile.
50
+
51
+ Continue with [c11 · Call on click](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/11_call_on_click.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
52
+
53
+ </details>
@@ -22,6 +22,12 @@ export class Page extends BasePage {
22
22
  facts.dd(`^.${key}`, {id: key});
23
23
  }
24
24
 
25
+ const auth = pane.section({class: 'card stack'});
26
+ auth.h2('0.2.15: Source authorization versus visibility');
27
+ auth.p('Source-auth marker', {auth: 'admin', id: 'protected-source'});
28
+ auth.p('Hidden marker is still delivered', {hidden: true, id: 'hidden-source'});
29
+ auth.p('Without an authorized identity, the protected paragraph is omitted from Source.');
30
+
25
31
  pane.dataSetter({destination_path: '.item', value: 'Lamp'});
26
32
  }
27
33
 
@@ -1,11 +1,63 @@
1
1
  # 11 · Call on click
2
2
 
3
- A `dataRpc` nested in a `button` is the click mechanism of the button: each click calls the endpoint. A button has one mechanism, as in example 06.
3
+ Buttons call endpoints and display errors. You learn how application failure differs from a refused request; both preserve the previous result. The later **0.2.15** section compares protected Source with merely hidden content. This gallery demonstrates anonymous refusal; the Kajenn guide covers authenticated access.
4
4
 
5
- - **`_onError`** is inline code that runs when the call fails, with the arguments and `error`. `error.code` is the outcome, `error.message` the message of the server, `error.remoteName` the name of the exception (`LookupError` in Python, `Error` in JavaScript).
6
- - **An endpoint that raises.** `reserve` raises: the outcome is `application_error` and nothing is written at `result_path`.
7
- - **A protected endpoint.** `cancel` declares the rule `auth="admin"` (Python `@endpoint(auth="admin")`, JavaScript `Page.registerEndpoint('cancel', {auth: 'admin'})`). A server without the `auth` capability knows no identity: the call answers `not_authenticated` and the method does not run.
5
+ **Uses:** Button-triggered `dataRpc` and endpoint authorization (0.2.14); Source authorization in the later part (0.2.15).
8
6
 
9
- Without `_onError` the failure reaches the browser console as an unhandled rejection.
7
+ <details>
8
+ <summary>More detail</summary>
10
9
 
11
- **Try it:** Press each button and read the error.
10
+
11
+ **Release stage:** Endpoint calls (core 0.2.14), after the local 0.2.0 lessons.
12
+
13
+ **Goal:** Compare application failure and default-deny authorization on the same button pattern.
14
+
15
+ **Before this lesson:** [c06 · Button controller](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/06_button_controller.md), [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). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c11 · Call on click** 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. This lesson executes authored inline JavaScript and needs the gallery’s permissive CSP profile; a strict named-Logic starter needs the corresponding named form.
18
+
19
+ ## Read the page
20
+
21
+ A nested `dataRpc` is the button’s click mechanism. Reserve deliberately raises; Cancel is protected by `auth="admin"`. `_onError` writes error code/message to Data. A failed call does not overwrite `.reserved`.
22
+
23
+ ```python
24
+ reserve.dataRpc("reserve", result_path=".reserved", item="=.item", _onError=SHOW_ERROR)
25
+ ```
26
+
27
+ `error.remoteName` is LookupError in Python and Error in JS. The visible code/message agree even though exception classes differ.
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. Reserve Lamp: expect `application_error: Lamp is out of stock`; Reserved stays empty.
34
+ 2. Cancel in the ordinary anonymous gallery: expect `not_authenticated`; Reserved remains empty and the protected method does not run.
35
+
36
+ ## Change it and check yourself
37
+
38
+ Initialize `.reserved` to `"unchanged"` with a setter and repeat both failures.
39
+
40
+ **Answer:** the existing value remains unchanged; a failed call is not a successful null/false result. The error display changes instead.
41
+
42
+ ## The same example across stages
43
+
44
+ c06 nested a local controller in a button. This 0.2.14 stage nests `dataRpc` instead: dispatch and result/error plumbing belong to the provider. You do not write a local controller solely to construct an HTTP request.
45
+
46
+
47
+ At the end of the 0.2.14 endpoint stage, return to [c08 · End-to-end story](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/08_end_to_end.md) for its separately labelled quote/result/error extension before continuing below.
48
+
49
+ ## Later stage: Source authorization (0.2.15)
50
+
51
+ Keep the 0.2.14 endpoint trial above. The added section demonstrates a later capability on the same page: `auth="admin"` removes a protected paragraph from the returned Source when the environment has no authorized identity. The hidden paragraph remains in Source and DOM with `hidden=True`.
52
+
53
+ On the common FileServer, inspect the main Source response and the DOM. Expect no `protected-source` node and no “Source-auth marker”; expect `hidden-source` to exist despite being invisible. Exercise: in your editable copy remove only the protected paragraph's `auth` attribute. **Answer:** it is now delivered and visible. Restore the rule after the trial. Hiding a node does not protect its content.
54
+
55
+ Authenticated allow/deny and a subsequent anonymous request belong to the [Kajenn authentication walkthrough](https://github.com/gramlot-org/gramlot-py-server/blob/main/docs/505-kajenn.md#gp-505-030), using its existing site identity integration. The walkthrough checks anonymous, refused, allowed and anonymous-again requests; the common gallery does not implement a login system.
56
+
57
+ ## Limits and next step
58
+
59
+ An identity-aware adapter can produce different auth outcomes according to the user’s tags. This page demonstrates default refusal, not login or an authenticated allow/deny matrix. The separately labelled 0.2.15 section demonstrates Source omission; the endpoint buttons remain the earlier 0.2.14 step. Without `_onError`, failures reach the console.
60
+
61
+ Continue with [c12 · Remote](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/12_remote.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
62
+
63
+ </details>
@@ -1,13 +1,60 @@
1
1
  # 12 · Remote
2
2
 
3
- `remote` makes the content of an element a Source fragment that a method of the page builds on the server. The node method `remote(name, **params)` writes it on the element.
3
+ A choice loads a page fragment built by the host. You learn when to request structure with `remote` rather than a value with `dataRpc`. The fragment method stays fixed; its parameters can change.
4
4
 
5
- - **Declare the fragment.** Python marks the method with `@source`; JavaScript calls `Page.registerSource('details')` after the class. The method receives the root of the fragment and the parameters, and populates the root.
6
- - **Parameters.** `record_id='^.record_id'` is a parameter of the call. A `^` parameter requests the fragment again at every change; a `=` parameter is only read.
7
- - **When.** The fragment is requested when the element starts. The authored content, `Waiting for the server`, stays until the first fragment arrives; each fragment replaces the content of the element.
8
- - **`**kwargs`.** A Python fragment method declares `**kwargs`, as a Python endpoint does.
9
- - **Latest wins.** A new request supersedes the one in flight; removing the element cancels its request.
5
+ **Uses:** Data binding and declarative remote Source — core 0.2.16.
10
6
 
11
- The request travels as one envelope on `POST /gramlot/rpc`; the page writes no `fetch`.
7
+ <details>
8
+ <summary>More detail</summary>
12
9
 
13
- **Try it:** Choose another record: the section shows the name and the stock that the server returns for it.
10
+
11
+ **Release stage:** Remote Source and lazy Data (core 0.2.16), after the 0.2.14 endpoint lessons.
12
+
13
+ **Goal:** Choose Source replacement when the server owns the content structure.
14
+
15
+ **Before this lesson:** [c09 · Endpoint](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/09_endpoint.md), [c11 · Call on click](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/11_call_on_click.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c12 · 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
+ The select writes `.record_id`. `remote` belongs to the details element, and its prefixed parameter follows that value. Python `@source` / JS `Page.registerSource('details')` declares a fragment method that populates a root.
22
+
23
+ ```python
24
+ details = pane.section("Waiting for the server", class_="card stack", id="details")
25
+ details.remote("details", record_id="^.record_id")
26
+ ```
27
+
28
+ The fragment contains a heading and stock paragraph. It replaces the element’s **content**, not the containing section. Earlier content remains while a replacement is pending.
29
+
30
+ 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.
31
+
32
+ ## Try it and predict the result
33
+
34
+ 1. After the first response expect Lamp and “3 in stock”.
35
+ 2. Choose Chair: expect Chair and “12 in stock”. Choose Desk: expect “0 in stock”.
36
+ 3. On HTTP inspect `contentType: "source"`, `name: "details"`; no application fetch or DOM insertion code is present.
37
+
38
+ ## Change it and check yourself
39
+
40
+ Change Chair’s stock in `RECORDS` from 12 to 7 and reload, then select Chair.
41
+
42
+ **Answer:** the returned fragment says “7 in stock”. Altering the source constant requires host reload; it is not an editable shared database.
43
+
44
+ ## The same example across stages
45
+
46
+ In 0.2.14, `dataRpc` brought a value into a predeclared display. In 0.2.16, `remote` can ask the Page to construct that display’s content. It removes explicit fragment-request/mount coordination from page code. It is not automatically shorter for every problem: keep `dataRpc` for a value when the client already owns the structure.
47
+
48
+ ## Advanced practice
49
+
50
+ After the basic trial, select an invalid record by editing an option’s value in your **source file** and reloading. First mount a valid record, then choose the invalid one. Expect the prior content to remain and a console failure. Restore the option afterwards. Do not invent an error callback on remote or reinterpret an empty method name: that lifecycle question remains undecided.
51
+
52
+ For nested fragments, the owning method can declare another `.remote(...)` in the returned Source. Each fragment has its own lifetime; this page does not claim to be a complete nested-fragment exercise.
53
+
54
+ ## Limits and next step
55
+
56
+ A fragment is typed Source, not an endpoint returning HTML text. New requests supersede previous ones; removing the owning element cancels its request. A failure retains already mounted content and is reported to the console; remote has no dataRpc-style `_onError` callback.
57
+
58
+ Continue with [c13 · Cached remote](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/13_cached_remote.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
59
+
60
+ </details>
@@ -6,7 +6,7 @@ const PRODUCTS = {
6
6
  chair: {name: 'Chair', category: 'seating'},
7
7
  };
8
8
  const RULES = {lighting: 'Ships in two days, bulbs not included.', seating: 'Ships in a week, assembly required.'};
9
- const ON_REMOTE = "this.SET('requests', (this.GET('requests') ?? 0) + 1); this.SET('status', 'Mounted the ' + category + ' rules')";
9
+ const ON_REMOTE = "this.SET('mounts', (this.GET('mounts') ?? 0) + 1); this.SET('status', 'Mounted the ' + category + ' rules')";
10
10
 
11
11
  export class Page extends BasePage {
12
12
  static title = 'Cached remote';
@@ -25,7 +25,7 @@ export class Page extends BasePage {
25
25
  rules.remote('rules', {category: '^.category', _cachedRemote: true, _onRemote: ON_REMOTE});
26
26
 
27
27
  const facts = pane.dl();
28
- for (const [label, key] of [['Requests', 'requests'], ['Status', 'status']]) {
28
+ for (const [label, key] of [['Mounted fragments', 'mounts'], ['Status', 'status']]) {
29
29
  facts.dt(label);
30
30
  facts.dd(`^${key}`, {id: key});
31
31
  }
@@ -1,12 +1,67 @@
1
1
  # 13 · Cached remote
2
2
 
3
- The control attributes of `remote` act on the requests of the element. The `article` has a variable datapath, as in binding example 02; the rules fragment depends only on the category of the product.
3
+ Revisit **Remote** with caching. Request the same content again and watch the mounted-fragment counter: you learn how unchanged inputs can reuse the current fragment. The counter measures mounts, not network calls.
4
4
 
5
- - **A change of the Data context** requests the fragment again: choosing another product moves the datapath of the `article`.
6
- - **`_cachedRemote=True`** makes no request when the parameters equal those of the last request. Lamp and Spotlight share the category `lighting`: switching between them makes no request.
7
- - **`_onRemote`** is inline code that runs after the fragment is mounted, with the parameters as locals: here it counts the mounted fragments and writes the status.
8
- - **`_if` and `_else`** are the other control attributes: a false `_if` makes no request and runs `_else`.
5
+ **Uses:** Data binding and cached remote Source — core 0.2.16. Advanced cache/lifetime trials need a qualified runtime.
9
6
 
10
- The callbacks are inline code: the page needs a Content Security Policy that allows it.
7
+ <details>
8
+ <summary>More detail</summary>
11
9
 
12
- **Try it:** Choose Spotlight, then Chair. Requests stays at 1 for Spotlight and becomes 2 for Chair.
10
+
11
+ **Release stage:** Remote Source and lazy Data (core 0.2.16), after the 0.2.14 endpoint lessons.
12
+
13
+ **Goal:** Reuse the last mounted fragment when the relevant inputs stay the same.
14
+
15
+ **Before this lesson:** [b02 · Variable datapath](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/02_variable_datapath.md), [c12 · Remote](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/12_remote.md). These prerequisites support the release sequence; later comparisons are explicitly labelled below.
16
+
17
+ **Open and edit:** select **c13 · Cached 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. This lesson executes authored inline JavaScript and needs the gallery’s permissive CSP profile; a strict named-Logic starter needs the corresponding named form.
18
+
19
+ ## Read the page
20
+
21
+ The product card uses b02’s variable datapath. The fragment depends on **category**, not product name: Lamp and Spotlight are both lighting. `_onRemote` runs after a successful mount.
22
+
23
+ ```python
24
+ rules.remote("rules", category="^.category", _cachedRemote=True, _onRemote=ON_REMOTE)
25
+ ```
26
+
27
+ **Mounted fragments** counts successful mounts, not requests sent. A pending, failed or cancelled call may send traffic without incrementing it. To verify a skipped HTTP call, inspect Network envelopes with `contentType: "source"` and `name: "rules"`; do not count bootstrap/main. Standalone uses Worker messages instead.
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. Wait for Lamp’s lighting rules: Mounted fragments = 1.
34
+ 2. Choose Spotlight: the product name changes, rules stay the same, mounts remain 1, and no additional rules call should be sent.
35
+ 3. Choose Chair and wait: mounts become 2 and seating rules appear. Return to Lamp and wait: mounts become 3. This is one last-result cache, not a dictionary retaining all categories.
36
+
37
+ ## Change it and check yourself
38
+
39
+ Remove only `_cachedRemote=True` from the declaration, reload, wait for Lamp, then choose Spotlight.
40
+
41
+ **Answer:** the same visible rules are requested/mounted again and the count becomes 2. Restore the attribute afterwards. Compare transport observations separately from the mount count.
42
+
43
+ ## The same example across stages
44
+
45
+ c12 declared reactive Source replacement. Here one `_cachedRemote=True` declaration removes the need for page code to remember which inputs produced the currently displayed fragment and decide whether to call again. Product context may change while the fragment’s relevant inputs remain equal.
46
+
47
+ The corrected identity is the method name plus the **TYTX snapshot of parameters at dispatch of the last successfully mounted response**. Failure does not replace that identity. Mutating a Bag/object in place can change the next snapshot even though its object identity stays the same. Reordering object keys may produce a different snapshot and an extra call; no order-insensitive normalization is promised.
48
+
49
+ ## Advanced practice
50
+
51
+ **Release history:** core 0.2.17 includes the corrected cache identity and cancellation behavior. Update an older runtime before trying these advanced cases.
52
+
53
+ Run these separately after the basic trial, using that core cache correction. Reload between source edits so each trial has a known starting state.
54
+
55
+ 1. **Delayed A → B → A.** Let Lamp/lighting (A) finish mounting. Slow responses in the HTTP environment, choose Chair/seating (B), then Lamp before B finishes. Expected: B’s pending call is cancelled/superseded; the already mounted lighting fragment remains; mounts stay 1. A cache hit must not allow a late B response to overwrite A. Observe the B request independently: the mount counter alone cannot establish cancellation.
56
+ 2. **Mutable parameters.** Add `pane.dataSetter("context", {"revision": 1})`, an input bound to `^context.revision`, and `context="=context"` to the remote declaration. After Lamp mounts, change revision to 2 and leave the field, then choose Spotlight. Expected: the `=` parameter alone is not a trigger, but the subsequent context switch reads the modified Bag and sends a new rules call. The fragment method already ignores unused keyword/object fields; the visible text stays the same and mounts become 2. This is a snapshot comparison, not a same-object comparison.
57
+
58
+ For `_if`, add a boolean Data checkbox and `enabled="^enabled", _if="enabled"` to the declaration in a separate copy. After one mount, disabling it retains the content and suppresses new calls; enable it to resume. Test request lifetime through observation, not by adding networking or cache state to the application.
59
+
60
+ ## Limits and next step
61
+
62
+ `_onRemote` and conditional code require the inline CSP profile. A false `_if` suppresses the call and can run `_else`; it also cancels a pending request. The method name is fixed: `remote("rules", category="^.category", ...)` varies the category while always calling `rules`. A Data pointer such as `remote("^method", ...)` is outside the contract and is correctly rejected during Source construction. A variable method would require a future explicit owner decision; it is not a missing exercise or capability to enable. Live Source-attribute-mutation semantics remain outside this lesson.
63
+
64
+
65
+ Continue with [c14 · Data remote](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/controllers/14_data_remote.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
66
+
67
+ </details>