@gramlot/gramlot-examples 0.2.12 → 0.2.15

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.
@@ -25,11 +25,8 @@ export class GalleryPage extends IndexPage {
25
25
  footer(footer) {
26
26
  super.footer(footer);
27
27
  const inspector = footer.button({type: 'button', id: 'gallery-inspector', class: 'gallery-inspector',
28
- title: 'Inspect the gallery or the visible example', aria_label: 'Inspector', aria_pressed: 'false', disabled: true});
29
- const icon = inspector.svg({viewBox: '0 0 24 24', width: '18', height: '18', aria_hidden: 'true'});
30
- icon.rect({x: '3', y: '3', width: '18', height: '18', rx: '2', fill: 'none', stroke: 'currentColor', stroke_width: '1.5'});
31
- icon.path({d: 'M9 3v18M12 8h6M12 12h6M12 16h4', fill: 'none', stroke: 'currentColor', stroke_width: '1.5'});
32
- inspector.span('Inspector');
28
+ title: 'Open inspector', aria_label: 'Inspector', aria_pressed: 'false', disabled: true});
29
+ inspector.span('🔍', {aria_hidden: 'true'});
33
30
 
34
31
  }
35
32
  behavior(shell) { shell.script({src: this.constructor.galleryScript}); }
@@ -33,6 +33,7 @@
33
33
  .gallery-source-tabs button:focus-visible { outline: 2px solid var(--gramlot-focus-ring); outline-offset: -2px; }
34
34
  .gallery-source-tabs button[hidden] { display: none; }
35
35
  .gallery-explanation { padding: .4rem; }
36
- .gallery-inspector { display: inline-flex; align-items: center; gap: .35rem; margin-inline-start: auto; padding: .15rem .5rem; min-block-size: 26px; font-size: inherit; }
37
- .gallery-inspector svg { flex: none; }
36
+ .gallery-inspector { display: inline-flex; align-items: center; justify-content: center; flex: none; margin-inline-start: auto; padding: 0; inline-size: 28px; block-size: 28px; min-block-size: 28px; border: 0; background: transparent; box-shadow: none; font-size: 16px; line-height: 1; }
37
+ .gallery-inspector:hover { background: var(--gramlot-surface); }
38
+ .gallery-inspector:focus-visible { outline: 2px solid var(--gramlot-focus-ring); outline-offset: 2px; }
38
39
  .gallery-inspector[aria-pressed="true"] { background: var(--gramlot-action); color: var(--gramlot-navigation-text); }
@@ -93,8 +93,9 @@ from [the gallery guide](https://github.com/gramlot-org/gramlot-examples/blob/ma
93
93
  c13/c14 separate the basic trial from advanced cache/error/lifetime exercises.
94
94
  In c13, “Mounted fragments” counts successful mounts; HTTP calls must be observed
95
95
  independently. c14 has an explicit read button to distinguish a stored snapshot
96
- from a later response. Neither page claims every environment has been qualified. The corrected cache behavior documented in
97
- the development core is not automatically present in published packages.
96
+ from a later response. The corrected cache behavior is included from core 0.2.17. Update older
97
+ installations before following the advanced trials; a startup check alone does
98
+ not qualify every exercise in an environment.
98
99
 
99
100
  The common FileServer gallery has no named resource groups: css_requires,
100
101
  js_requires and named connect_on handlers need genro-kajenn's resource host.
@@ -48,7 +48,7 @@ The corrected identity is the method name plus the **TYTX snapshot of parameters
48
48
 
49
49
  ## Advanced practice
50
50
 
51
- **Development distinction:** the corrected cache identity is locally implemented in the core stabilization work; it is not a new published release. Check the runtime you are using before interpreting these advanced trials. Installed released packages may retain the earlier defects.
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
52
 
53
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
54
 
@@ -0,0 +1,36 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Publish and subscribe';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+ main(root) {
7
+ const pane = root.main({class: 'example-page stack', datapath: 'messages'});
8
+ pane.h1('The same action, two entry points');
9
+ pane.p('Compare the direct controller with a page topic. Both update the same summary.');
10
+ pane.dataSetter({destination_path: '.text', value: 'Hello'});
11
+ pane.html_label('Message', {for: 'message'});
12
+ pane.input({id: 'message', value: '^.text', live: true});
13
+ pane.button('Direct controller', {id: 'direct'}).dataController({func: 'save', text: '=.text'});
14
+ pane.button('Publish', {id: 'publish'}).dataController({func: 'publishMessage', text: '=.text'});
15
+ pane.dataController({func: 'save', subscribe_saved: true});
16
+ pane.p('^.summary', {id: 'summary'});
17
+ pane.button('Ask, then publish', {id: 'ask'}).dataController({func: 'publishMessage',
18
+ _ask: {title: 'Message', fields: [{name: 'text', lbl: 'Message', required: true}]}, text: '=.text'});
19
+ pane.h2('Two independent receivers');
20
+ for (const name of ['left', 'right']) {
21
+ const box = pane.section({class: 'card stack', datapath: `.${name}`});
22
+ box.h3(name[0].toUpperCase() + name.slice(1));
23
+ const target = box.dataController({func: 'receive', selfsubscribe_refresh: true});
24
+ box.button(`Send to ${name}`, {id: `send-${name}`}).dataController({func: 'sendToNode', target, text: '=.text'});
25
+ box.p('^.received', {id: `received-${name}`});
26
+ box.dataSetter({destination_path: '.text', value: `Hello ${name}`});
27
+ }
28
+ }
29
+ }
30
+
31
+ export class Logic {
32
+ save(node, {text, _reason}) { node.SET('.summary', `${text} (${_reason})`); }
33
+ publishMessage(node, {text}) { this.page.publish('saved', {text}); }
34
+ sendToNode(node, {target, text}) { target.publish('refresh', {text}); }
35
+ receive(node, {text}) { node.SET('.received', text); }
36
+ }
@@ -0,0 +1,20 @@
1
+ # 18 · Publish and subscribe
2
+
3
+ Revisit **Button controller**: one button calls a controller directly, another publishes a message to the same behavior. Then address two independent receivers without looking up their IDs, and collect a message in a dialog.
4
+
5
+ **Uses:** Source, Data, named Logic, page/self subscriptions, authored references and native `ask` — core 0.2.18. No endpoint calls. Earlier lessons remain the 0.2.0 stage.
6
+
7
+ **Try it:** edit Message, press Direct controller, then Publish. The summary is the same except for the trigger reason. Send to left: only the left receiver changes. Ask, then publish lets you edit the outgoing message; Cancel leaves the summary unchanged.
8
+
9
+ <details>
10
+ <summary>More detail</summary>
11
+
12
+ The direct controller keeps the c06 click mechanism. The publishing controller knows the topic `saved`; `subscribe_saved=True` connects the receiver. The publisher does not need a Data trigger path to activate that receiver. Repeating the same message still delivers it.
13
+
14
+ Each receiver declares `selfsubscribe_refresh=True`. Python passes that controller as `target`; Gramlot resolves the authored reference in the browser. `target.publish('refresh', {text})` addresses only that node. The JavaScript Page makes the same declaration, and both use the shared Logic class.
15
+
16
+ `_ask` adds native field collection to the existing controller. It runs the controller once after confirmation and never after cancellation. This example uses named methods throughout; it does not need inline JavaScript evaluation. For a conditional confirmation use a string `_ask` with `_ask_if`; those inline conditions need the corresponding CSP policy.
17
+
18
+ **Exercise:** subscribe a second controller to `saved`, writing another Data path. Predict whether both run when Publish is pressed. **Answer:** both run, in registration order; each should write its own result. A self-subscription does not also subscribe to the plain page topic.
19
+
20
+ </details>
@@ -0,0 +1,39 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Parent and child messages';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+ main(root) {
7
+ const pane = root.main({class: 'example-page stack', datapath: 'frames'});
8
+ pane.h1('Parent and child messages');
9
+ pane.dataSetter({destination_path: '.child', value: false});
10
+ pane.dataSetter({destination_path: '.text', value: 'Hello'});
11
+ pane.html_label('Message', {for: 'message'});
12
+ pane.input({id: 'message', value: '^.text', live: true});
13
+ const parent = pane.section({hidden: '^.child'});
14
+ const frame = parent.iframe({id: 'detail', title: 'Child page', style: 'width:100%;height:260px;border:1px solid #aab4c2'});
15
+ parent.button('Send to child', {id: 'send-child'}).dataController({func: 'sendChild', target: frame, text: '=.text'});
16
+ pane.button('Reply to parent', {id: 'reply'}).dataController({func: 'reply', text: '=.text', child: '=.child'});
17
+ pane.p('^.received', {id: 'received'});
18
+ pane.dataController({func: 'receive', subscribe_message: true});
19
+ pane.dataController({func: 'setup', frame, _onStart: true});
20
+ }
21
+ }
22
+
23
+ export class Logic {
24
+ setup(node, {frame}) {
25
+ const url = new URL(this.page.dom.getDomNode(node.parentNode).ownerDocument.location.href);
26
+ const child = url.hash === '#child';
27
+ node.SET('.child', child);
28
+ if (!child) { url.hash = 'child'; frame.setAttr({src: url.href}); }
29
+ }
30
+ sendChild(node, {target, text}) {
31
+ try { this.page.publish({topic: 'message', iframe: target}, {text}); }
32
+ catch (error) { node.SET('.received', error.message); }
33
+ }
34
+ reply(node, {text, child}) {
35
+ if (child) this.page.publish({topic: 'message', parent: true}, {text});
36
+ else node.SET('.received', 'Use Reply inside the child page.');
37
+ }
38
+ receive(node, {text}) { node.SET('.received', text); }
39
+ }
@@ -0,0 +1,20 @@
1
+ # 19 · Parent and child messages
2
+
3
+ Send a message to the embedded page, then reply from that page. The example reuses the page-topic receiver from **Publish and subscribe** and adds a destination to `publish`.
4
+
5
+ **Uses:** Source, Data, named Logic, authored iframe references and direct parent/child `postMessage` — core 0.2.18. No endpoint calls or server messaging.
6
+
7
+ **Try it:** wait for the child field to appear, edit the outer Message and press Send to child. Both summaries change: publication is local first, then reaches the selected child. Edit the child's Message and press its Reply to parent. The child publishes locally first and the parent receives the reply.
8
+
9
+ <details>
10
+ <summary>More detail</summary>
11
+
12
+ The same page opens with `#child` inside the iframe. Its setup controller hides the parent section and does not load another iframe, so nesting stops after one child. The URL hash is only this example's mode selector; it is not a messaging address.
13
+
14
+ Python passes the iframe Source as `target`. Logic uses `{topic: 'message', iframe: target}`. The child uses `{topic: 'message', parent: true}`. All cross-frame calls use postMessage, including same-origin pages; each receiving page activates its ordinary subscribe_message controller.
15
+
16
+ The child must be connected and ready. An early click reports the readiness error; retry after the child appears. There is no automatic queue or broadcast. Source/DOM objects stay local: the iframe reference selects a local destination, while only the message data crosses the connection. Reloading the child establishes a new connection.
17
+
18
+ **Exercise:** add another owned child and send only to its reference. **Answer:** local delivery still occurs, but only the selected child receives the remote copy. There is no recursive broadcast in this release.
19
+
20
+ </details>
@@ -0,0 +1,40 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Actions, confirmations and references';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+ main(root) {
7
+ const pane = root.main({class: 'example-page stack', datapath: 'buttons'});
8
+ pane.h1('One action, with or without a question');
9
+ pane.dataSetter({destination_path: '.name', value: 'Ada'});
10
+ pane.dataSetter({destination_path: '.confirm', value: true});
11
+ pane.dataSetter({destination_path: '.result', value: 'No action yet'});
12
+ pane.html_label('Ask before running', {for: 'confirm-needed'});
13
+ pane.input({id: 'confirm-needed', type: 'checkbox', value: '^.confirm'});
14
+ pane.button('Run action', {id: 'action', name: '=.name', action: "this.SET('.result', 'Hello ' + name)"});
15
+ pane.button('Ask for a name', {id: 'action-ask', name: '=.name', action: "this.SET('.result', 'Hello ' + name)",
16
+ ask: {title: 'Greeting', fields: [{name: 'name', lbl: 'Name', required: true}]}});
17
+ pane.button('Conditional action', {id: 'conditional-action', name: '=.name', confirm_needed: '=.confirm',
18
+ action: "this.SET('.result', 'Hello ' + name)", ask: 'Send this greeting?', _ask_if: 'confirm_needed'});
19
+ pane.button('Conditional controller', {id: 'conditional-controller'}).dataController({func: 'greet', name: '=.name',
20
+ confirm_needed: '=.confirm', _ask: 'Send this greeting?', _ask_if: 'confirm_needed'});
21
+ pane.button('Conditional fields', {id: 'conditional-fields', name: '=.name', confirm_needed: '=.confirm',
22
+ action: "this.SET('.result', 'Hello ' + name)",
23
+ ask: {title: 'Greeting', _if: 'confirm_needed', fields: [{name: 'name', lbl: 'Name'}]}});
24
+ pane.button('Legacy askIf', {id: 'legacy-askif', name: '=.name', action: "this.SET('.result', 'Hello ' + name)",
25
+ ask: {msg: 'askIf=false alone still asks in legacy semantics.', askIf: 'false'}});
26
+ pane.p('^.result', {id: 'result'});
27
+ pane.h2('Pass the element, without looking up an ID');
28
+ const target = pane.p('Original text', {id: 'reference-target'});
29
+ pane.button('Use Source reference', {id: 'source-reference'}).dataController({func: 'changeSource', target});
30
+ pane.button('Read DOM reference', {id: 'dom-reference'}).dataController({func: 'describeDom',
31
+ target: target.builder.reference(target, 'dom')});
32
+ pane.p('^.reference', {id: 'reference-result'});
33
+ }
34
+ }
35
+
36
+ export class Logic {
37
+ greet(node, {name}) { node.SET('.result', `Hello ${name}`); }
38
+ changeSource(node, {target}) { target.setValue('Changed through Source'); }
39
+ describeDom(node, {target}) { node.SET('.reference', `DOM element: ${target.tagName}`); }
40
+ }
@@ -0,0 +1,20 @@
1
+ # 20 · Actions, confirmations and references
2
+
3
+ The same greeting runs immediately, after a question, or after collecting a name. Switch **Ask before running** off and on to see when the question appears. Then pass a paragraph to a controller as a Source reference or as a DOM reference.
4
+
5
+ **Uses:** Source, Data, buttons, inline actions, named controllers, native ask and local references — core 0.2.18. No endpoints or cross-frame references. This revisits the earlier button lesson with the 0.2.18 additions.
6
+
7
+ **Try it:** choose **Ask for a name**, enter Grace and confirm. The result becomes Hello Grace; Cancel keeps the old result. Turn the checkbox off: both conditional buttons run immediately. Turn it on: they wait for confirmation. **Conditional fields** applies the same choice to a dialog with a field.
8
+
9
+ <details>
10
+ <summary>More detail</summary>
11
+
12
+ `action` is a small JavaScript expression on the button. `ask` belongs to that action; `_ask` belongs to its nested controller. The string forms use `_ask_if`. The dictionary form uses `_if` to skip the dialog when false. These inline conditions/actions require the gallery's permissive CSP; named controller methods remain the usual choice for reusable behavior.
13
+
14
+ Legacy `askIf` is not the skip condition: `askIf="false"` alone still asks. The dedicated button demonstrates this difference. Modifier-based `askOn`/`skipOn` are not supported. Use `_ask_if`, or dictionary `_if`, when a false condition must run without asking.
15
+
16
+ Python passes `target=target` to obtain the live Source node. Its controller changes the paragraph through Source. `target.builder.reference(target, kind="dom")` explicitly requests the rendered element; the second controller reads its tag name. JavaScript spells this `reference(target, 'dom')`. Neither requires an application ID lookup. These references remain local to the page and cannot be sent as RPC or interframe payloads.
17
+
18
+ **Exercise:** change the initial name to Linus and turn the checkbox off. **Result:** the conditional controls use Linus without opening a dialog; no field collection has taken place.
19
+
20
+ </details>
@@ -0,0 +1,36 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Plain and delayed buttons';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+ main(root) {
7
+ const pane = root.main({class: 'example-page stack', datapath: 'clicks'});
8
+ pane.h1('Native HTML, then Gramlot behavior');
9
+ const form = pane.form({class: 'card stack'});
10
+ form.html_label('Native value', {for: 'native-value'});
11
+ form.input({id: 'native-value', value: 'Original'});
12
+ form.html_button('Native reset', {type: 'reset', id: 'native-reset'});
13
+ pane.p('Edit the field and Reset: the browser restores its initial value, without a Data controller.');
14
+ pane.dataSetter({destination_path: '.count', value: 0});
15
+ pane.dataSetter({destination_path: '.runs', value: 0});
16
+ pane.button('Immediate action', {id: 'immediate',
17
+ action: "this.SET('.count', _counter); this.SET('.runs', this.GET('.runs') + 1)"});
18
+ pane.button('Delayed action', {id: 'delayed', _delay: 600,
19
+ action: "this.SET('.count', _counter); this.SET('.runs', this.GET('.runs') + 1)"});
20
+ pane.button('Delayed controller', {id: 'delayed-controller', _delay: 600}).dataController({func: 'countBurst'});
21
+ pane.button('Delayed confirmation', {id: 'delayed-ask', _delay: 600,
22
+ action: "this.SET('.count', _counter); this.SET('.runs', this.GET('.runs') + 1)", ask: 'Run this group of clicks?'});
23
+ pane.p('Click a delayed button several times quickly. Wait: one execution reports the number of clicks in that group.');
24
+ pane.html_label('Clicks', {for: 'count'});
25
+ pane.output('^.count', {id: 'count'});
26
+ pane.html_label('Executions', {for: 'runs'});
27
+ pane.output('^.runs', {id: 'runs'});
28
+ }
29
+ }
30
+
31
+ export class Logic {
32
+ countBurst(node, {button_counter}) {
33
+ node.SET('.count', button_counter);
34
+ node.SET('.runs', node.GET('.runs') + 1);
35
+ }
36
+ }
@@ -0,0 +1,20 @@
1
+ # 21 · Plain and delayed buttons
2
+
3
+ Reset an ordinary HTML form, then compare the same Gramlot action with and without a delay. Several quick clicks become one execution, with the size of the group displayed below.
4
+
5
+ **Uses:** Source, Data and button actions/controllers; core 0.2.19 additions: `html_button` and button `_delay`. No Web Components or endpoint calls. Earlier lessons keep their original release scope.
6
+
7
+ **Try it:** edit Native value and press Native reset. Click Immediate action twice: Executions increases twice. Click Delayed action three times quickly: after 600 ms from the last click, Executions increases once and Clicks shows 3. A new group starts again from 1. Delayed controller does the same with a named method. Delayed confirmation opens one question after the group; Cancel does not run the action.
8
+
9
+ <details>
10
+ <summary>More detail</summary>
11
+
12
+ `html_button` renders a native `<button>` with no implicit type or Gramlot click mechanism. Here `type="reset"` lets the browser reset an unbound field. Use `type="button"` for a plain button that must not submit a form; explicit events use `connect_onclick`. `action`, `ask`, `fire` and `_delay` belong to `button`, not `html_button`.
13
+
14
+ Both immediate and delayed buttons render as `<button>`. The delayed example adds only `_delay=600`; the action stays the same. Every click restarts the timer. The last event supplies modifiers, and the delayed action reads its arguments when it runs. `_counter` is the group count; a nested controller receives it as `button_counter`. Without delay, the existing counter counts the button's whole lifetime.
15
+
16
+ A container can supply inherited `_delay`; an explicit zero on a button makes it immediate. Button delay precedes action, ask, fire and the nested provider. A controller's own `_delay` separately delays its Data/subscription triggers. Explicit native event listeners still run on each click. Removing the button or closing its page cancels pending work. The old legacy 200 ms disabled guard for immediate clicks is not introduced.
17
+
18
+ **Exercise:** change only `_delay` from 600 to 1200. **Result:** the last click postpones the same action for 1.2 seconds; the action and its count logic do not change.
19
+
20
+ </details>
@@ -9,6 +9,8 @@ Add calculations and actions to bound pages, then calls to their host. Each stag
9
9
  - **0.2.15:** the later part of **Call on click** adds Source authorization.
10
10
  - **0.2.16:** **Remote**, **Cached remote** and **Data remote** add Source fragments and values loaded on demand.
11
11
 
12
+ - **0.2.18:** revisit **Button controller** in **Publish and subscribe**, then use **Parent and child messages** for direct iframe exchange. These pages require core 0.2.18 or later.
13
+
12
14
  The host is Python, Node/Bun or a standalone browser Worker. Local Logic runs in the browser. Stable example numbers identify pages; the menu follows release order.
13
15
 
14
16
  <details>
@@ -33,8 +35,8 @@ c12 teaches Source replacement in the later 0.2.16 stage.
33
35
  The Page/Logic module arrangement is a 0.2.5 addition to the named-logic lesson. c08's first pass combines local binding/lifecycle. Return after c09/c11 for its
34
36
  0.2.14 quote/result/error section; it does not run automatically or add remote/lazy Data. After b02 and c12, use c13
35
37
  for a first cache trial; c14 needs the endpoint concepts of c09/c10. Advanced
36
- cache/lifecycle exercises are separate in c13/c14; development-only cache
37
- corrections and pending environment-wide qualification are explicitly labelled. See the [release-by-release route](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md#gc-025-015). Prerequisites support the
38
+ cache/lifecycle exercises are separate in c13/c14; their corrected cache behavior
39
+ is available from core 0.2.17. See the [release-by-release route](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md#gc-025-015). Prerequisites support the
38
40
  chronological sequence; they do not replace it. The maintained files may use
39
41
  later APIs, so they are not snapshots of the earliest release.
40
42
 
@@ -63,3 +65,9 @@ The date initialization issue in c17 and named-resource-host prerequisites remai
63
65
  explicit gaps; the local file lesson does not imply HTTP service or mail delivery.
64
66
 
65
67
  </details>
68
+
69
+
70
+ After the messaging lessons, **c20 · Actions, confirmations and references**
71
+ completes the 0.2.18 button examples. **c21 · Plain and delayed buttons** then
72
+ shows the 0.2.19 additions, comparing the same action before
73
+ and after adding `_delay`. It needs core 0.2.19 or later.