@gramlot/gramlot-examples 0.2.14 → 0.2.16
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.
- package/README.md +9 -4
- package/package.json +2 -2
- package/src/gramlot_examples/catalog.json +21 -3
- package/src/gramlot_examples/pages/binding/06_style_and_visibility.md +77 -12
- package/src/gramlot_examples/pages/controllers/02_controller.js +1 -1
- package/src/gramlot_examples/pages/controllers/16_attribute_projection.js +1 -1
- package/src/gramlot_examples/pages/controllers/16_attribute_projection.md +3 -3
- package/src/gramlot_examples/pages/controllers/20_actions_ask_references.js +40 -0
- package/src/gramlot_examples/pages/controllers/20_actions_ask_references.md +20 -0
- package/src/gramlot_examples/pages/controllers/21_plain_and_delayed_buttons.js +36 -0
- package/src/gramlot_examples/pages/controllers/21_plain_and_delayed_buttons.md +20 -0
- package/src/gramlot_examples/pages/controllers/22_pointer_transforms.js +39 -0
- package/src/gramlot_examples/pages/controllers/22_pointer_transforms.md +64 -0
- package/src/gramlot_examples/pages/controllers/README.md +9 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npm install @gramlot/gramlot-examples
|
|
|
12
12
|
[](https://pypi.org/project/gramlot-examples/)
|
|
13
13
|
[](https://www.npmjs.com/package/@gramlot/gramlot-examples)
|
|
14
14
|
|
|
15
|
-
Current release: 0.2.
|
|
15
|
+
Current release: 0.2.16.
|
|
16
16
|
|
|
17
17
|
[Documentation and learning sequence](https://gramlot-examples.readthedocs.io/en/latest/)
|
|
18
18
|
include every lesson, its Python and JavaScript source, and the gallery guide.
|
|
@@ -24,6 +24,10 @@ The repositories of the family are described in
|
|
|
24
24
|
[The Gramlot family](https://github.com/gramlot-org/gramlot/blob/main/docs/public/055-family.md)
|
|
25
25
|
(GC-055).
|
|
26
26
|
|
|
27
|
+
## Updating
|
|
28
|
+
|
|
29
|
+
Choose [packaged or source-checkout updates](docs/010-gallery.md#ge-010-050). A Python gallery installed from source also needs its JavaScript bundles rebuilt; updating Python files alone does not update the footer inspector.
|
|
30
|
+
|
|
27
31
|
## Pages
|
|
28
32
|
|
|
29
33
|
[`src/gramlot_examples/pages/`](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/README.md) holds three
|
|
@@ -32,10 +36,11 @@ same-name README:
|
|
|
32
36
|
|
|
33
37
|
- `html_svg/`: thirteen HTML and SVG pages, without binding (`e01`–`e13`);
|
|
34
38
|
- `binding/`: eleven pages whose DOM follows the Data (`b01`–`b11`);
|
|
35
|
-
- `controllers/`:
|
|
39
|
+
- `controllers/`: twenty-one pages with formulas, controllers, named logic, events, the
|
|
36
40
|
end-to-end story, `dataRpc` calls to the endpoints of the page, `remote` fragments and
|
|
37
|
-
`dataRemote` values (`c01`–`
|
|
38
|
-
|
|
41
|
+
`dataRemote` values (`c01`–`c21`). Later lessons introduce topics, authored references, native ask,
|
|
42
|
+
parent/child messages, plain HTML buttons and delayed Gramlot clicks. The package
|
|
43
|
+
requires core 0.2.20 or later; each lesson states which features it uses.
|
|
39
44
|
|
|
40
45
|
Each `NN_name.js` exports `Page` and `Logic`; the Python page `NN_name.py` takes its
|
|
41
46
|
`Logic` from the module beside it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gramlot/gramlot-examples",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
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.
|
|
17
|
+
"@gramlot/gramlot": ">=0.2.20"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@genrojs/bag": ">=0.11.0",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"title": "Cards with icons",
|
|
73
73
|
"folder": "10_cards_with_icons",
|
|
74
74
|
"description": "HTML cards with SVG icons and live Source removal.",
|
|
75
|
-
"explanation": "Helpers and loops build the cards. Each
|
|
75
|
+
"explanation": "Helpers and loops build the cards. Each × button removes its labelled Source node through the browser Builder; Gramlot updates the DOM without reloading. Reload restores the initial cards. No Data binding or server synchronization is involved."
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
"key": "e11",
|
|
@@ -304,13 +304,31 @@
|
|
|
304
304
|
"key": "c18",
|
|
305
305
|
"title": "Publish and subscribe",
|
|
306
306
|
"folder": "18_publish_subscribe",
|
|
307
|
-
"description": "
|
|
307
|
+
"description": "Core 0.2.18: revisit button actions with page topics, self receivers, references and native ask."
|
|
308
308
|
},
|
|
309
309
|
{
|
|
310
310
|
"key": "c19",
|
|
311
311
|
"title": "Parent and child messages",
|
|
312
312
|
"folder": "19_frame_messages",
|
|
313
|
-
"description": "
|
|
313
|
+
"description": "Core 0.2.18: local-first publish and direct parent/child postMessage exchange."
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"key": "c20",
|
|
317
|
+
"title": "Actions, confirmations and references",
|
|
318
|
+
"folder": "20_actions_ask_references",
|
|
319
|
+
"description": "Core 0.2.18: action, conditional ask and explicit Source/DOM references."
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"key": "c21",
|
|
323
|
+
"title": "Plain and delayed buttons",
|
|
324
|
+
"folder": "21_plain_and_delayed_buttons",
|
|
325
|
+
"description": "Core 0.2.19: native html_button and grouped clicks with _delay."
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"key": "c22",
|
|
329
|
+
"title": "Pointer templates and expressions",
|
|
330
|
+
"folder": "22_pointer_transforms",
|
|
331
|
+
"description": "Core 0.2.20: revisit CSS lengths, boolean state and formatted text with attribute pointer transforms."
|
|
314
332
|
}
|
|
315
333
|
]
|
|
316
334
|
}
|
|
@@ -16,29 +16,94 @@ Controls change an element’s appearance and visibility. You learn how Data dri
|
|
|
16
16
|
|
|
17
17
|
**Open and edit:** select **b06 · Style and visibility** in your environment’s gallery. For source changes, copy/edit the matching page in your own project using the [setup guide](https://github.com/gramlot-org/gramlot/blob/main/docs/public/025-try.md); the gallery is a source viewer. Save, then reload/reopen as that guide explains. The snippets below are excerpts/edits within that Page, not standalone launch commands.
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## From Python to Source to DOM
|
|
20
20
|
|
|
21
|
-
The
|
|
21
|
+
The controls above change Data. The preview box reads that Data through its bindings.
|
|
22
|
+
Follow three steps side by side; these are **excerpts of the existing page**, not
|
|
23
|
+
additional declarations to paste into it. Source cells list attributes, not serialized
|
|
24
|
+
Bags; DOM cells show only relevant CSS and omit other styles/classes. Browsers may
|
|
25
|
+
normalize colors to `rgb(...)` and change declaration spacing/order.
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
| Python declaration | Source attributes | DOM inline style at startup |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `color="^.color"` | `color: "^.color"` | `color: #ffffff` |
|
|
30
|
+
| `background_color="^.background"` | `background_color: "^.background"` | `background-color: #456bc4` |
|
|
31
|
+
| `font_size="^.size"` | `font_size: "^.size"` | `font-size: 1.5rem` |
|
|
32
|
+
| `padding="1rem"` | `padding: "1rem"` | `padding: 1rem` |
|
|
33
|
+
| `style="^.style"` | `style: "^.style"` | `border: 4px dashed #ffc400` |
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
All these attributes belong to the **same** preview div. Its enclosing pane has
|
|
36
|
+
`datapath="look"`, so `^.color` follows `look.color`. The renderer combines the
|
|
37
|
+
resolved values into one HTML `style` attribute. It does not put a `color` HTML
|
|
38
|
+
attribute on the div or replace the Source pointer with its current value.
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
For comparison, a literal declaration has no Data dependency:
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
| Python | Source attributes | DOM |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `pane.div("Hello", color="red", font_size="18px")` | `color: "red"`, `font_size: "18px"` | `<div style="color: red; font-size: 18px;">Hello</div>` |
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
This comparison uses the literal styling foundation; the controls in this lesson
|
|
47
|
+
add the core 0.2.0 Data binding layer. No later controller or component feature is needed.
|
|
34
48
|
|
|
35
|
-
|
|
49
|
+
## Try the binding
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
1. Choose a new **Text color**, for example `#ff0000`.
|
|
52
|
+
2. The color input writes its value to `look.color`.
|
|
53
|
+
3. The preview's `color="^.color"` reads that value and becomes red.
|
|
54
|
+
4. Inspect its Source: `color` still contains `^.color`. Inspect the preview element
|
|
55
|
+
in browser DevTools: the inline style now has the resolved red color.
|
|
56
|
+
|
|
57
|
+
The input writes Data; the div displays it. This is not the div's CSS writing back
|
|
58
|
+
to Data. Python built the Source; these edits and projections execute in the browser.
|
|
59
|
+
|
|
60
|
+
## Which style wins?
|
|
61
|
+
|
|
62
|
+
Restore **Text color** to white, then replace the **style** field with
|
|
63
|
+
`color: red; border: 4px dashed #ffc400` and leave the field to commit the edit.
|
|
64
|
+
|
|
65
|
+
| Declaration retained in Source | Current Data | Resulting inline style (excerpt) |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `style="^.style"`, `color="^.color"` | `look.style = "color: red; border: 4px dashed #ffc400"`; `look.color = "#ffffff"` | `color: #ffffff; border: 4px dashed #ffc400` |
|
|
68
|
+
|
|
69
|
+
**The text stays white:** the explicit `color` shortcut wins over the same property
|
|
70
|
+
inside `style`. The border survives because it has no conflicting shortcut. Now
|
|
71
|
+
replace the style field with only `color: red`: the border disappears, while text
|
|
72
|
+
stays white. Replacing the style string removes its old declarations; other shortcut
|
|
73
|
+
properties still compose into the result.
|
|
74
|
+
|
|
75
|
+
This is Gramlot's composition of inline style. The browser then applies the CSS
|
|
76
|
+
cascade: an applicable `!important` stylesheet rule can change the computed result.
|
|
77
|
+
A resolved null shortcut removes its property; it does not restore an overwritten
|
|
78
|
+
property from the style string. Class/style values accept strings or null, not objects.
|
|
79
|
+
|
|
80
|
+
## What about the reverse direction?
|
|
81
|
+
|
|
82
|
+
As a temporary debugging exercise, select the preview's `#box` element in browser
|
|
83
|
+
DevTools and change its inline `color` to green. Source still has `^.color`, and
|
|
84
|
+
Data still has the selected color: **editing CSS does not update either one**.
|
|
85
|
+
Choose another Text color in the page: the Data-driven projection takes over again.
|
|
86
|
+
Reloading also rebuilds the page from its declarations.
|
|
87
|
+
|
|
88
|
+
Editing a literal Source attribute through the Gramlot inspector does project it to
|
|
89
|
+
the DOM, but does not save it to the Python/JS source file. The inspector's **Styles**
|
|
90
|
+
tab previews theme variables, not reverse conversion of element CSS into Source;
|
|
91
|
+
use **Download CSS** there to export applied theme overrides. Computed CSS may come
|
|
92
|
+
from classes, inheritance or variables, so there is no single reverse mapping.
|
|
93
|
+
|
|
94
|
+
## Visibility and a small exercise
|
|
95
|
+
|
|
96
|
+
Uncheck **visible**: the box keeps its space. Restore it and check **hidden**: the
|
|
97
|
+
space collapses. Restore visible and clear hidden, then choose **2rem** font size
|
|
98
|
+
and **24px** corners. The same box changes appearance; its Source structure stays
|
|
99
|
+
unchanged. Use explicit CSS units for sizes.
|
|
100
|
+
|
|
101
|
+
For the broader theme and cascade explanation, see
|
|
102
|
+
[Python styles and a reactive example](https://github.com/gramlot-org/gramlot/blob/main/docs/public/096-themes-and-css.md#gc-096-040).
|
|
38
103
|
|
|
39
104
|
## Limits and next step
|
|
40
105
|
|
|
41
|
-
Stay with the agreed string/null contract.
|
|
106
|
+
Stay with the agreed string/null contract. This lesson does not teach object coercions or rely on them.
|
|
42
107
|
|
|
43
108
|
Continue with [b07 · Bound SVG](https://github.com/gramlot-org/gramlot-examples/blob/main/src/gramlot_examples/pages/binding/07_bound_svg.md). The [authoring reference](https://github.com/gramlot-org/gramlot/blob/main/docs/public/095-writing-pages.md) explains the full contract.
|
|
44
109
|
|
|
@@ -47,7 +47,7 @@ export class Page extends BasePage {
|
|
|
47
47
|
}
|
|
48
48
|
for (const [key, path, exact] of [['name_exact', '.filter.name', true], ['name_all', '.filter.name', false],
|
|
49
49
|
['address_exact', '.filter.address', true], ['address_all', '.filter.address', false]]) {
|
|
50
|
-
filtered.p(`^.${key}`, {mask: `${key}:
|
|
50
|
+
filtered.p(`^.${key}`, {mask: `${key}: {}`, id: key});
|
|
51
51
|
filtered.dataController({func: 'countPath', watched: `^${path}`, counter: `.${key}`, _userChanges: exact});
|
|
52
52
|
pane.dataSetter({destination_path: `.${key}`, value: 0});
|
|
53
53
|
}
|
|
@@ -9,7 +9,7 @@ export class Page extends BasePage {
|
|
|
9
9
|
pane.h2('0.2.0: one value, several projections');
|
|
10
10
|
pane.html_label('Name', {for: 'name'});
|
|
11
11
|
pane.input({value: '^.name', live: true, id: 'name'});
|
|
12
|
-
pane.p('^.name', {mask: 'Hello,
|
|
12
|
+
pane.p('^.name', {mask: 'Hello, {}', id: 'greeting'});
|
|
13
13
|
pane.div('Hover to read the title', {customer: '^.name', title: 'Customer: ${customer}', id: 'card'});
|
|
14
14
|
pane.p('The template consumes customer; it is not a DOM attribute.');
|
|
15
15
|
pane.h2('0.2.3: a separate HTML preview');
|
|
@@ -23,10 +23,10 @@ A template reads another attribute of its own Source node **after pointer resolu
|
|
|
23
23
|
```python
|
|
24
24
|
pane.div("Hover to read the title", customer="^.name",
|
|
25
25
|
title="Customer: ${customer}")
|
|
26
|
-
pane.p("^.name", mask="Hello,
|
|
26
|
+
pane.p("^.name", mask="Hello, {}")
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`${customer}` is Gramlot syntax, not Python interpolation. `customer` supplies the template and is consumed instead of becoming a DOM attribute. `mask` formats the projected text; it does not enable HTML.
|
|
29
|
+
`${customer}` is Gramlot syntax, not Python interpolation. `customer` supplies the template and is consumed instead of becoming a DOM attribute. `mask` formats the projected text; it does not enable HTML. The maintained syntax uses `{}`; older releases used `%s`.
|
|
30
30
|
|
|
31
31
|
The later section uses `pane.iframe(srcdoc="^.html", ...)`. Data-provided HTML gets `sandbox=""`: it is an isolated document, with scripts/forms/popups blocked by default. Ordinary `pane.p("^.html")` would show literal markup instead.
|
|
32
32
|
|
|
@@ -39,7 +39,7 @@ The later section uses `pane.iframe(srcdoc="^.html", ...)`. Data-provided HTML g
|
|
|
39
39
|
|
|
40
40
|
## Change it and check yourself
|
|
41
41
|
|
|
42
|
-
Change the mask to `Welcome,
|
|
42
|
+
Change the mask to `Welcome, {}` and the title to `Guest: ${customer}`. Leave the Data pointer alone.
|
|
43
43
|
|
|
44
44
|
**Answer:** one Name edit still updates both destinations with different formatting; no extra provider or shared formatted Data value is needed.
|
|
45
45
|
|
|
@@ -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>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'Pointer templates and expressions';
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
static locale = 'it-IT';
|
|
7
|
+
static timeZone = 'Europe/Rome';
|
|
8
|
+
|
|
9
|
+
main(root) {
|
|
10
|
+
const pane = root.main({class: 'example-page stack', datapath: 'transforms'});
|
|
11
|
+
for (const [path, value] of [['width', 38], ['draft', false], ['name', 'Ada'], ['amount', 1234.5]]) {
|
|
12
|
+
pane.dataSetter({destination_path: `.${path}`, value});
|
|
13
|
+
}
|
|
14
|
+
pane.h1('Same Data, different presentation');
|
|
15
|
+
const sizes = pane.section({class: 'card stack'});
|
|
16
|
+
sizes.h2('1. A number becomes a CSS length');
|
|
17
|
+
sizes.html_label('Width in pixels', {for: 'width-input'});
|
|
18
|
+
sizes.input({id: 'width-input', type: 'number', value: '^.width', live: true});
|
|
19
|
+
sizes.div('Width', {id: 'width-preview', width: '^.width:{}px', background_color: '#456bc4', color: 'white'});
|
|
20
|
+
sizes.p('Edit the number: only its presentation gains px. Clear it: the explicit width disappears.');
|
|
21
|
+
const state = pane.section({class: 'card stack'});
|
|
22
|
+
state.h2('2. A boolean becomes another boolean');
|
|
23
|
+
state.html_label('Draft: allow editing', {for: 'draft-input'});
|
|
24
|
+
state.input({id: 'draft-input', type: 'checkbox', value: '^.draft'});
|
|
25
|
+
state.html_label('Name', {for: 'name-input'});
|
|
26
|
+
state.input({id: 'name-input', value: '^.name', readonly: '^.draft:=!{}'});
|
|
27
|
+
const display = pane.section({class: 'card stack'});
|
|
28
|
+
display.h2('3. Format a number, then wrap the text');
|
|
29
|
+
display.html_label('Amount', {for: 'amount-input'});
|
|
30
|
+
display.input({id: 'amount-input', type: 'number', value: '^.amount', live: true});
|
|
31
|
+
display.p('^.amount', {id: 'amount-display', format: '0.00', mask: 'Total: {} €'});
|
|
32
|
+
display.p('Hover to read the same amount in the title.', {id: 'amount-title',
|
|
33
|
+
title: "^.amount:=_F({}, {pattern: '0.00', mask: '{} €'})"});
|
|
34
|
+
display.p('This page uses Italian formatting. The amount in Data remains a number.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The page logic: this page names no methods. */
|
|
39
|
+
export class Logic {}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# 22 · Pointer templates and expressions
|
|
2
|
+
|
|
3
|
+
Resize a bar, enable a field and display an amount. The same typed Data drives all three: you learn when to produce text and when to keep a boolean or number.
|
|
4
|
+
|
|
5
|
+
**Uses:** Source, Data, attribute pointers and the page formatter — core 0.2.20. No Web Components or endpoint calls. Earlier lessons retain their release scope.
|
|
6
|
+
|
|
7
|
+
**Try it:** enter 80 in Width, then clear it. Turn Draft on to edit Name. Change Amount and compare the displayed text with the title shown on hover.
|
|
8
|
+
|
|
9
|
+
<details>
|
|
10
|
+
<summary>More detail</summary>
|
|
11
|
+
|
|
12
|
+
## One value, one template
|
|
13
|
+
|
|
14
|
+
Earlier lessons needed a supporting attribute to turn a number into CSS text:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
pane.div(width="${pixels}px", pixels="^.width")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The suffix expresses that same operation directly:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
pane.div(width="^.width:{}px")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Read `.width`, then replace `{}` in the text after `:`. A Data number 38 becomes the CSS string `38px`; Data still contains 38. The old multi-attribute template remains supported.
|
|
27
|
+
|
|
28
|
+
## An expression keeps its result type
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
pane.input(value="^.name", readonly="^.draft:=!{}")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
After `:=` comes a JavaScript expression. If Draft is false, `!{}` is true: Name is read-only. With Draft true it becomes editable. `:!{}` would create text instead, which is not the intended boolean result.
|
|
35
|
+
|
|
36
|
+
Keep `value` as a plain pointer: suffix transformations are not allowed on the widget value. They do not define how to convert edited text back into Data.
|
|
37
|
+
|
|
38
|
+
## Format first, mask second
|
|
39
|
+
|
|
40
|
+
For the content of a paragraph, ordinary attributes are the clearest choice:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
pane.p("^.amount", format="0.00", mask="Total: {} €")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`format` produces the number text using the page locale; `mask` wraps that text. This page declares `locale = "it-IT"`, so 1234.5 displays as `Total: 1234,50 €`.
|
|
47
|
+
|
|
48
|
+
For an attribute such as a tooltip, use the same formatter inside an expression:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
pane.p("Hover here", title="^.amount:=_F({}, {pattern: '0.00', mask: '{} €'})")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The first `{}` passes the typed amount. The quoted `'{} €'` is the mask passed to `_F`; it is not another pointer placeholder. `_F` calls this page's `gramlot.format` and does not create a global function.
|
|
55
|
+
|
|
56
|
+
## Empty values
|
|
57
|
+
|
|
58
|
+
An ordinary template slot requires a nonempty value. Null or an empty string makes the whole template null; the destination applies its usual null behavior. `0` and `False` are valid values. Use `{?}` to allow an empty string explicitly, for example `title="^.name:Customer: {?}"`. It still does not admit null.
|
|
59
|
+
|
|
60
|
+
Expressions receive typed values, including null. The standalone `mask` wraps formatted text; `gramlot.format(None)` returns empty text without applying the mask.
|
|
61
|
+
|
|
62
|
+
**Exercise:** change only `readonly="^.draft:=!{}"` to `readonly="^.draft"`. **Result:** the checkbox now has the opposite effect; the name binding does not change.
|
|
63
|
+
|
|
64
|
+
</details>
|
|
@@ -65,3 +65,12 @@ The date initialization issue in c17 and named-resource-host prerequisites remai
|
|
|
65
65
|
explicit gaps; the local file lesson does not imply HTTP service or mail delivery.
|
|
66
66
|
|
|
67
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.
|
|
74
|
+
|
|
75
|
+
**c22 · Pointer templates and expressions** follows c21 in core 0.2.20: repeat the CSS-width exercise with a shorter pointer, invert a boolean
|
|
76
|
+
and reuse the page formatter for text and attributes. It does not use Web Components.
|