@gramlot/gramlot-examples 0.2.15 → 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 CHANGED
@@ -12,7 +12,7 @@ npm install @gramlot/gramlot-examples
12
12
  [![PyPI](https://img.shields.io/pypi/v/gramlot-examples)](https://pypi.org/project/gramlot-examples/)
13
13
  [![npm](https://img.shields.io/npm/v/@gramlot/gramlot-examples)](https://www.npmjs.com/package/@gramlot/gramlot-examples)
14
14
 
15
- Current release: 0.2.15.
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
@@ -36,7 +40,7 @@ same-name README:
36
40
  end-to-end story, `dataRpc` calls to the endpoints of the page, `remote` fragments and
37
41
  `dataRemote` values (`c01`–`c21`). Later lessons introduce topics, authored references, native ask,
38
42
  parent/child messages, plain HTML buttons and delayed Gramlot clicks. The package
39
- requires core 0.2.19 or later; each lesson states which features it uses.
43
+ requires core 0.2.20 or later; each lesson states which features it uses.
40
44
 
41
45
  Each `NN_name.js` exports `Page` and `Logic`; the Python page `NN_name.py` takes its
42
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.15",
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.19"
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 \u00d7 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."
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",
@@ -323,6 +323,12 @@
323
323
  "title": "Plain and delayed buttons",
324
324
  "folder": "21_plain_and_delayed_buttons",
325
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."
326
332
  }
327
333
  ]
328
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
- ## Read the page
19
+ ## From Python to Source to DOM
20
20
 
21
- The box binds class, style, color, background, size, radius and visibility to Data. Supported class/style values are strings or null. CSS shortcuts compose the style and take precedence over a duplicate property in `style`.
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
- `visible=False` sets hidden visibility but retains space; native `hidden=True` removes the box from layout. Use CSS units such as `1.5rem` for font size.
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
- Python builds Source on the server; its JavaScript counterpart builds it on Node/Bun or in a standalone Worker. Bindings, actions and any Logic execute in the browser; Python strings containing JavaScript are not executed by Python.
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
- ## Try it and predict the result
40
+ For comparison, a literal declaration has no Data dependency:
28
41
 
29
- 1. Expect a blue box, white text, dashed border, 1.5rem font and rounded corners.
30
- 2. Replace the style field with `color: red` and leave it: the color shortcut still makes the text white; replacing the style string also removes its old border declaration.
31
- 3. Uncheck visible: the space remains. Then check hidden: the space collapses.
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
- ## Change it and check yourself
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
- Restore visible, clear hidden and choose 2rem with 24px corners.
49
+ ## Try the binding
36
50
 
37
- **Answer:** the same box changes appearance from Data; its text and Source structure are unchanged. A null shortcut removes its own property, not every other style declaration.
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. The development review has a pending renderer-parity correction for rejection of unsupported class/style types; this lesson does not teach object coercions or rely on them.
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}: %s`, id: 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, %s', id: 'greeting'});
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, %s")
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, %s` and the title to `Guest: ${customer}`. Leave the Data pointer alone.
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,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>
@@ -71,3 +71,6 @@ After the messaging lessons, **c20 · Actions, confirmations and references**
71
71
  completes the 0.2.18 button examples. **c21 · Plain and delayed buttons** then
72
72
  shows the 0.2.19 additions, comparing the same action before
73
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.