@gramlot/gramlot-examples 0.2.2

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 (98) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +8 -0
  3. package/README.md +49 -0
  4. package/package.json +52 -0
  5. package/src/gramlot_examples/catalog.json +253 -0
  6. package/src/gramlot_examples/gallery/dist/LICENSE +176 -0
  7. package/src/gramlot_examples/gallery/dist/NOTICE +8 -0
  8. package/src/gramlot_examples/gallery/dist/frame.js +28 -0
  9. package/src/gramlot_examples/gallery/dist/gallery.js +21239 -0
  10. package/src/gramlot_examples/gallery/dist/notices.json +38 -0
  11. package/src/gramlot_examples/gallery/gallery-page.js +97 -0
  12. package/src/gramlot_examples/gallery/gallery.css +110 -0
  13. package/src/gramlot_examples/gallery/page.js +26 -0
  14. package/src/gramlot_examples/index.js +79 -0
  15. package/src/gramlot_examples/pages/README.md +21 -0
  16. package/src/gramlot_examples/pages/binding/01_pointers.js +35 -0
  17. package/src/gramlot_examples/pages/binding/01_pointers.md +14 -0
  18. package/src/gramlot_examples/pages/binding/02_variable_datapath.js +33 -0
  19. package/src/gramlot_examples/pages/binding/02_variable_datapath.md +9 -0
  20. package/src/gramlot_examples/pages/binding/03_setters_and_defaults.js +42 -0
  21. package/src/gramlot_examples/pages/binding/03_setters_and_defaults.md +10 -0
  22. package/src/gramlot_examples/pages/binding/04_native_editing.js +53 -0
  23. package/src/gramlot_examples/pages/binding/04_native_editing.md +11 -0
  24. package/src/gramlot_examples/pages/binding/05_checkbox_radio.js +41 -0
  25. package/src/gramlot_examples/pages/binding/05_checkbox_radio.md +9 -0
  26. package/src/gramlot_examples/pages/binding/06_style_and_visibility.js +51 -0
  27. package/src/gramlot_examples/pages/binding/06_style_and_visibility.md +11 -0
  28. package/src/gramlot_examples/pages/binding/07_bound_svg.js +33 -0
  29. package/src/gramlot_examples/pages/binding/07_bound_svg.md +7 -0
  30. package/src/gramlot_examples/pages/binding/08_freeze.css +1 -0
  31. package/src/gramlot_examples/pages/binding/08_freeze.js +26 -0
  32. package/src/gramlot_examples/pages/binding/08_freeze.md +11 -0
  33. package/src/gramlot_examples/pages/binding/08_freeze_aux.js +26 -0
  34. package/src/gramlot_examples/pages/binding/09_forms.css +8 -0
  35. package/src/gramlot_examples/pages/binding/09_forms.js +79 -0
  36. package/src/gramlot_examples/pages/binding/09_forms.md +12 -0
  37. package/src/gramlot_examples/pages/binding/10_svg_shapes.js +42 -0
  38. package/src/gramlot_examples/pages/binding/10_svg_shapes.md +9 -0
  39. package/src/gramlot_examples/pages/binding/11_complete_page.css +5 -0
  40. package/src/gramlot_examples/pages/binding/11_complete_page.js +127 -0
  41. package/src/gramlot_examples/pages/binding/11_complete_page.md +12 -0
  42. package/src/gramlot_examples/pages/binding/README.md +9 -0
  43. package/src/gramlot_examples/pages/controllers/01_formula.js +34 -0
  44. package/src/gramlot_examples/pages/controllers/01_formula.md +12 -0
  45. package/src/gramlot_examples/pages/controllers/02_controller.js +44 -0
  46. package/src/gramlot_examples/pages/controllers/02_controller.md +10 -0
  47. package/src/gramlot_examples/pages/controllers/03_named_logic.js +28 -0
  48. package/src/gramlot_examples/pages/controllers/03_named_logic.md +12 -0
  49. package/src/gramlot_examples/pages/controllers/03_named_logic_aux.js +19 -0
  50. package/src/gramlot_examples/pages/controllers/04_inline_expressions.js +38 -0
  51. package/src/gramlot_examples/pages/controllers/04_inline_expressions.md +12 -0
  52. package/src/gramlot_examples/pages/controllers/05_node_methods.css +1 -0
  53. package/src/gramlot_examples/pages/controllers/05_node_methods.js +39 -0
  54. package/src/gramlot_examples/pages/controllers/05_node_methods.md +11 -0
  55. package/src/gramlot_examples/pages/controllers/06_button_controller.js +36 -0
  56. package/src/gramlot_examples/pages/controllers/06_button_controller.md +11 -0
  57. package/src/gramlot_examples/pages/controllers/07_events.js +38 -0
  58. package/src/gramlot_examples/pages/controllers/07_events.md +10 -0
  59. package/src/gramlot_examples/pages/controllers/08_remote_source.js +34 -0
  60. package/src/gramlot_examples/pages/controllers/08_remote_source.md +10 -0
  61. package/src/gramlot_examples/pages/controllers/08_remote_source_aux.js +8 -0
  62. package/src/gramlot_examples/pages/controllers/09_end_to_end.css +1 -0
  63. package/src/gramlot_examples/pages/controllers/09_end_to_end.js +67 -0
  64. package/src/gramlot_examples/pages/controllers/09_end_to_end.md +14 -0
  65. package/src/gramlot_examples/pages/controllers/09_end_to_end_aux.js +35 -0
  66. package/src/gramlot_examples/pages/controllers/README.md +11 -0
  67. package/src/gramlot_examples/pages/html_svg/01_hello_world.js +12 -0
  68. package/src/gramlot_examples/pages/html_svg/01_hello_world.md +9 -0
  69. package/src/gramlot_examples/pages/html_svg/02_text_and_links.js +28 -0
  70. package/src/gramlot_examples/pages/html_svg/02_text_and_links.md +9 -0
  71. package/src/gramlot_examples/pages/html_svg/03_lists.js +29 -0
  72. package/src/gramlot_examples/pages/html_svg/03_lists.md +9 -0
  73. package/src/gramlot_examples/pages/html_svg/04_semantic_page.js +25 -0
  74. package/src/gramlot_examples/pages/html_svg/04_semantic_page.md +9 -0
  75. package/src/gramlot_examples/pages/html_svg/05_tables.js +27 -0
  76. package/src/gramlot_examples/pages/html_svg/05_tables.md +9 -0
  77. package/src/gramlot_examples/pages/html_svg/06_forms.css +5 -0
  78. package/src/gramlot_examples/pages/html_svg/06_forms.js +63 -0
  79. package/src/gramlot_examples/pages/html_svg/06_forms.md +9 -0
  80. package/src/gramlot_examples/pages/html_svg/07_native_disclosure.js +27 -0
  81. package/src/gramlot_examples/pages/html_svg/07_native_disclosure.md +9 -0
  82. package/src/gramlot_examples/pages/html_svg/08_svg_shapes.js +23 -0
  83. package/src/gramlot_examples/pages/html_svg/08_svg_shapes.md +9 -0
  84. package/src/gramlot_examples/pages/html_svg/09_svg_composition.js +42 -0
  85. package/src/gramlot_examples/pages/html_svg/09_svg_composition.md +9 -0
  86. package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.css +6 -0
  87. package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.js +40 -0
  88. package/src/gramlot_examples/pages/html_svg/10_cards_with_icons.md +20 -0
  89. package/src/gramlot_examples/pages/html_svg/11_static_report.css +3 -0
  90. package/src/gramlot_examples/pages/html_svg/11_static_report.js +68 -0
  91. package/src/gramlot_examples/pages/html_svg/11_static_report.md +9 -0
  92. package/src/gramlot_examples/pages/html_svg/12_complete_page.css +5 -0
  93. package/src/gramlot_examples/pages/html_svg/12_complete_page.js +120 -0
  94. package/src/gramlot_examples/pages/html_svg/12_complete_page.md +9 -0
  95. package/src/gramlot_examples/pages/html_svg/13_live_source.css +3 -0
  96. package/src/gramlot_examples/pages/html_svg/13_live_source.js +64 -0
  97. package/src/gramlot_examples/pages/html_svg/13_live_source.md +18 -0
  98. package/src/gramlot_examples/pages/html_svg/README.md +10 -0
@@ -0,0 +1,127 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const SESSIONS = [
4
+ ['09:00', 'A first Source tree', 'Mira', 'mira', 'Describe a page with semantic HTML.'],
5
+ ['10:30', 'Drawing with SVG', 'Theo', 'theo', 'Build a small scene from SVG shapes.'],
6
+ ['13:00', 'Putting it together', 'Mira & Theo', null, 'Compose cards, a schedule and a summary.'],
7
+ ];
8
+ const SPEAKERS = [['mira', 'Mira', 'Page authoring'], ['theo', 'Theo', 'SVG composition']];
9
+ const STOPS = [['entrance', 80, 110, 'Entrance'], ['studio', 300, 65, 'Studio'], ['hall', 510, 65, 'Hall']];
10
+ const QUESTIONS = [['Do I need a service?', 'This local example has no remote data.'],
11
+ ['Is the map interactive?', 'The highlighted stop follows the radio buttons above it.']];
12
+
13
+ export class Page extends BasePage {
14
+ static title = 'A complete page with binding';
15
+ static css = ['/themes/gramlot-base/theme.css'];
16
+
17
+ main(root) {
18
+ const shell = root.div({class: 'example-page complete-page', datapath: 'event'});
19
+ this.header(shell);
20
+ const main_content = shell.html_main({class: 'stack'});
21
+ this.hero(main_content);
22
+ this.schedule(main_content);
23
+ this.speakers(main_content);
24
+ this.map(main_content);
25
+ this.faq(main_content);
26
+ shell.footer().small('^.footer');
27
+ shell.dataSetter({destination_path: '.title', value: 'A day of small interfaces'});
28
+ shell.dataSetter({destination_path: '.footer', value: 'Gramlot binding examples · Data-driven content'});
29
+ shell.dataSetter({destination_path: '.show', value: {mira: true, theo: true}});
30
+ shell.dataSetter({destination_path: '.stop', value: {entrance: false, studio: true, hall: false}});
31
+ shell.dataSetter({destination_path: '.faq_open', value: false});
32
+ }
33
+
34
+ header(shell) {
35
+ const header = shell.header({class: 'complete-header'});
36
+ header.p('GRAMLOT FIELD NOTES', {class: 'eyebrow'});
37
+ header.h1('^.title', {id: 'title'});
38
+ header.html_label('Event title', {for: 'title-field'});
39
+ header.input({value: '^.title', live: true, id: 'title-field'});
40
+ const nav = header.nav({aria_label: 'Page sections'});
41
+ for (const [label, target] of [['Schedule', 'schedule'], ['Speakers', 'speakers'], ['Map', 'map'], ['FAQ', 'faq']]) {
42
+ nav.a(label, {href: `#${target}`});
43
+ }
44
+ }
45
+
46
+ hero(main_content) {
47
+ const hero = main_content.section({class: 'card complete-hero', aria_label: 'Illustration'});
48
+ const drawing = hero.svg({viewBox: '0 0 720 180', role: 'img', aria_labelledby: 'complete-art-title', class: 'example-art'});
49
+ drawing.title('Three connected steps', {id: 'complete-art-title'});
50
+ drawing.line({x1: 125, y1: 90, x2: 595, y2: 90, stroke: 'var(--gramlot-border)', stroke_width: 8});
51
+ for (const [x, label, color] of [[125, 'WRITE', 'var(--gramlot-action)'],
52
+ [360, 'COMPOSE', 'var(--gramlot-warning)'],
53
+ [595, 'REVIEW', 'var(--gramlot-success)']]) {
54
+ const group = drawing.g({transform: `translate(${x} 90)`});
55
+ group.circle({cx: 0, cy: 0, r: 47, fill: color});
56
+ group.text(label, {x: 0, y: 6, text_anchor: 'middle', font_size: 15,
57
+ font_weight: 'bold', fill: 'var(--gramlot-background)'});
58
+ }
59
+ hero.p('^.title');
60
+ }
61
+
62
+ schedule(main_content) {
63
+ const section = main_content.section({id: 'schedule', class: 'stack'});
64
+ section.h2('Schedule');
65
+ const filters = section.div();
66
+ for (const [key, name] of SPEAKERS) {
67
+ filters.input({type: 'checkbox', value: `^.show.${key}`, id: `show-${key}`});
68
+ filters.html_label(`Sessions of ${name}`, {for: `show-${key}`});
69
+ }
70
+ const table = section.table();
71
+ table.caption('Three short sessions');
72
+ const heading = table.thead().tr();
73
+ for (const title of ['Time', 'Session', 'Speaker', 'Focus']) heading.th(title, {scope: 'col'});
74
+ const body = table.tbody();
75
+ SESSIONS.forEach(([time, title, speaker, key, focus], index) => {
76
+ const row = key ? body.tr({visible: `^.show.${key}`, id: `session-${index}`}) : body.tr({id: `session-${index}`});
77
+ row.th(time, {scope: 'row'});
78
+ for (const text of [title, speaker, focus]) row.td(text);
79
+ });
80
+ }
81
+
82
+ speakers(main_content) {
83
+ const section = main_content.section({id: 'speakers', class: 'stack'});
84
+ section.h2('Speakers');
85
+ const cards = section.div({class: 'grid complete-grid'});
86
+ for (const [key, name, specialty] of SPEAKERS) {
87
+ const card = cards.article({class: 'card', visible: `^.show.${key}`});
88
+ card.h3(name);
89
+ card.p(specialty);
90
+ }
91
+ }
92
+
93
+ map(main_content) {
94
+ const section = main_content.section({id: 'map', class: 'stack'});
95
+ section.h2('A schematic venue map');
96
+ const choice = section.div();
97
+ for (const [key, , , label] of STOPS) {
98
+ choice.input({type: 'radio', group: 'stop', value: `^.stop.${key}`, id: `stop-${key}`});
99
+ choice.html_label(label, {for: `stop-${key}`});
100
+ }
101
+ const figure = section.figure({class: 'card'});
102
+ const drawing = figure.svg({viewBox: '0 0 600 210', role: 'img', aria_labelledby: 'venue-title', class: 'example-art'});
103
+ drawing.title('Entrance, studio and hall connected by a path', {id: 'venue-title'});
104
+ drawing.path({d: 'M80 110 H300 V65 H510', fill: 'none', stroke: 'var(--gramlot-action)', stroke_width: 8});
105
+ for (const [key, x, y, label] of STOPS) {
106
+ const stop = drawing.g({transform: `translate(${x} ${y})`});
107
+ stop.circle({cx: 0, cy: 0, r: 25, fill: 'none', stroke: 'var(--gramlot-success)', stroke_width: 5,
108
+ visible: `^.stop.${key}`, id: `ring-${key}`});
109
+ stop.circle({cx: 0, cy: 0, r: 15, fill: 'var(--gramlot-warning)'});
110
+ stop.text(label, {x: 0, y: 45, text_anchor: 'middle', font_size: 17, fill: 'currentColor'});
111
+ }
112
+ figure.figcaption('The ring marks the stop chosen above the map.');
113
+ }
114
+
115
+ faq(main_content) {
116
+ const section = main_content.section({id: 'faq', class: 'stack'});
117
+ section.h2('Frequently asked questions');
118
+ const toggle = section.div();
119
+ toggle.input({type: 'checkbox', value: '^.faq_open', id: 'faq-open'});
120
+ toggle.html_label('Open all answers', {for: 'faq-open'});
121
+ for (const [question, answer] of QUESTIONS) {
122
+ const details = section.details({class: 'card', open: '^.faq_open'});
123
+ details.summary(question);
124
+ details.p(answer);
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,12 @@
1
+ # 11 · A complete page with binding
2
+
3
+ The event page of HTML / SVG example 12, with its visible state in the Data record `event`.
4
+
5
+ - The heading, the hero caption and the footer read `^.title` and `^.footer`; the field in the header edits the title while you type.
6
+ - The two checkboxes write `show.mira` and `show.theo`. Schedule rows and speaker cards bind `visible` to them; the joint session has no pointer and stays.
7
+ - The radio buttons above the map own `stop.entrance`, `stop.studio` and `stop.hall`; each map stop shows its ring through `visible`.
8
+ - The FAQ `details` bind `open` to one checkbox.
9
+
10
+ The main landmark is written `html_main`, held in the variable `main_content`, because the page also has a header and a footer.
11
+
12
+ **Try it:** Hide Theo, move to the hall, then open all answers.
@@ -0,0 +1,9 @@
1
+ # Binding
2
+
3
+ The pages of this family keep their content in the Data and let the DOM follow it.
4
+
5
+ A `^` pointer in a text or an attribute reads a Data path and keeps the element up to date. A native control whose `value` is a `^` pointer also writes the Data when you edit it. `dataSetter` puts the initial values in the Data before the first render.
6
+
7
+ Start with **Pointers**, then follow the order of the list: a context that changes, initial values and defaults, editing, booleans, style and SVG, freeze. The last three pages take the content of HTML / SVG examples 06, 08 and 12 and add the binding, so the same page shows the difference.
8
+
9
+ Each example has a Python page and its JavaScript equivalent. Both run on the same packaged runtime.
@@ -0,0 +1,34 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Formula';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'order'});
9
+ pane.h1('Formula');
10
+ const form = pane.form({class: 'card stack'});
11
+ for (const [label, key] of [['Price', 'price'], ['Quantity', 'quantity'], ['VAT rate', 'vat']]) {
12
+ form.html_label(label, {for: key});
13
+ form.input({type: 'number', step: 'any', value: `^.${key}`, live: true, id: key});
14
+ }
15
+
16
+ const result = pane.section({class: 'card stack'});
17
+ result.h2('Results');
18
+ const facts = result.dl();
19
+ for (const [label, key] of [['Total (price × quantity)', 'total'], ['Gross (total × (1 + VAT), = vat)', 'gross'],
20
+ ['Size (_if / _else)', 'size']]) {
21
+ facts.dt(label);
22
+ facts.dd(`^.${key}`, {id: key});
23
+ }
24
+
25
+ pane.dataFormula({result_path: '.total', formula: 'price * quantity', price: '^.price', quantity: '^.quantity', _init: true});
26
+ pane.dataFormula({result_path: '.gross', formula: 'Math.round(total * (1 + vat) * 100) / 100', total: '^.total', vat: '=.vat',
27
+ _init: true});
28
+ pane.dataFormula({result_path: '.size', formula: "'large order'", total: '^.total', _if: 'total > 100', _else: "'small order'",
29
+ _init: true});
30
+ pane.dataSetter({destination_path: '.price', value: 12.5});
31
+ pane.dataSetter({destination_path: '.quantity', value: 4});
32
+ pane.dataSetter({destination_path: '.vat', value: 0.2});
33
+ }
34
+ }
@@ -0,0 +1,12 @@
1
+ # 01 · Formula
2
+
3
+ `dataFormula(result_path, formula, **parameters)` computes an expression and writes the result at `result_path`. The names of the parameters are the names inside the expression.
4
+
5
+ - A `^` parameter triggers the formula when its Data changes: editing the price or the quantity recomputes the total.
6
+ - A `=` parameter is read at each run but does not trigger it. The gross uses `vat='=.vat'`: changing the rate alone does not recompute it; the next change of the total does.
7
+ - `_if` is a condition: when it is false the formula writes the `_else` expression instead.
8
+ - `_init=True` runs each formula once when the page starts, after the setters.
9
+
10
+ A formula result is itself Data: the gross and the size are formulas of the total.
11
+
12
+ **Try it:** Change the VAT rate, then the quantity, and watch when the gross follows.
@@ -0,0 +1,44 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Controller';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'demo'});
9
+ pane.h1('Controller');
10
+
11
+ const check = pane.section({class: 'card stack'});
12
+ check.h2('React to a value');
13
+ check.html_label('Name', {for: 'name'});
14
+ check.input({value: '^.name', live: true, id: 'name'});
15
+ check.p('The name is empty', {id: 'warning', class: 'status status--warning', visible: '^.invalid'});
16
+ check.dataController({script: "this.SET('.invalid', !name.trim())", name: '^.name', _init: true});
17
+
18
+ const fired = pane.section({class: 'card stack'});
19
+ fired.h2('React to a fired path');
20
+ fired.button('Send a request', {fire: '.request', id: 'request'});
21
+ fired.p('^.requests', {id: 'requests'});
22
+ fired.dataController({script: "this.SET('.requests', requests + 1)", _fired: '^.request', requests: '=.requests'});
23
+
24
+ const delayed = pane.section({class: 'card stack'});
25
+ delayed.h2('Wait for a pause (_delay)');
26
+ delayed.html_label('Search', {for: 'query'});
27
+ delayed.input({value: '^.query', live: true, id: 'query'});
28
+ delayed.p('^.searched', {id: 'searched'});
29
+ delayed.dataController({script: "this.SET('.searched', 'Searching for ' + query)", query: '^.query', _delay: 400});
30
+
31
+ const timed = pane.section({class: 'card stack'});
32
+ timed.h2('Run at start and at intervals (_onStart, _timing)');
33
+ timed.html_label('Interval in seconds', {for: 'interval'});
34
+ const interval = timed.select({value: '^.interval', id: 'interval'});
35
+ for (const seconds of [0, 1, 5]) interval.option(String(seconds), {value: String(seconds)});
36
+ timed.p('^.ticks', {id: 'ticks'});
37
+ timed.dataController({script: "this.SET('.ticks', (ticks ?? 0) + 1)", ticks: '=.ticks', _timing: '^.interval',
38
+ _onStart: true});
39
+
40
+ pane.dataSetter({destination_path: '.name', value: 'Ada'});
41
+ pane.dataSetter({destination_path: '.requests', value: 0});
42
+ pane.dataSetter({destination_path: '.interval', value: '1'});
43
+ }
44
+ }
@@ -0,0 +1,10 @@
1
+ # 02 · Controller
2
+
3
+ `dataController(script, **parameters)` runs code for its effects. Inside the script `this` is the controller's Source node: `this.SET('.path', value)` writes the Data relative to its context.
4
+
5
+ - **React to a value.** `name='^.name'` triggers the controller at every change; it writes `.invalid`, and the warning binds `visible` to it. `_init=True` runs it once at the start.
6
+ - **React to a fired path.** The button's `fire='.request'` fires the path at every click, also with an equal value. The controller counts the requests; `requests='=.requests'` is read without triggering.
7
+ - **Wait for a pause.** `_delay=400` runs the controller 400 ms after the last change, once.
8
+ - **Run at start and at intervals.** `_onStart=True` runs the controller when the page has started; `_timing='^.interval'` repeats it every *n* seconds, and 0 stops it.
9
+
10
+ **Try it:** Clear the name, type a search quickly, then change the interval to 5.
@@ -0,0 +1,28 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Named logic';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'cart'});
9
+ pane.h1('Named logic');
10
+ pane.p('The formula and the controller name methods of class Logic in 03_named_logic_aux.js.',
11
+ {class: 'muted'});
12
+ const form = pane.form({class: 'card stack'});
13
+ form.html_label('Price', {for: 'price'});
14
+ form.input({type: 'number', value: '^.price', live: true, id: 'price'});
15
+
16
+ const result = pane.section({class: 'card stack'});
17
+ const facts = result.dl();
18
+ for (const [label, key] of [["Final price (func='finalPrice')", 'final'], ["Changes seen (func='countChange')", 'changes'],
19
+ ['Last reason', 'reason']]) {
20
+ facts.dt(label);
21
+ facts.dd(`^.${key}`, {id: key});
22
+ }
23
+
24
+ pane.dataFormula({result_path: '.final', func: 'finalPrice', price: '^.price', threshold: 100, _init: true});
25
+ pane.dataController({func: 'countChange', price: '^.price'});
26
+ pane.dataSetter({destination_path: '.price', value: 80});
27
+ }
28
+ }
@@ -0,0 +1,12 @@
1
+ # 03 · Named logic
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.
4
+
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
+ - A controller method receives its Source node and the parameters. `node.SET('.changes', …)` writes relative to the node's context.
7
+ - `this` is the logic group: `this.changes` keeps a count between calls, and `this.page` is the Gramlot page.
8
+ - The parameters also carry `_reason`, `_node` and `_triggerpars`.
9
+
10
+ Named logic compiles nothing at run time, so it also works under a Content Security Policy without `'unsafe-eval'`.
11
+
12
+ **Try it:** Raise the price above 100, then add a second formula that names `finalPrice` with another threshold.
@@ -0,0 +1,19 @@
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
+ }
@@ -0,0 +1,38 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Inline expressions';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'player'});
9
+ pane.h1('Inline expressions');
10
+ const form = pane.form({class: 'card stack'});
11
+ for (const [label, key] of [['First name', 'first'], ['Last name', 'last']]) {
12
+ form.html_label(label, {for: key});
13
+ form.input({value: `^.${key}`, live: true, id: key});
14
+ }
15
+ form.html_label('Score', {for: 'score'});
16
+ form.input({type: 'range', min: 0, max: 100, value: '^.score', live: true, id: 'score'});
17
+ const row = form.div();
18
+ row.input({type: 'checkbox', value: '^.accepted', id: 'accepted'});
19
+ row.html_label('I accept the rules', {for: 'accepted'});
20
+
21
+ const result = pane.section({class: 'card stack'});
22
+ result.h2("==first + ' ' + last", {first: '^.first', last: '^.last', id: 'full-name'});
23
+ result.p("==score >= 50 ? 'Passed' : 'Not yet'", {score: '^.score', id: 'verdict',
24
+ class: "==score >= 50 ? 'status status--success' : 'status status--error'",
25
+ title: "=='Score ' + score + ' of 100'"});
26
+ const bar = result.svg({viewBox: '0 0 300 20', role: 'img', aria_label: 'Score bar', class: 'example-art'});
27
+ bar.rect({x: 0, y: 0, height: 20, width: '==score * 3', score: '^.score', fill: 'var(--gramlot-action)', id: 'bar'});
28
+ result.button('Start', {disabled: '==!accepted', accepted: '^.accepted', id: 'start',
29
+ action: "this.SET('.started', true)"});
30
+ result.p('Started', {visible: '^.started', id: 'started', class: 'muted'});
31
+
32
+ pane.dataSetter({destination_path: '.first', value: 'Ada'});
33
+ pane.dataSetter({destination_path: '.last', value: 'Lovelace'});
34
+ pane.dataSetter({destination_path: '.score', value: 40});
35
+ pane.dataSetter({destination_path: '.accepted', value: false});
36
+ pane.dataSetter({destination_path: '.started', value: false});
37
+ }
38
+ }
@@ -0,0 +1,12 @@
1
+ # 04 · Inline expressions
2
+
3
+ A text or an attribute that starts with `==` is a JavaScript expression. Its names are the other attributes of the same node, which are often `^` pointers: the expression is computed again when one of them changes.
4
+
5
+ - The heading is `==first + ' ' + last`, with `first='^.first'` and `last='^.last'`.
6
+ - The verdict computes its text, its `class` and its `title` from `score`.
7
+ - The SVG bar computes its `width`; the button computes `disabled` from the checkbox.
8
+ - The button's `action` is an inline script run at the click, with `this` = the button node.
9
+
10
+ Inline code needs a Content Security Policy that allows `'unsafe-eval'`; under a strict policy it stops with a clear error naming the node and the attribute. The legacy macros `SET path = value` and `$1` still work and log a deprecation warning: write `this.SET(path, value)` and `arguments[0]`.
11
+
12
+ **Try it:** Move the score across 50, accept the rules, then press Start.
@@ -0,0 +1 @@
1
+ .actions { display: flex; flex-wrap: wrap; gap: .5rem; }
@@ -0,0 +1,39 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const ACTIONS = [
4
+ ['SET: add 1', 'set', "this.SET('.value', this.GET('.value') + 1)"],
5
+ ['PUT: add 10 silently', 'put', "this.PUT('.value', this.GET('.value') + 10)"],
6
+ ["FIRE 'now'", 'fire', "this.FIRE('.ping', 'now')"],
7
+ ["FIRE_AFTER 'later', 1 s", 'fire-after', "this.FIRE_AFTER('.ping', 'later', 1000)"],
8
+ ];
9
+
10
+ export class Page extends BasePage {
11
+ static title = 'Node methods';
12
+ static css = ['/themes/gramlot-base/theme.css'];
13
+
14
+ main(root) {
15
+ const pane = root.div({class: 'example-page stack', datapath: 'methods'});
16
+ pane.h1('Node methods');
17
+ const actions = pane.div({class: 'actions'});
18
+ for (const [label, key, script] of ACTIONS) actions.button(label, {action: script, id: key});
19
+
20
+ const result = pane.section({class: 'card stack'});
21
+ const facts = result.dl();
22
+ for (const [label, pointer, key] of [['value (^.value)', '^.value', 'value'],
23
+ ['Changes notified', '^.notified', 'notified'],
24
+ ['Pings received', '^.pings', 'pings'],
25
+ ['Last ping', '^.last', 'last']]) {
26
+ facts.dt(label);
27
+ facts.dd(pointer, {id: key});
28
+ }
29
+ result.button('GET value', {action: "this.SET('.read', this.GET('.value'))", id: 'get'});
30
+ result.p('^.read', {id: 'read', class: 'muted'});
31
+
32
+ pane.dataController({script: "this.SET('.notified', notified + 1)", value: '^.value', notified: '=.notified'});
33
+ pane.dataController({script: "this.SET('.pings', pings + 1); this.SET('.last', ping)", ping: '^.ping',
34
+ pings: '=.pings'});
35
+ pane.dataSetter({destination_path: '.value', value: 0});
36
+ pane.dataSetter({destination_path: '.notified', value: 0});
37
+ pane.dataSetter({destination_path: '.pings', value: 0});
38
+ }
39
+ }
@@ -0,0 +1,11 @@
1
+ # 05 · Node methods
2
+
3
+ Every Source node offers five methods for the Data. A path starting with `.` is relative to the node's context; here each button's `action` calls them with `this` = the button node.
4
+
5
+ - `this.SET(path, value)` writes and notifies: the value on screen changes and the observer counts one change.
6
+ - `this.GET(path)` reads. **GET value** copies the current value into `.read`.
7
+ - `this.PUT(path, value)` writes silently: nothing is notified, so the value on screen keeps the old number until the next SET. **GET value** shows the real one.
8
+ - `this.FIRE(path, value)` writes the value and resets the path silently afterwards, so every FIRE is delivered, also with an equal value.
9
+ - `this.FIRE_AFTER(path, value, delay)` fires after `delay` ms. The timer belongs to the node: removing the node cancels it.
10
+
11
+ **Try it:** Press PUT, then GET value, then SET, and compare the three numbers.
@@ -0,0 +1,36 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const KEYS = "[button_shift && 'Shift', button_ctrl && 'Ctrl', button_alt && 'Alt', button_meta && 'Meta']" +
4
+ ".filter(Boolean).join('+') || 'none'";
5
+
6
+ export class Page extends BasePage {
7
+ static title = 'Button controller';
8
+ static css = ['/themes/gramlot-base/theme.css'];
9
+
10
+ main(root) {
11
+ const pane = root.div({class: 'example-page stack', datapath: 'buttons'});
12
+ pane.h1('Button controller');
13
+
14
+ const nested = pane.section({class: 'card stack'});
15
+ nested.h2('A controller nested in the button');
16
+ const button = nested.button('Press me, also with Shift or Alt', {id: 'press'});
17
+ button.dataController({script: `this.SET('.clicks', button_counter); this.SET('.keys', ${KEYS})`});
18
+ const facts = nested.dl();
19
+ for (const [label, key] of [['button_counter', 'clicks'], ['Modifier keys', 'keys']]) {
20
+ facts.dt(label);
21
+ facts.dd(`^.${key}`, {id: key});
22
+ }
23
+
24
+ const fired = pane.section({class: 'card stack'});
25
+ fired.h2('fire and fire_<name>');
26
+ fired.button('Save (fire)', {fire: '.saved', id: 'save'});
27
+ fired.p('^.saved_with', {id: 'saved-with'});
28
+ fired.button('Save and close (fire_save, fire_close)', {fire_save: '.command', fire_close: '.command',
29
+ id: 'save-close'});
30
+ fired.p('^.log', {id: 'log'});
31
+
32
+ pane.dataController({script: "this.SET('.saved_with', 'fired with ' + saved)", saved: '^.saved'});
33
+ pane.dataController({script: "this.SET('.log', (log ? log + ', ' : '') + command)", command: '^.command', log: '=.log'});
34
+ pane.dataSetter({destination_path: '.clicks', value: 0});
35
+ }
36
+ }
@@ -0,0 +1,11 @@
1
+ # 06 · Button controller
2
+
3
+ A button runs one click mechanism: a nested `dataController`, an `action`, or the `fire` family. Declaring two of them on one button is an error.
4
+
5
+ - **Nested controller.** The `dataController` inside the button runs once per click. Its code reads `button_counter`, which counts the clicks of this button and survives a rebuild, and `button_shift`, `button_ctrl`, `button_alt`, `button_meta`; `_evt` is the DOM event.
6
+ - **fire.** `fire='.saved'` fires the path at each click. The value is the modifier string, such as `Shift`, or `true` without modifiers.
7
+ - **fire_<name>.** `fire_save` and `fire_close` fire their paths with the values `save` and `close`, in attribute order: the log gets two entries per click.
8
+
9
+ A button with a mechanism gets `type="button"`, so it never submits a form by itself.
10
+
11
+ **Try it:** Press the first button with Shift, then Save with Alt.
@@ -0,0 +1,38 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Events';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'events'});
9
+ pane.h1('Events');
10
+
11
+ const clicks = pane.section({class: 'card stack', id: 'click-area',
12
+ connect_onclick: "this.SET('.clicked', event.target.id || event.target.localName)"});
13
+ clicks.h2('connect_onclick on a section');
14
+ clicks.p('Click anywhere in this card, or on one of the chips.');
15
+ for (const name of ['red', 'green', 'blue']) clicks.span(name, {id: `chip-${name}`, class: 'status'});
16
+ clicks.p('^.clicked', {id: 'clicked', class: 'muted'});
17
+
18
+ const typing = pane.section({class: 'card stack'});
19
+ typing.h2('connect_oninput and connect_onkeydown');
20
+ typing.html_label('Type here', {for: 'typing'});
21
+ typing.input({id: 'typing', connect_oninput: "this.SET('.length', event.target.value.length)",
22
+ connect_onkeydown: "this.SET('.key', event.key)"});
23
+ const facts = typing.dl();
24
+ for (const [label, key] of [['Length', 'length'], ['Last key', 'key']]) {
25
+ facts.dt(label);
26
+ facts.dd(`^.${key}`, {id: key});
27
+ }
28
+
29
+ const hover = pane.section({class: 'card stack'});
30
+ hover.h2('connect_onmouseover and connect_onmouseout');
31
+ hover.div('Move the pointer over this box', {id: 'hover-box', class: '^.hover_class',
32
+ connect_onmouseover: "this.SET('.hover_class', 'card status status--success')",
33
+ connect_onmouseout: "this.SET('.hover_class', 'card')"});
34
+
35
+ pane.dataSetter({destination_path: '.hover_class', value: 'card'});
36
+ pane.dataSetter({destination_path: '.length', value: 0});
37
+ }
38
+ }
@@ -0,0 +1,10 @@
1
+ # 07 · Events
2
+
3
+ `connect_on<event>` adds one DOM listener to the element of its node. The event name is the part after `connect_on`, lower-cased: `connect_onclick` listens to `click`, `connect_onkeydown` to `keydown`.
4
+
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
+ - 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.
8
+ - Removing the node removes its listeners.
9
+
10
+ **Try it:** Click the green chip, type a word, then add `connect_onfocus` to the field.
@@ -0,0 +1,34 @@
1
+ import {Page as BasePage, source} from '@gramlot/gramlot/page';
2
+
3
+ const TOPICS = {
4
+ binding: ['Binding', 'Pointers connect the DOM to the Data in both directions.'],
5
+ controllers: ['Controllers', 'Formulas and controllers run code when the Data changes.'],
6
+ svg: ['SVG', 'SVG attributes follow the Data like HTML attributes.'],
7
+ };
8
+
9
+ export class Page extends BasePage {
10
+ static title = 'Remote Source';
11
+ static css = ['/themes/gramlot-base/theme.css'];
12
+
13
+ main(root) {
14
+ const pane = root.div({class: 'example-page stack', datapath: 'remote'});
15
+ pane.h1('Remote Source');
16
+ pane.html_label('Topic', {for: 'topic'});
17
+ const topic = pane.select({value: '^.topic', id: 'topic'});
18
+ for (const [key, [title]] of Object.entries(TOPICS)) topic.option(title, {value: key});
19
+ pane.button('Load from the server', {id: 'load'}).dataController({func: 'load', topic: '=.topic'});
20
+ const target = pane.section({node_id: 'details', class: 'card stack', id: 'details'});
21
+ target.p('Nothing loaded yet.', {class: 'muted'});
22
+ pane.dataSetter({destination_path: '.topic', value: 'binding'});
23
+ }
24
+
25
+ details(root, {topic = 'binding'} = {}) {
26
+ const [title, summary] = TOPICS[topic];
27
+ const branch = root.div({datapath: `topics.${topic}`, class: 'stack'});
28
+ branch.h2('^.title', {id: 'remote-title'});
29
+ branch.p('^.summary', {id: 'remote-summary'});
30
+ branch.dataSetter({destination_path: '.title', value: title});
31
+ branch.dataSetter({destination_path: '.summary', value: summary});
32
+ }
33
+ }
34
+ source(Page.prototype.details);
@@ -0,0 +1,10 @@
1
+ # 08 · Remote Source
2
+
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
+
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.
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
+ - 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
+
10
+ **Try it:** Choose SVG and load it, then add a third `dataSetter` to the remote branch.
@@ -0,0 +1,8 @@
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
+ }
@@ -0,0 +1 @@
1
+ .actions { display: flex; flex-wrap: wrap; gap: .5rem; }