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