@gramlot/gramlot-examples 0.2.3 → 0.2.5

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 (51) hide show
  1. package/README.md +4 -1
  2. package/package.json +2 -2
  3. package/src/gramlot_examples/catalog.json +2 -2
  4. package/src/gramlot_examples/gallery/dist/gallery.js +12 -1
  5. package/src/gramlot_examples/gallery/gallery-page.js +1 -1
  6. package/src/gramlot_examples/gallery/page.js +6 -3
  7. package/src/gramlot_examples/index.js +6 -5
  8. package/src/gramlot_examples/pages/README.md +5 -4
  9. package/src/gramlot_examples/pages/binding/01_pointers.js +3 -0
  10. package/src/gramlot_examples/pages/binding/02_variable_datapath.js +3 -0
  11. package/src/gramlot_examples/pages/binding/03_setters_and_defaults.js +3 -0
  12. package/src/gramlot_examples/pages/binding/04_native_editing.js +3 -0
  13. package/src/gramlot_examples/pages/binding/05_checkbox_radio.js +3 -0
  14. package/src/gramlot_examples/pages/binding/06_style_and_visibility.js +3 -0
  15. package/src/gramlot_examples/pages/binding/07_bound_svg.js +3 -0
  16. package/src/gramlot_examples/pages/binding/08_freeze.js +27 -0
  17. package/src/gramlot_examples/pages/binding/08_freeze.md +1 -1
  18. package/src/gramlot_examples/pages/binding/09_forms.js +3 -0
  19. package/src/gramlot_examples/pages/binding/10_svg_shapes.js +3 -0
  20. package/src/gramlot_examples/pages/binding/11_complete_page.js +3 -0
  21. package/src/gramlot_examples/pages/controllers/01_formula.js +3 -0
  22. package/src/gramlot_examples/pages/controllers/02_controller.js +3 -0
  23. package/src/gramlot_examples/pages/controllers/03_named_logic.js +21 -1
  24. package/src/gramlot_examples/pages/controllers/03_named_logic.md +1 -1
  25. package/src/gramlot_examples/pages/controllers/04_inline_expressions.js +3 -0
  26. package/src/gramlot_examples/pages/controllers/05_node_methods.js +3 -0
  27. package/src/gramlot_examples/pages/controllers/06_button_controller.js +3 -0
  28. package/src/gramlot_examples/pages/controllers/07_events.js +3 -0
  29. package/src/gramlot_examples/pages/controllers/07_events.md +1 -1
  30. package/src/gramlot_examples/pages/controllers/08_remote_source.js +9 -0
  31. package/src/gramlot_examples/pages/controllers/08_remote_source.md +1 -1
  32. package/src/gramlot_examples/pages/controllers/09_end_to_end.js +36 -0
  33. package/src/gramlot_examples/pages/controllers/09_end_to_end.md +1 -1
  34. package/src/gramlot_examples/pages/controllers/README.md +4 -4
  35. package/src/gramlot_examples/pages/html_svg/01_hello_world.js +3 -0
  36. package/src/gramlot_examples/pages/html_svg/02_text_and_links.js +3 -0
  37. package/src/gramlot_examples/pages/html_svg/03_lists.js +3 -0
  38. package/src/gramlot_examples/pages/html_svg/04_semantic_page.js +3 -0
  39. package/src/gramlot_examples/pages/html_svg/05_tables.js +3 -0
  40. package/src/gramlot_examples/pages/html_svg/06_forms.js +3 -0
  41. package/src/gramlot_examples/pages/html_svg/07_native_disclosure.js +3 -0
  42. package/src/gramlot_examples/pages/html_svg/08_svg_shapes.js +3 -0
  43. package/src/gramlot_examples/pages/html_svg/09_svg_composition.js +3 -0
  44. package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.js +3 -0
  45. package/src/gramlot_examples/pages/html_svg/11_static_report.js +3 -0
  46. package/src/gramlot_examples/pages/html_svg/12_complete_page.js +3 -0
  47. package/src/gramlot_examples/pages/html_svg/13_live_source.js +3 -0
  48. package/src/gramlot_examples/pages/binding/08_freeze_aux.js +0 -26
  49. package/src/gramlot_examples/pages/controllers/03_named_logic_aux.js +0 -19
  50. package/src/gramlot_examples/pages/controllers/08_remote_source_aux.js +0 -8
  51. package/src/gramlot_examples/pages/controllers/09_end_to_end_aux.js +0 -35
package/README.md CHANGED
@@ -21,9 +21,12 @@ same-name README:
21
21
 
22
22
  - `html_svg/`: thirteen HTML and SVG pages, without binding (`e01`–`e13`);
23
23
  - `binding/`: eleven pages whose DOM follows the Data (`b01`–`b11`);
24
- - `controllers/`: nine pages with formulas, controllers, logic companions, events and
24
+ - `controllers/`: nine pages with formulas, controllers, named logic, events and
25
25
  remote Source (`c01`–`c09`).
26
26
 
27
+ Each `NN_name.js` exports `Page` and `Logic`; the Python page `NN_name.py` takes its
28
+ `Logic` from the module beside it.
29
+
27
30
  ## Gallery
28
31
 
29
32
  The gallery page lists the families and opens each example beside its README and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramlot/gramlot-examples",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Gramlot example pages, the gallery page and its catalogue.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -14,7 +14,7 @@
14
14
  "prepack": "node scripts/build-browser.mjs"
15
15
  },
16
16
  "dependencies": {
17
- "@gramlot/gramlot": ">=0.2.2"
17
+ "@gramlot/gramlot": ">=0.2.5"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@genrojs/bag": ">=0.10.1",
@@ -202,8 +202,8 @@
202
202
  "key": "c03",
203
203
  "title": "Named logic",
204
204
  "folder": "03_named_logic",
205
- "description": "Formula and controller bodies in the companion 03_named_logic_aux.js.",
206
- "explanation": "The companion exports class Logic. func names one of its methods; the method receives the resolved parameters, and a controller also its node. Named logic runs under a strict CSP."
205
+ "description": "Formula and controller bodies in class Logic of 03_named_logic.js.",
206
+ "explanation": "The page module exports class Logic beside Page. func names one of its methods; the method receives the resolved parameters, and a controller also its node. Named logic runs under a strict CSP."
207
207
  },
208
208
  {
209
209
  "key": "c04",
@@ -14387,6 +14387,15 @@
14387
14387
  if (!node.resolver && value instanceof Bag) yield* serializationNodes(value, path);
14388
14388
  }
14389
14389
  }
14390
+ function xmlDateText(value) {
14391
+ const iso = value.toISOString();
14392
+ const isEpochDate = value.getUTCFullYear() === 1970 && value.getUTCMonth() === 0 && value.getUTCDate() === 1;
14393
+ const isMidnight = value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0 && value.getUTCMilliseconds() === 0;
14394
+ if (isMidnight && !isEpochDate) return iso.slice(0, iso.indexOf("T"));
14395
+ const timestamp = iso.replace(/\.000Z$/, "Z");
14396
+ if (isEpochDate && !isMidnight) return timestamp.slice(timestamp.indexOf("T") + 1);
14397
+ return timestamp;
14398
+ }
14390
14399
  var Bag = class _Bag {
14391
14400
  static tytxSuffix = "X";
14392
14401
  /**
@@ -16147,6 +16156,7 @@
16147
16156
  * Serialize Bag to XML format.
16148
16157
  *
16149
16158
  * All values are converted to strings without type information.
16159
+ * Date values use ISO 8601 text in UTC (date, time, or datetime).
16150
16160
  * For type-preserving serialization, use toTytx() instead.
16151
16161
  *
16152
16162
  * @param {Object} [options={}] - Serialization options.
@@ -16224,7 +16234,8 @@ ${" ".repeat(depth)}</${tag2}>`;
16224
16234
  }
16225
16235
  return `<${tag2}${attrsStr}></${tag2}>`;
16226
16236
  }
16227
- const text2 = this._escapeXml(String(value));
16237
+ const scalar = value instanceof Date && !Number.isNaN(value.getTime()) ? xmlDateText(value) : String(value);
16238
+ const text2 = this._escapeXml(scalar);
16228
16239
  return `<${tag2}${attrsStr}>${text2}</${tag2}>`;
16229
16240
  }
16230
16241
  /**
@@ -89,7 +89,7 @@ export class GalleryPage extends GramlotPage {
89
89
  codepane.pre().code(example.source, {class: 'language-javascript',
90
90
  id: `code-${key}`});
91
91
  if (example.logic != null) {
92
- codepane.h3(`Companion · ${example.folder}_aux.js`);
92
+ codepane.h3(`Logic · ${example.folder}_aux.js`);
93
93
  codepane.pre().code(example.logic, {class: 'language-javascript',
94
94
  id: `logic-${key}`});
95
95
  }
@@ -1,5 +1,5 @@
1
1
  /** Node integration: provide original example text to the browser-safe gallery UI. */
2
- import {existsSync, readFileSync} from 'node:fs';
2
+ import {readFileSync} from 'node:fs';
3
3
  import {join} from 'node:path';
4
4
  import {buildGallery} from '../index.js';
5
5
  import {GalleryPage} from './gallery-page.js';
@@ -10,7 +10,8 @@ export class Page extends GalleryPage {
10
10
  static catalogs = [];
11
11
 
12
12
  static get families() {
13
- return buildGallery({catalogs: this.catalogs}).families.map(({key, title, path, examples}) => {
13
+ const {families, routes} = buildGallery({catalogs: this.catalogs});
14
+ return families.map(({key, title, path, examples}) => {
14
15
  const read = name => readFileSync(join(path, name), 'utf8');
15
16
  return {
16
17
  key, title, readme: read('README.md'),
@@ -18,7 +19,9 @@ export class Page extends GalleryPage {
18
19
  key: example.key, title: example.title, folder: example.folder, frameUrl: example.key,
19
20
  readme: read(`${example.folder}.md`),
20
21
  source: read(`${example.folder}.js`),
21
- logic: existsSync(join(path, `${example.folder}_aux.js`)) ? read(`${example.folder}_aux.js`) : null,
22
+ // The page module shows its own Logic; only a companion NN_name_aux.js is shown apart.
23
+ logic: routes[example.key].logic === routes[example.key].page
24
+ ? null : readFileSync(routes[example.key].logic, 'utf8'),
22
25
  })),
23
26
  };
24
27
  });
@@ -35,8 +35,10 @@ function readCatalog(catalog, pages, common) {
35
35
  * Return the families, routes and static assets of the common gallery plus `catalogs`.
36
36
  *
37
37
  * Each item of `catalogs` is a pair `[catalog.json, pages folder]` of one environment.
38
- * `routes` maps each key to its JavaScript page and same-name companions; `assets` maps
39
- * each URL to its file and media type. Nothing is written or served here.
38
+ * `routes` maps each key to its JavaScript page, its same-name stylesheet and its logic
39
+ * module: `NN_name_aux.js`, else `NN_name.js`, the page module itself, whose `Logic` export
40
+ * the host takes (the host throws when both hold a `Logic`). `assets` maps each URL to its
41
+ * file and media type. Nothing is written or served here.
40
42
  */
41
43
  export function buildGallery({catalogs = []} = {}) {
42
44
  const families = readCatalog(join(PACKAGE, 'catalog.json'), join(PACKAGE, 'pages'), true);
@@ -66,9 +68,8 @@ export function buildGallery({catalogs = []} = {}) {
66
68
  const page = join(folder, `${example.folder}.js`);
67
69
  if (!existsSync(page)) throw new Error(`Missing page: ${page}`);
68
70
  const stylesheet = join(folder, `${example.folder}.css`);
69
- const logic = join(folder, `${example.folder}_aux.js`);
70
- routes[example.key] = {page, stylesheet: existsSync(stylesheet) ? stylesheet : null,
71
- logic: existsSync(logic) ? logic : null};
71
+ const logic = [join(folder, `${example.folder}_aux.js`), page].find(path => existsSync(path));
72
+ routes[example.key] = {page, stylesheet: existsSync(stylesheet) ? stylesheet : null, logic};
72
73
  for (const suffix of SUFFIXES) {
73
74
  const source = join(folder, `${example.folder}${suffix}`);
74
75
  if (existsSync(source)) assets[`/pages/${family.key}/${basename(source)}`] = {file: source, type: mediaType(source)};
@@ -8,13 +8,14 @@ JavaScript, in three families:
8
8
  editing, booleans, style, SVG and freeze. The last three take the content of
9
9
  `html_svg` examples 06, 08 and 12 and add the binding.
10
10
  - [`controllers/`](controllers/): nine pages with formulas, controllers, named
11
- logic in a companion `NN_name_aux.js`, inline expressions, node methods, buttons,
11
+ logic in the `Logic` class of the page module, inline expressions, node methods, buttons,
12
12
  events, remote Source and the end-to-end story of Gramlot 0.2.0.
13
13
 
14
14
  Each example is a file page: `NN_name.py`, its JavaScript equivalent `NN_name.js`,
15
- its README `NN_name.md`, and where needed a same-name stylesheet `NN_name.css` and a
16
- logic companion `NN_name_aux.js`. The gallery uses only what the minimal `FileHost`
17
- serves: `Page.css`, the same-name companions and the companion's root logic group.
15
+ its README `NN_name.md`, and where needed a same-name stylesheet `NN_name.css`.
16
+ `NN_name.js` exports `Page` and `Logic`: the JavaScript page uses both, the Python
17
+ page takes `Logic` from it. The gallery uses only what the minimal `FileHost`
18
+ serves: `Page.css`, the same-name stylesheet and the root logic group.
18
19
 
19
20
  The pages depend only on the Gramlot core. Shared visual styling is the
20
21
  `gramlot-base` theme shipped in the core package, served by each environment at
@@ -33,3 +33,6 @@ export class Page extends BasePage {
33
33
  pane.dataSetter({destination_path: 'person.name', value: 'Ada Lovelace', role: 'Mathematician'});
34
34
  }
35
35
  }
36
+
37
+ /** The page logic: this page names no methods. */
38
+ export class Logic {}
@@ -31,3 +31,6 @@ export class Page extends BasePage {
31
31
  pane.dataSetter({destination_path: 'current', value: 'people.ada'});
32
32
  }
33
33
  }
34
+
35
+ /** The page logic: this page names no methods. */
36
+ export class Logic {}
@@ -40,3 +40,6 @@ export class Page extends BasePage {
40
40
  later.dataSetter({destination_path: '.shipping', value: {city: 'London', zone: 'EU'}});
41
41
  }
42
42
  }
43
+
44
+ /** The page logic: this page names no methods. */
45
+ export class Logic {}
@@ -51,3 +51,6 @@ export class Page extends BasePage {
51
51
  pane.dataSetter({destination_path: '.topics', value: ['html', 'data']});
52
52
  }
53
53
  }
54
+
55
+ /** The page logic: this page names no methods. */
56
+ export class Logic {}
@@ -39,3 +39,6 @@ export class Page extends BasePage {
39
39
  pane.dataSetter({destination_path: '.plan.plus', value: true});
40
40
  }
41
41
  }
42
+
43
+ /** The page logic: this page names no methods. */
44
+ export class Logic {}
@@ -49,3 +49,6 @@ export class Page extends BasePage {
49
49
  pane.dataSetter({destination_path: '.style', value: 'border: 4px dashed #ffc400'});
50
50
  }
51
51
  }
52
+
53
+ /** The page logic: this page names no methods. */
54
+ export class Logic {}
@@ -31,3 +31,6 @@ export class Page extends BasePage {
31
31
  pane.dataSetter({destination_path: '.bar', value: '#456bc4'});
32
32
  }
33
33
  }
34
+
35
+ /** The page logic: this page names no methods. */
36
+ export class Logic {}
@@ -24,3 +24,30 @@ export class Page extends BasePage {
24
24
  pane.dataSetter({destination_path: '.frozen', value: false});
25
25
  }
26
26
  }
27
+
28
+ /** Logic of 08_freeze.py and 08_freeze.js: each button's nested controller calls one method. */
29
+ export class Logic {
30
+ sourceNode(nodeId) {
31
+ return this.page.source.getNodeByAttr('node_id', nodeId);
32
+ }
33
+
34
+ freeze(node) {
35
+ this.page.renderer.freeze(this.sourceNode('board'));
36
+ node.SET('.frozen', true);
37
+ }
38
+
39
+ add(node) {
40
+ node.SET('.count', node.GET('.count') + 1);
41
+ }
42
+
43
+ removeLast() {
44
+ const items = this.sourceNode('items').value;
45
+ const last = items.getNodes().at(-1);
46
+ if (last) items.popNode(last.label);
47
+ }
48
+
49
+ thaw(node) {
50
+ this.page.renderer.unfreeze(this.sourceNode('board'));
51
+ node.SET('.frozen', false);
52
+ }
53
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  `renderer.freeze(node)` suspends the DOM work of a branch; `renderer.unfreeze(node)` resumes it and builds the current Source of the branch once.
4
4
 
5
- The four buttons call methods of the companion `08_freeze_aux.js`, through a `dataController` nested in each button.
5
+ The four buttons call methods of the `Logic` class in `08_freeze.js`, through a `dataController` nested in each button.
6
6
 
7
7
  - While the board is frozen, **Add one** still changes the counter: the elements already built keep following the Data.
8
8
  - **Remove last item** removes a Source node at once, but its `<li>` stays on screen until the thaw.
@@ -77,3 +77,6 @@ export class Page extends BasePage {
77
77
  parent.input({type: kind, id: key, name: key, value: `^.${key}`, ...attrs});
78
78
  }
79
79
  }
80
+
81
+ /** The page logic: this page names no methods. */
82
+ export class Logic {}
@@ -40,3 +40,6 @@ export class Page extends BasePage {
40
40
  pane.dataSetter({destination_path: '.label', value: 'rectangle · circle · ellipse · line · polyline · polygon'});
41
41
  }
42
42
  }
43
+
44
+ /** The page logic: this page names no methods. */
45
+ export class Logic {}
@@ -125,3 +125,6 @@ export class Page extends BasePage {
125
125
  }
126
126
  }
127
127
  }
128
+
129
+ /** The page logic: this page names no methods. */
130
+ export class Logic {}
@@ -32,3 +32,6 @@ export class Page extends BasePage {
32
32
  pane.dataSetter({destination_path: '.vat', value: 0.2});
33
33
  }
34
34
  }
35
+
36
+ /** The page logic: this page names no methods. */
37
+ export class Logic {}
@@ -42,3 +42,6 @@ export class Page extends BasePage {
42
42
  pane.dataSetter({destination_path: '.interval', value: '1'});
43
43
  }
44
44
  }
45
+
46
+ /** The page logic: this page names no methods. */
47
+ export class Logic {}
@@ -7,7 +7,7 @@ export class Page extends BasePage {
7
7
  main(root) {
8
8
  const pane = root.div({class: 'example-page stack', datapath: 'cart'});
9
9
  pane.h1('Named logic');
10
- pane.p('The formula and the controller name methods of class Logic in 03_named_logic_aux.js.',
10
+ pane.p('The formula and the controller name methods of class Logic in 03_named_logic.js.',
11
11
  {class: 'muted'});
12
12
  const form = pane.form({class: 'card stack'});
13
13
  form.html_label('Price', {for: 'price'});
@@ -26,3 +26,23 @@ export class Page extends BasePage {
26
26
  pane.dataSetter({destination_path: '.price', value: 80});
27
27
  }
28
28
  }
29
+
30
+ /**
31
+ * Logic of 03_named_logic.py and 03_named_logic.js. The minimal FileHost serves this module as the
32
+ * root group of the page logic: `func='finalPrice'` names the method below.
33
+ */
34
+ export class Logic {
35
+ /** A formula method receives the resolved parameters and returns the result. */
36
+ finalPrice(kwargs) {
37
+ const {price, threshold} = kwargs;
38
+ if (price == null) return null;
39
+ return price >= threshold ? Math.round(price * 90) / 100 : price;
40
+ }
41
+
42
+ /** A controller method receives its node and the parameters; `this` is the group. */
43
+ countChange(node, kwargs) {
44
+ this.changes = (this.changes ?? 0) + 1;
45
+ node.SET('.changes', this.changes);
46
+ node.SET('.reason', `${kwargs._reason}: price ${kwargs.price}`);
47
+ }
48
+ }
@@ -1,6 +1,6 @@
1
1
  # 03 · Named logic
2
2
 
3
- The code of this page lives in its companion `03_named_logic_aux.js`, a JavaScript module beside the page that exports `class Logic`. The minimal FileHost serves it for the Python page and for the JavaScript page alike.
3
+ The code of this page lives in `class Logic`, which `03_named_logic.js` exports beside `Page`. The minimal FileHost serves that module as the page logic for the JavaScript page and for the Python page `03_named_logic.py` alike.
4
4
 
5
5
  - `func='finalPrice'` names a method of `Logic`. A formula method receives one object with the resolved parameters and returns the result: 10% off from the `threshold`.
6
6
  - A controller method receives its Source node and the parameters. `node.SET('.changes', …)` writes relative to the node's context.
@@ -36,3 +36,6 @@ export class Page extends BasePage {
36
36
  pane.dataSetter({destination_path: '.started', value: false});
37
37
  }
38
38
  }
39
+
40
+ /** The page logic: this page names no methods. */
41
+ export class Logic {}
@@ -37,3 +37,6 @@ export class Page extends BasePage {
37
37
  pane.dataSetter({destination_path: '.pings', value: 0});
38
38
  }
39
39
  }
40
+
41
+ /** The page logic: this page names no methods. */
42
+ export class Logic {}
@@ -34,3 +34,6 @@ export class Page extends BasePage {
34
34
  pane.dataSetter({destination_path: '.clicks', value: 0});
35
35
  }
36
36
  }
37
+
38
+ /** The page logic: this page names no methods. */
39
+ export class Logic {}
@@ -36,3 +36,6 @@ export class Page extends BasePage {
36
36
  pane.dataSetter({destination_path: '.length', value: 0});
37
37
  }
38
38
  }
39
+
40
+ /** The page logic: this page names no methods. */
41
+ export class Logic {}
@@ -4,7 +4,7 @@
4
4
 
5
5
  - The value is inline code with `this` = the declaring node and the parameter `event`. `this.SET('.path', …)` writes relative to the node's context.
6
6
  - The handler runs with the declaring node also when the event hits a descendant: a click on a chip reaches the section's handler, and `event.target` names the chip.
7
- - A value of dotted names, such as `gui.focused`, is named logic of a `js_requires` group instead. The minimal FileHost serves only the companion root group, so this page uses inline code.
7
+ - A value of dotted names, such as `gui.focused`, is named logic of a `js_requires` group instead. The minimal FileHost serves only the root logic group, so this page uses inline code.
8
8
  - Removing the node removes its listeners.
9
9
 
10
10
  **Try it:** Click the green chip, type a word, then add `connect_onfocus` to the field.
@@ -32,3 +32,12 @@ export class Page extends BasePage {
32
32
  }
33
33
  }
34
34
  source(Page.prototype.details);
35
+
36
+ /** Logic of 08_remote_source.py and 08_remote_source.js. */
37
+ export class Logic {
38
+ /** The button's controller: ask the server for the Source method `details` of this Page. */
39
+ load(node, kwargs) {
40
+ const target = this.page.source.getNodeByAttr('node_id', 'details');
41
+ return this.page.remoteSource(target, 'details', {topic: kwargs.topic});
42
+ }
43
+ }
@@ -3,7 +3,7 @@
3
3
  `gramlot.remoteSource(target, method, params)` asks the host to run a Source method of the Page and replaces the children of `target` with the returned branch.
4
4
 
5
5
  - The Page declares the method with `@source` in Python and `source(Page.prototype.details)` in JavaScript. It builds into its own `root` and runs on the server.
6
- - The button's nested controller names `load` in the companion `08_remote_source_aux.js`. It finds the target by its `node_id` and passes the chosen topic.
6
+ - The button's nested controller names `load` in the `Logic` class of `08_remote_source.js`. It finds the target by its `node_id` and passes the chosen topic.
7
7
  - The new branch carries its own `dataSetter` nodes. They run before the branch is built, so its heading and summary show their values at the first render.
8
8
  - The request lives as long as the target: if the target is removed first, the late answer is ignored. When two requests overlap, the latest wins.
9
9
 
@@ -65,3 +65,39 @@ export class Page extends BasePage {
65
65
  }
66
66
  }
67
67
  source(Page.prototype.extras);
68
+
69
+ /** Logic of 09_end_to_end.py and 09_end_to_end.js: the named logic of the story. */
70
+ export class Logic {
71
+ sourceNode(nodeId) {
72
+ return this.page.source.getNodeByAttr('node_id', nodeId);
73
+ }
74
+
75
+ /** Controller of the total: the circle turns red above 10. */
76
+ paint(node, kwargs) {
77
+ node.SET('.color', kwargs.total > 10 ? '#b13d48' : '#23745b');
78
+ }
79
+
80
+ /** The button's nested controller: one call per click, with its counter and modifiers. */
81
+ press(node, kwargs) {
82
+ node.SET('.presses', `Pressed ${kwargs.button_counter} times`);
83
+ node.SET('.modifiers', kwargs.button_shift ? 'with Shift' : 'without modifiers');
84
+ }
85
+
86
+ loadExtras() {
87
+ return this.page.remoteSource(this.sourceNode('extras'), 'extras');
88
+ }
89
+
90
+ freeze() {
91
+ this.page.renderer.freeze(this.sourceNode('later'));
92
+ }
93
+
94
+ removeNote() {
95
+ const notes = this.sourceNode('notes').value;
96
+ const last = notes.getNodes().at(-1);
97
+ if (last) notes.popNode(last.label);
98
+ }
99
+
100
+ thaw() {
101
+ this.page.renderer.unfreeze(this.sourceNode('later'));
102
+ }
103
+ }
@@ -1,6 +1,6 @@
1
1
  # 09 · End-to-end story
2
2
 
3
- The acceptance page of Gramlot 0.2.0 (unified plan §8.1), in Python and JavaScript with one companion.
3
+ The acceptance page of Gramlot 0.2.0 (unified plan §8.1), in Python and JavaScript with one `Logic` class in `09_end_to_end.js`.
4
4
 
5
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
6
  2. **Editing.** The caption field writes at `change`; after checking `live` it writes at every keystroke.
@@ -2,10 +2,10 @@
2
2
 
3
3
  The pages of this family run code when the Data changes.
4
4
 
5
- `dataFormula` computes a value and writes it to its result path. `dataController` runs code for its effects. 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 in the page companion `NN_name_aux.js`.
5
+ `dataFormula` computes a value and writes it to its result path. `dataController` runs code for its effects. 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
6
 
7
- Inline code needs a Content Security Policy that allows it. Named logic in the companion also runs under a strict policy.
7
+ Inline code needs a Content Security Policy that allows it. Named logic in the `Logic` class also runs under a strict policy.
8
8
 
9
- Start with **Formula** and **Controller**, then move the code to the companion with **Named logic**. The last page is the end-to-end story of Gramlot 0.2.0: every piece of the two families on one page.
9
+ Start with **Formula** and **Controller**, then move the code to the `Logic` class with **Named logic**. The last page is the end-to-end story of Gramlot 0.2.0: every piece of the two families on one page.
10
10
 
11
- Each example has a Python page and its JavaScript equivalent. Both share the same companion.
11
+ Each example has a Python page and its JavaScript equivalent. Both use the `Logic` class of the JavaScript module.
@@ -10,3 +10,6 @@ export class Page extends BasePage {
10
10
  page.p('This heading and paragraph come from a Source tree.');
11
11
  }
12
12
  }
13
+
14
+ /** The page logic: this page names no methods. */
15
+ export class Logic {}
@@ -26,3 +26,6 @@ export class Page extends BasePage {
26
26
  page.a('Jump back to the heading', {href: '#destination'});
27
27
  }
28
28
  }
29
+
30
+ /** The page logic: this page names no methods. */
31
+ export class Logic {}
@@ -27,3 +27,6 @@ export class Page extends BasePage {
27
27
  groups.li('SVG');
28
28
  }
29
29
  }
30
+
31
+ /** The page logic: this page names no methods. */
32
+ export class Logic {}
@@ -23,3 +23,6 @@ export class Page extends BasePage {
23
23
  page.footer().small('Built with Gramlot Source.');
24
24
  }
25
25
  }
26
+
27
+ /** The page logic: this page names no methods. */
28
+ export class Logic {}
@@ -25,3 +25,6 @@ export class Page extends BasePage {
25
25
  for (const value of [ROWS.reduce((n, row) => n + row[1], 0), ROWS.reduce((n, row) => n + row[2], 0), ROWS.reduce((n, row) => n + row[1] + row[2], 0)]) total.td(String(value));
26
26
  }
27
27
  }
28
+
29
+ /** The page logic: this page names no methods. */
30
+ export class Logic {}
@@ -61,3 +61,6 @@ export class Page extends BasePage {
61
61
  parent.input({type: kind, id: key, name: key, ...attrs});
62
62
  }
63
63
  }
64
+
65
+ /** The page logic: this page names no methods. */
66
+ export class Logic {}
@@ -25,3 +25,6 @@ export class Page extends BasePage {
25
25
  measures.p('These numbers are fixed page content; no live progress is implied.', {class: 'muted'});
26
26
  }
27
27
  }
28
+
29
+ /** The page logic: this page names no methods. */
30
+ export class Logic {}
@@ -21,3 +21,6 @@ export class Page extends BasePage {
21
21
  figure.figcaption('The shape attributes are part of the Source tree.');
22
22
  }
23
23
  }
24
+
25
+ /** The page logic: this page names no methods. */
26
+ export class Logic {}
@@ -40,3 +40,6 @@ export class Page extends BasePage {
40
40
  group.circle({cx: 0, cy: 184, r: 4, fill: 'var(--gramlot-brand-yellow)'});
41
41
  }
42
42
  }
43
+
44
+ /** The page logic: this page names no methods. */
45
+ export class Logic {}
@@ -38,3 +38,6 @@ export class Page extends BasePage {
38
38
  }
39
39
  }
40
40
  }
41
+
42
+ /** The page logic: this page names no methods. */
43
+ export class Logic {}
@@ -66,3 +66,6 @@ export class Page extends BasePage {
66
66
  card.p(value, {class: 'report-number'});
67
67
  }
68
68
  }
69
+
70
+ /** The page logic: this page names no methods. */
71
+ export class Logic {}
@@ -118,3 +118,6 @@ export class Page extends BasePage {
118
118
  }
119
119
  }
120
120
  }
121
+
122
+ /** The page logic: this page names no methods. */
123
+ export class Logic {}
@@ -62,3 +62,6 @@ export class Page extends BasePage {
62
62
  motion.p('Removing this section also clears its interval through renderer.onDispose.', {class: 'muted'});
63
63
  }
64
64
  }
65
+
66
+ /** The page logic: this page names no methods. */
67
+ export class Logic {}
@@ -1,26 +0,0 @@
1
- /** Logic of 08_freeze.py and 08_freeze.js: each button's nested controller calls one method. */
2
- export class Logic {
3
- sourceNode(nodeId) {
4
- return this.page.source.getNodeByAttr('node_id', nodeId);
5
- }
6
-
7
- freeze(node) {
8
- this.page.renderer.freeze(this.sourceNode('board'));
9
- node.SET('.frozen', true);
10
- }
11
-
12
- add(node) {
13
- node.SET('.count', node.GET('.count') + 1);
14
- }
15
-
16
- removeLast() {
17
- const items = this.sourceNode('items').value;
18
- const last = items.getNodes().at(-1);
19
- if (last) items.popNode(last.label);
20
- }
21
-
22
- thaw(node) {
23
- this.page.renderer.unfreeze(this.sourceNode('board'));
24
- node.SET('.frozen', false);
25
- }
26
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * Companion of 03_named_logic.py and 03_named_logic.js. The minimal FileHost serves it as the
3
- * root group of the page logic: `func='finalPrice'` names the method below.
4
- */
5
- export class Logic {
6
- /** A formula method receives the resolved parameters and returns the result. */
7
- finalPrice(kwargs) {
8
- const {price, threshold} = kwargs;
9
- if (price == null) return null;
10
- return price >= threshold ? Math.round(price * 90) / 100 : price;
11
- }
12
-
13
- /** A controller method receives its node and the parameters; `this` is the group. */
14
- countChange(node, kwargs) {
15
- this.changes = (this.changes ?? 0) + 1;
16
- node.SET('.changes', this.changes);
17
- node.SET('.reason', `${kwargs._reason}: price ${kwargs.price}`);
18
- }
19
- }
@@ -1,8 +0,0 @@
1
- /** Companion of 08_remote_source.py and 08_remote_source.js. */
2
- export class Logic {
3
- /** The button's controller: ask the server for the Source method `details` of this Page. */
4
- load(node, kwargs) {
5
- const target = this.page.source.getNodeByAttr('node_id', 'details');
6
- return this.page.remoteSource(target, 'details', {topic: kwargs.topic});
7
- }
8
- }
@@ -1,35 +0,0 @@
1
- /** Companion of 09_end_to_end.py and 09_end_to_end.js: the named logic of the story. */
2
- export class Logic {
3
- sourceNode(nodeId) {
4
- return this.page.source.getNodeByAttr('node_id', nodeId);
5
- }
6
-
7
- /** Controller of the total: the circle turns red above 10. */
8
- paint(node, kwargs) {
9
- node.SET('.color', kwargs.total > 10 ? '#b13d48' : '#23745b');
10
- }
11
-
12
- /** The button's nested controller: one call per click, with its counter and modifiers. */
13
- press(node, kwargs) {
14
- node.SET('.presses', `Pressed ${kwargs.button_counter} times`);
15
- node.SET('.modifiers', kwargs.button_shift ? 'with Shift' : 'without modifiers');
16
- }
17
-
18
- loadExtras() {
19
- return this.page.remoteSource(this.sourceNode('extras'), 'extras');
20
- }
21
-
22
- freeze() {
23
- this.page.renderer.freeze(this.sourceNode('later'));
24
- }
25
-
26
- removeNote() {
27
- const notes = this.sourceNode('notes').value;
28
- const last = notes.getNodes().at(-1);
29
- if (last) notes.popNode(last.label);
30
- }
31
-
32
- thaw() {
33
- this.page.renderer.unfreeze(this.sourceNode('later'));
34
- }
35
- }