@gramlot/gramlot-examples 0.2.15 → 0.3.0
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 +12 -5
- package/package.json +4 -3
- package/src/gramlot_examples/catalog.json +163 -1
- package/src/gramlot_examples/gallery/collections.js +4 -0
- package/src/gramlot_examples/gallery/dist/frame.js +88 -28
- package/src/gramlot_examples/gallery/dist/gallery.js +211 -52
- package/src/gramlot_examples/gallery/dist/inputs.js +24456 -0
- package/src/gramlot_examples/gallery/dist/inspector.js +42 -18
- package/src/gramlot_examples/gallery/dist/layout.js +541 -0
- package/src/gramlot_examples/gallery/dist/theme-styles.js +2 -2
- package/src/gramlot_examples/gallery/gallery-page.js +49 -17
- package/src/gramlot_examples/gallery/gallery.css +50 -4
- package/src/gramlot_examples/index.js +3 -2
- package/src/gramlot_examples/pages/README.md +5 -2
- package/src/gramlot_examples/pages/binding/06_style_and_visibility.md +77 -12
- package/src/gramlot_examples/pages/binding/09_forms.js +1 -1
- package/src/gramlot_examples/pages/components/01_textbox.js +32 -0
- package/src/gramlot_examples/pages/components/01_textbox.md +14 -0
- package/src/gramlot_examples/pages/components/02_checkbox.js +25 -0
- package/src/gramlot_examples/pages/components/02_checkbox.md +14 -0
- package/src/gramlot_examples/pages/components/03_radio_button_text.js +30 -0
- package/src/gramlot_examples/pages/components/03_radio_button_text.md +16 -0
- package/src/gramlot_examples/pages/components/04_text_box_area.js +23 -0
- package/src/gramlot_examples/pages/components/04_text_box_area.md +14 -0
- package/src/gramlot_examples/pages/components/05_passwordbox.js +23 -0
- package/src/gramlot_examples/pages/components/05_passwordbox.md +14 -0
- package/src/gramlot_examples/pages/components/06_radio_button.js +28 -0
- package/src/gramlot_examples/pages/components/06_radio_button.md +14 -0
- package/src/gramlot_examples/pages/components/07_colorpicker.js +24 -0
- package/src/gramlot_examples/pages/components/07_colorpicker.md +14 -0
- package/src/gramlot_examples/pages/components/08_horizontal_slider.js +24 -0
- package/src/gramlot_examples/pages/components/08_horizontal_slider.md +14 -0
- package/src/gramlot_examples/pages/components/09_vertical_slider.js +24 -0
- package/src/gramlot_examples/pages/components/09_vertical_slider.md +14 -0
- package/src/gramlot_examples/pages/components/10_number_textbox.js +30 -0
- package/src/gramlot_examples/pages/components/10_number_textbox.md +16 -0
- package/src/gramlot_examples/pages/components/11_date_textbox.js +23 -0
- package/src/gramlot_examples/pages/components/11_date_textbox.md +18 -0
- package/src/gramlot_examples/pages/components/12_time_textbox.js +23 -0
- package/src/gramlot_examples/pages/components/12_time_textbox.md +16 -0
- package/src/gramlot_examples/pages/components/13_filtering_select.js +23 -0
- package/src/gramlot_examples/pages/components/13_filtering_select.md +20 -0
- package/src/gramlot_examples/pages/components/14_callback_select.js +45 -0
- package/src/gramlot_examples/pages/components/14_callback_select.md +24 -0
- package/src/gramlot_examples/pages/components/15_combo_box.js +23 -0
- package/src/gramlot_examples/pages/components/15_combo_box.md +22 -0
- package/src/gramlot_examples/pages/components/16_checkbox_text.js +23 -0
- package/src/gramlot_examples/pages/components/16_checkbox_text.md +23 -0
- package/src/gramlot_examples/pages/components/17_date_calendar.js +25 -0
- package/src/gramlot_examples/pages/components/17_date_calendar.md +22 -0
- package/src/gramlot_examples/pages/components/18_validation.js +31 -0
- package/src/gramlot_examples/pages/components/18_validation.md +29 -0
- package/src/gramlot_examples/pages/components/19_formlet.js +26 -0
- package/src/gramlot_examples/pages/components/19_formlet.md +12 -0
- package/src/gramlot_examples/pages/components/20_group_box.js +26 -0
- package/src/gramlot_examples/pages/components/20_group_box.md +12 -0
- package/src/gramlot_examples/pages/components/21_switch.js +19 -0
- package/src/gramlot_examples/pages/components/21_switch.md +12 -0
- package/src/gramlot_examples/pages/components/22_button.js +18 -0
- package/src/gramlot_examples/pages/components/22_button.md +12 -0
- package/src/gramlot_examples/pages/components/23_toggle_button.js +18 -0
- package/src/gramlot_examples/pages/components/23_toggle_button.md +12 -0
- package/src/gramlot_examples/pages/components/24_dropdown_button.js +21 -0
- package/src/gramlot_examples/pages/components/24_dropdown_button.md +12 -0
- package/src/gramlot_examples/pages/components/25_validation_engine.js +51 -0
- package/src/gramlot_examples/pages/components/25_validation_engine.md +26 -0
- package/src/gramlot_examples/pages/components/README.md +40 -0
- package/src/gramlot_examples/pages/controllers/02_controller.js +1 -1
- package/src/gramlot_examples/pages/controllers/16_attribute_projection.js +3 -3
- package/src/gramlot_examples/pages/controllers/16_attribute_projection.md +8 -8
- 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 +3 -0
- package/src/gramlot_examples/pages/html_svg/06_forms.js +3 -3
|
@@ -16,20 +16,50 @@ export class GalleryPage extends IndexPage {
|
|
|
16
16
|
intro.p('This gallery shows each example page beside the code that creates it. ' +
|
|
17
17
|
'The Python and JavaScript integrations have the same examples.');
|
|
18
18
|
intro.p('Choose an example from the list. Its tab stays open so you can return to it. ' +
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'Choose Base or
|
|
19
|
+
'Open Settings (⚙) in the footer and enable Keyboard navigation to move through tabs by keyboard. ' +
|
|
20
|
+
'Use the top bar to show or hide Navigation and Code; drag their dividers to resize them. ' +
|
|
21
|
+
'Choose Base, Compact, mimi or joanna independently of the color mode.');
|
|
22
22
|
|
|
23
23
|
}
|
|
24
24
|
groupPanel(pane, group) { pane.div(group.readme, {id: `readme-${group.key}`}); }
|
|
25
25
|
footer(footer) {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
footer.html_button('⚙', {type: 'button', id: 'gallery-settings-toggle', class: 'gallery-settings-toggle',
|
|
27
|
+
title: 'Settings', aria_label: 'Settings', popovertarget: 'gallery-settings'});
|
|
28
|
+
const settings = footer.div({id: 'gallery-settings', popover: 'auto', aria_labelledby: 'gallery-settings-title'});
|
|
29
|
+
const heading = settings.div({class: 'gallery-settings-heading'});
|
|
30
|
+
heading.h2('Settings', {id: 'gallery-settings-title'});
|
|
31
|
+
heading.html_button('×', {type: 'button', aria_label: 'Close settings', popovertarget: 'gallery-settings', popovertargetaction: 'hide'});
|
|
32
|
+
super.footer(settings);
|
|
33
|
+
const labels = settings.div({class: 'index-theme'});
|
|
34
|
+
labels.html_label('Label position', {for: 'gallery-label-position'});
|
|
35
|
+
const position = labels.select({id: 'gallery-label-position', value: 'example'});
|
|
36
|
+
position.option('As in the example', {value: 'example', selected: true});
|
|
37
|
+
for (const code of ['TL', 'TC', 'TR', 'L', 'R', 'BL', 'BC', 'BR']) position.option(code, {value: code});
|
|
38
|
+
settings.p("Label position applies to input components in the previews. Choose As in the example to restore the lesson's layout.");
|
|
39
|
+
const inspector = footer.html_button({type: 'button', id: 'gallery-inspector', class: 'gallery-inspector',
|
|
28
40
|
title: 'Open inspector', aria_label: 'Inspector', aria_pressed: 'false', disabled: true});
|
|
29
41
|
inspector.span('🔍', {aria_hidden: 'true'});
|
|
30
42
|
|
|
31
43
|
}
|
|
32
|
-
|
|
44
|
+
header(shell) {
|
|
45
|
+
const header = shell.header({class: 'gallery-toolbar'});
|
|
46
|
+
header.html_button('☰ Navigation', {type: 'button', id: 'index-navigation-toggle',
|
|
47
|
+
aria_controls: 'index-sidebar', aria_expanded: 'true'});
|
|
48
|
+
header.h1(this.constructor.title);
|
|
49
|
+
header.html_button('Code ⟨/⟩', {type: 'button', id: 'index-code-toggle',
|
|
50
|
+
aria_controls: 'index-code', aria_expanded: 'false', disabled: true});
|
|
51
|
+
}
|
|
52
|
+
behavior(shell) {
|
|
53
|
+
shell.div({class: 'index-divider', id: 'index-code-divider', hidden: true,
|
|
54
|
+
title: 'Drag to resize code', role: 'separator', aria_orientation: 'vertical',
|
|
55
|
+
tabindex: 0, aria_label: 'Resize code', aria_controls: 'index-code',
|
|
56
|
+
aria_valuemin: 200, aria_valuemax: 800, aria_valuenow: 420});
|
|
57
|
+
const code = shell.aside({id: 'index-code', aria_label: 'Example source code', hidden: true});
|
|
58
|
+
for (const family of this.constructor.navigation) {
|
|
59
|
+
for (const example of family.children) this.codePanel(code, example);
|
|
60
|
+
}
|
|
61
|
+
shell.script({src: this.constructor.galleryScript});
|
|
62
|
+
}
|
|
33
63
|
|
|
34
64
|
panel(parent, example) {
|
|
35
65
|
const key = example.key;
|
|
@@ -38,21 +68,23 @@ export class GalleryPage extends IndexPage {
|
|
|
38
68
|
const explanation = panel.header({class: 'gallery-explanation'});
|
|
39
69
|
explanation.div(example.readme, {id: `readme-${key}`});
|
|
40
70
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
71
|
+
const preview = panel.div({class: 'gallery-preview', id: `preview-${key}`});
|
|
72
|
+
const modes = preview.div({class: 'gallery-preview-modes', role: 'group', aria_label: 'Preview mode'});
|
|
73
|
+
for (const [mode, caption] of [['normal', 'Normale'], ['mobile', 'Mobile']]) {
|
|
74
|
+
modes.html_button(caption, {type: 'button', id: `preview-${mode}-${key}`, aria_pressed: mode === 'normal' ? 'true' : 'false'});
|
|
75
|
+
}
|
|
76
|
+
const stage = preview.div({class: 'gallery-preview-stage', id: `preview-stage-${key}`});
|
|
77
|
+
stage.iframe({title: `${example.title} preview`, name: `example-${key}`,
|
|
45
78
|
id: `frame-${key}`});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const codepane = split.div({class: 'gallery-codepane', id: `codepane-${key}`});
|
|
79
|
+
}
|
|
80
|
+
codePanel(parent, example) {
|
|
81
|
+
const key = example.key;
|
|
82
|
+
const codepane = parent.div({class: 'gallery-codepane', id: `codepane-${key}`, hidden: true});
|
|
51
83
|
const tabs = codepane.div({class: 'gallery-source-tabs', role: 'tablist', aria_label: 'Source files'});
|
|
52
|
-
tabs.
|
|
84
|
+
tabs.html_button('JavaScript', {title: `${example.folder}.js`, type: 'button', role: 'tab',
|
|
53
85
|
id: `code-tab-${key}`, aria_controls: `code-panel-${key}`, aria_selected: 'true', tabindex: 0});
|
|
54
86
|
if (example.logic != null) {
|
|
55
|
-
tabs.
|
|
87
|
+
tabs.html_button('logic.js', {hidden: true, title: `${example.folder}_aux.js`, type: 'button', role: 'tab',
|
|
56
88
|
id: `logic-tab-${key}`, aria_controls: `logic-panel-${key}`, aria_selected: 'false', tabindex: -1});
|
|
57
89
|
}
|
|
58
90
|
codepane.pre({id: `code-panel-${key}`, role: 'tabpanel', aria_labelledby: `code-tab-${key}`, tabindex: 0})
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/* The source viewer is a sibling of navigation and content, never an iframe overlay. */
|
|
2
|
+
#gallery.index-shell {
|
|
3
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
4
|
+
grid-template-columns: var(--navigation-size, min(var(--sidebar-width, 240px), 60vw))
|
|
5
|
+
var(--navigation-divider, 8px) minmax(0, 1fr) var(--code-divider, 0px) var(--code-size, 0px);
|
|
6
|
+
}
|
|
7
|
+
.gallery-toolbar { grid-column: 1 / -1; grid-row: 1; display: flex; align-items: center;
|
|
8
|
+
gap: .6rem; padding: .35rem .6rem; background: var(--gramlot-surface-subtle);
|
|
9
|
+
border-block-start: 3px solid var(--gramlot-brand-yellow); border-block-end: 1px solid var(--gramlot-border); }
|
|
10
|
+
.gallery-toolbar h1 { flex: 1; min-inline-size: 0; margin: 0; font-size: 1rem;
|
|
11
|
+
text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
12
|
+
.gallery-toolbar button { margin: 0; padding: .25rem .5rem; min-block-size: 28px; font-size: .875rem; }
|
|
13
|
+
#gallery > .index-sidebar { grid-column: 1; grid-row: 2; border-block-start: 0; }
|
|
14
|
+
#gallery > .index-sidebar header h1 { display: none; }
|
|
15
|
+
#gallery > #index-sidebar-divider { grid-column: 2; grid-row: 2; }
|
|
16
|
+
#gallery > .index-main { grid-column: 3; grid-row: 2; }
|
|
17
|
+
#gallery > #index-code-divider { grid-column: 4; grid-row: 2; }
|
|
18
|
+
#index-code { grid-column: 5; grid-row: 2; min-inline-size: 0; min-block-size: 0; overflow: hidden; background: #1e1e1e; }
|
|
19
|
+
#gallery > .index-footer { grid-row: 3; }
|
|
20
|
+
#gallery[data-navigation-hidden="true"] { --navigation-size: 0px; --navigation-divider: 0px; }
|
|
21
|
+
#gallery[data-code-visible="true"] { --code-size: min(var(--code-width, 420px), 60vw); --code-divider: 8px; }
|
|
22
|
+
#gallery [hidden] { display: none; }
|
|
1
23
|
.gallery-panel { block-size: 100%; min-block-size: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
|
2
24
|
.gallery-explanation { max-block-size: 28dvh; overflow: auto; padding: .5rem .75rem; background: var(--gramlot-surface); border-block-end: 1px solid var(--gramlot-border); font-size: 1rem; }
|
|
3
25
|
.gallery-explanation h1 { font-size: 1.25rem; margin-block: 0 .35rem; }
|
|
@@ -7,11 +29,10 @@
|
|
|
7
29
|
.gallery-explanation :last-child { margin-block-end: 0; }
|
|
8
30
|
.gallery-description { font-weight: 600; }
|
|
9
31
|
.gallery-resources { display: flex; gap: .6rem; font-size: .95rem; }
|
|
10
|
-
.gallery-split { min-inline-size: 0; min-block-size: 0; display: grid; grid-template-columns: minmax(0, calc(var(--split-position) - 4px)) 8px minmax(0, 1fr); background: var(--gramlot-surface); }
|
|
11
32
|
.gallery-preview { min-inline-size: 0; min-block-size: 0; overflow: hidden; }
|
|
12
33
|
.gallery-preview iframe { display: block; inline-size: 100%; block-size: 100%; min-block-size: 0; border: 0; background: var(--gramlot-surface); }
|
|
13
|
-
.gallery-codepane { min-inline-size: 0; min-block-size: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); background: #1e1e1e; }
|
|
14
|
-
.gallery-codepane pre { min-inline-size: 0; min-block-size: 0; margin: 0; padding:
|
|
34
|
+
.gallery-codepane { block-size: 100%; min-inline-size: 0; min-block-size: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); background: #1e1e1e; }
|
|
35
|
+
.gallery-codepane pre { min-inline-size: 0; min-block-size: 0; margin: 0 8px 8px; padding: 12px 16px 16px 10px; border: 0; border-radius: 0; overflow: auto; font: 11px/1.5 var(--gramlot-font-code); background: #1e1e1e; color: #dcdcdc; }
|
|
15
36
|
.gallery-codepane code { white-space: pre; font-size: inherit; }
|
|
16
37
|
.gallery-codepane .hljs { color: #dcdcdc; counter-reset: code-line; }
|
|
17
38
|
.gallery-codepane .code-line::before { counter-increment: code-line; content: counter(code-line); display: inline-block; inline-size: 2.5em; margin-inline-end: .75em; padding-inline-end: .25em; text-align: end; color: #6e7681; user-select: none; }
|
|
@@ -28,7 +49,7 @@
|
|
|
28
49
|
.gallery-codepane :is(.hljs-attr, .hljs-attribute) { color: #9cdcfe; }
|
|
29
50
|
.gallery-codepane > pre[hidden] { display: none; }
|
|
30
51
|
.gallery-source-tabs { display: flex; min-inline-size: 0; overflow-x: auto; background: #2d2d30; }
|
|
31
|
-
.gallery-source-tabs button { flex: none; min-block-size: 28px; margin: 0; padding: .2rem .6rem; border: 0; border-radius: 0; background: transparent; color: #ccc; font: 600
|
|
52
|
+
.gallery-source-tabs button { flex: none; min-block-size: 28px; margin: 0; padding: .2rem .6rem; border: 0; border-radius: 0; background: transparent; color: #ccc; font: 600 11px/1.35 var(--gramlot-font-code); white-space: nowrap; }
|
|
32
53
|
.gallery-source-tabs button[aria-selected="true"] { background: #1e1e1e; color: #fff; box-shadow: inset 0 2px var(--gramlot-brand-blue); }
|
|
33
54
|
.gallery-source-tabs button:focus-visible { outline: 2px solid var(--gramlot-focus-ring); outline-offset: -2px; }
|
|
34
55
|
.gallery-source-tabs button[hidden] { display: none; }
|
|
@@ -37,3 +58,28 @@
|
|
|
37
58
|
.gallery-inspector:hover { background: var(--gramlot-surface); }
|
|
38
59
|
.gallery-inspector:focus-visible { outline: 2px solid var(--gramlot-focus-ring); outline-offset: 2px; }
|
|
39
60
|
.gallery-inspector[aria-pressed="true"] { background: var(--gramlot-action); color: var(--gramlot-navigation-text); }
|
|
61
|
+
|
|
62
|
+
/* Native popover keeps settings out of both the footer and lesson layout. */
|
|
63
|
+
.gallery-settings-toggle { inline-size: 28px; block-size: 28px; min-block-size: 28px;
|
|
64
|
+
padding: 0; margin: 0; border: 0; background: transparent; color: inherit; font-size: 18px; }
|
|
65
|
+
#gallery-settings { inset: auto auto 42px 10px; margin: 0; box-sizing: border-box;
|
|
66
|
+
inline-size: min(320px, calc(100vw - 20px)); max-block-size: calc(100dvh - 60px); overflow: auto;
|
|
67
|
+
padding: 12px; border: 1px solid var(--gramlot-border); border-radius: var(--gramlot-radius);
|
|
68
|
+
background: var(--gramlot-surface); color: var(--gramlot-ink); box-shadow: 0 4px 20px #0003; }
|
|
69
|
+
#gallery-settings:popover-open { display: grid; gap: 10px; }
|
|
70
|
+
.gallery-settings-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
71
|
+
.gallery-settings-heading h2 { margin: 0; font-size: 1rem; }
|
|
72
|
+
.gallery-settings-heading button { padding: 0 6px; min-block-size: 24px; font-size: 1rem; }
|
|
73
|
+
#gallery-settings .index-theme { display: grid; gap: 4px; }
|
|
74
|
+
#gallery-settings .index-theme select { inline-size: 100%; }
|
|
75
|
+
#gallery-settings .index-install { order: 2; }
|
|
76
|
+
#gallery-settings .index-install[open] { position: static; }
|
|
77
|
+
#gallery-settings p { margin: 0; font-size: .85rem; line-height: 1.4; }
|
|
78
|
+
|
|
79
|
+
/* Resize the existing preview; switching modes never replaces the iframe. */
|
|
80
|
+
.gallery-preview{display:grid;grid-template-rows:auto minmax(0,1fr)}
|
|
81
|
+
.gallery-preview-modes{display:flex;gap:4px;border-block-end:1px solid var(--gramlot-border);background:var(--gramlot-surface)}
|
|
82
|
+
.gallery-preview-modes button{font:inherit;font-size:12px;background:transparent;color:var(--gramlot-text-secondary);border:0;border-radius:0;border-block-end:2px solid transparent;padding:5px 12px;min-block-size:28px}
|
|
83
|
+
.gallery-preview-modes button[aria-pressed=true]{color:var(--gramlot-action);border-block-end-color:currentColor}
|
|
84
|
+
.gallery-preview-stage{min-block-size:0;min-inline-size:0;overflow:hidden;background:var(--gramlot-surface-subtle);display:flex;justify-content:center}
|
|
85
|
+
.gallery-preview-stage[data-preview-mode=mobile] iframe{inline-size:390px;max-inline-size:100%;flex:none;border-inline:1px solid var(--gramlot-border)}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {collections} from './gallery/collections.js';
|
|
1
2
|
/** Gramlot example pages, the gallery page and its catalogue. */
|
|
2
3
|
import {existsSync, readFileSync} from 'node:fs';
|
|
3
4
|
import {basename, extname, join} from 'node:path';
|
|
@@ -6,7 +7,7 @@ import {fileURLToPath} from 'node:url';
|
|
|
6
7
|
const PACKAGE = fileURLToPath(new URL('./', import.meta.url));
|
|
7
8
|
const MEDIA_TYPES = {'.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.json': 'application/json'};
|
|
8
9
|
const SUFFIXES = ['.py', '.js', '.css', '.md', '_aux.js'];
|
|
9
|
-
const DIST = ['gallery.js', 'frame.js', 'notices.json', 'LICENSE', 'NOTICE'];
|
|
10
|
+
const DIST = ['gallery.js', 'frame.js', 'inputs.js', 'layout.js', 'notices.json', 'LICENSE', 'NOTICE'];
|
|
10
11
|
|
|
11
12
|
/** Media type of a gallery asset; only the built gallery scripts are served as JavaScript. */
|
|
12
13
|
function mediaType(path, script = false) {
|
|
@@ -77,5 +78,5 @@ export function buildGallery({catalogs = []} = {}) {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
|
-
return {families, routes, assets};
|
|
81
|
+
return {families, routes, assets, collections};
|
|
81
82
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# Gramlot examples
|
|
2
2
|
|
|
3
3
|
The [gallery](https://github.com/gramlot-org/gramlot-examples/blob/main/docs/010-gallery.md) presents the same examples in Python and
|
|
4
|
-
JavaScript, in
|
|
4
|
+
JavaScript, in four families:
|
|
5
5
|
|
|
6
6
|
- [`html_svg/`](html_svg/): thirteen HTML and SVG pages, without binding.
|
|
7
7
|
- [`binding/`](binding/): eleven pages whose DOM follows the Data through pointers,
|
|
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
|
-
- [`controllers/`](controllers/):
|
|
10
|
+
- [`controllers/`](controllers/): twenty-two pages with formulas, controllers, named
|
|
11
11
|
logic in the `Logic` class of the page module, inline expressions, node methods, buttons,
|
|
12
12
|
events, the end-to-end story, `dataRpc` calls to the endpoints of the page, `remote`
|
|
13
13
|
fragments and `dataRemote` values.
|
|
14
14
|
|
|
15
|
+
- [`components/`](components/): twenty-five lessons for inputs, labelbox, layout,
|
|
16
|
+
command buttons and validation, with compact desktop/touch presentation.
|
|
17
|
+
|
|
15
18
|
Each example is a file page: `NN_name.py`, its JavaScript equivalent `NN_name.js`,
|
|
16
19
|
its README `NN_name.md`, and where needed a same-name stylesheet `NN_name.css`.
|
|
17
20
|
`NN_name.js` exports `Page` and `Logic`: the JavaScript page uses both, the Python
|
|
@@ -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
|
|
|
@@ -56,7 +56,7 @@ export class Page extends BasePage {
|
|
|
56
56
|
options.input({type: 'text', id: 'unavailable', value: 'Disabled', disabled: true});
|
|
57
57
|
options.html_label('Attachment', {for: 'attachment'});
|
|
58
58
|
options.input({type: 'file', id: 'attachment', name: 'attachment'});
|
|
59
|
-
form.
|
|
59
|
+
form.html_button('Submit unavailable', {type: 'submit', disabled: true});
|
|
60
60
|
|
|
61
61
|
const record = layout.aside({class: 'card stack'});
|
|
62
62
|
record.h2('The Data record');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
5
|
+
static title = "Textbox and labelbox";
|
|
6
|
+
static css = ["/themes/gramlot-base/theme.css"];
|
|
7
|
+
|
|
8
|
+
main(root) {
|
|
9
|
+
const pane = root.main({class: "example-page stack"});
|
|
10
|
+
pane.h1("One value, two textboxes");
|
|
11
|
+
pane.p("Type in either field. Both share the same Data value.");
|
|
12
|
+
const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
13
|
+
controls.html_label("Label position", {for: "position"});
|
|
14
|
+
const position = controls.select({id: "position", value: "^position", width: "8rem", max_width: "100%"});
|
|
15
|
+
for (const code of ["TL", "TC", "TR", "L", "R", "BL", "BC", "BR"]) {
|
|
16
|
+
position.option(code, {value: code});
|
|
17
|
+
}
|
|
18
|
+
controls.checkbox({value: "^live", lbl: "Editing", label: "Update while typing"});
|
|
19
|
+
controls.checkbox({value: "^readonly", lbl: "State", label: "Read only"});
|
|
20
|
+
const fields = pane.formlet({class: "card stack", width: "100%", max_width: "32rem"});
|
|
21
|
+
fields.textbox({id: "customer", value: "^customer", lbl: "Customer", lbl_position: "^position", live: "^live", readonly: "^readonly", placeholder: "Enter a name", lbl_color: "#315fa8", box_padding: "8px", box_l_padding: "4px", box_c_width: "240px"});
|
|
22
|
+
fields.textbox({id: "customer-copy", value: "^customer", lbl: "Same customer", live: "^live", readonly: "^readonly"});
|
|
23
|
+
pane.h2("The Data");
|
|
24
|
+
pane.p("^customer", {id: "customer-value"});
|
|
25
|
+
for (const [path, value] of [["customer", "Ada"], ["position", "TL"], ["live", true], ["readonly", false]]) {
|
|
26
|
+
pane.dataSetter({destination_path: path, value});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** This page needs no named client methods. */
|
|
32
|
+
export class Logic {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 01 · Textbox and labelbox
|
|
2
|
+
|
|
3
|
+
Two textboxes share a customer name. Type in either one, switch live editing off and on, and move the label without rebuilding the field.
|
|
4
|
+
|
|
5
|
+
**Uses:** the first 0.3 input components, shared labelbox, Source, Data and bindings. No endpoints or validation engine.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`lbl_` styles the label; `box_` styles the whole labelbox. `box_l_` and `box_c_` address its label and control containers. Read only stops editing while Data updates can still reach the field.
|
|
11
|
+
|
|
12
|
+
**Try it:** choose each of `TL`, `TC`, `TR`, `L`, `R`, `BL`, `BC`, `BR`. The default is `TL`. Open the inspector to see the same values in Data.
|
|
13
|
+
|
|
14
|
+
</details>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = "Checkbox and labelbox";
|
|
5
|
+
static css = ["/themes/gramlot-base/theme.css"];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const pane = root.main({class: "example-page stack"});
|
|
9
|
+
pane.h1("A boolean value");
|
|
10
|
+
pane.p("lbl names the field; label is the caption beside its checkbox.");
|
|
11
|
+
const fields = pane.formlet({class: "card stack", width: "100%", max_width: "32rem"});
|
|
12
|
+
fields.checkbox({id: "newsletter", value: "^newsletter", lbl: "Contact preferences", label: "Send me the newsletter", lbl_color: "#315fa8", box_padding: "8px", disabled: "^locked"});
|
|
13
|
+
fields.checkbox({id: "newsletter-copy", value: "^newsletter", lbl: "Same preference", label: "Newsletter", disabled: "^locked"});
|
|
14
|
+
const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
15
|
+
controls.checkbox({value: "^locked", lbl: "State", label: "Disable the preference"});
|
|
16
|
+
pane.p("Subscribed", {hidden: "^newsletter:=!{}", id: "subscribed"});
|
|
17
|
+
pane.p("Not subscribed", {hidden: "^newsletter", id: "not-subscribed"});
|
|
18
|
+
for (const [path, value] of [["newsletter", false], ["locked", false], ]) {
|
|
19
|
+
pane.dataSetter({destination_path: path, value});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** This page needs no named client methods. */
|
|
25
|
+
export class Logic {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 02 · Checkbox and labelbox
|
|
2
|
+
|
|
3
|
+
Two checkboxes share one boolean preference. Click either caption, then disable both using the State checkbox.
|
|
4
|
+
|
|
5
|
+
**Uses:** the first 0.3 input components, shared labelbox, Source, Data and bindings. No endpoints or validation engine.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`lbl` names the field; `label` is the caption beside the checkbox. The stored value is `True`/`False` in Python and `true`/`false` in JavaScript.
|
|
11
|
+
|
|
12
|
+
**Try it:** click either checkbox and inspect the shared boolean in Data. For layout variants, use Settings (⚙) in the gallery footer.
|
|
13
|
+
|
|
14
|
+
</details>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = "RadioButtonText and labelbox";
|
|
5
|
+
static css = ["/themes/gramlot-base/theme.css"];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const pane = root.main({class: "example-page stack"});
|
|
9
|
+
pane.h1("One code for each group");
|
|
10
|
+
pane.p("Choose a delivery option: the Data stores its code, not its caption. The second group has its own value. Arrow keys move between options.");
|
|
11
|
+
const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
12
|
+
controls.html_label("Columns (1–3): ", {for: "columns"}).span("^columns", {id: "columns-value"});
|
|
13
|
+
controls.input({id: "columns", type: "range", min: 1, max: 3, value: "^columns", live: true, width: "16rem", max_width: "100%"});
|
|
14
|
+
const fields = pane.formlet({class: "card stack", width: "100%", max_width: "36rem"});
|
|
15
|
+
const choices = "standard:Standard,express:Express,pickup:Pick up";
|
|
16
|
+
fields.radioButtonText({id: "shipping", value: "^shipping", values: choices, lbl: "Delivery", cols: "^columns", lbl_color: "#315fa8", box_padding: "8px"});
|
|
17
|
+
fields.radioButtonText({id: "alternative", value: "^alternative", values: choices, lbl: "Alternative delivery", cols: 3, box_padding: "8px"});
|
|
18
|
+
const facts = pane.dl();
|
|
19
|
+
facts.dt("Delivery code");
|
|
20
|
+
facts.dd("^shipping", {id: "shipping-value"});
|
|
21
|
+
facts.dt("Alternative code");
|
|
22
|
+
facts.dd("^alternative", {id: "alternative-value"});
|
|
23
|
+
for (const [path, value] of [["shipping", "standard"], ["alternative", "pickup"], ["columns", 3]]) {
|
|
24
|
+
pane.dataSetter({destination_path: path, value});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** This page needs no named client methods. */
|
|
30
|
+
export class Logic {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# 03 · RadioButtonText and labelbox
|
|
2
|
+
|
|
3
|
+
Choose one delivery option in each group. Change the column count, and try the arrow keys inside a group.
|
|
4
|
+
|
|
5
|
+
**Uses:** the first 0.3 input components, shared labelbox, Source, Data and bindings. No endpoints or validation engine.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`values="standard:Standard,express:Express,pickup:Pick up"` separates stored codes from visible captions. Each group writes only its own Data path; `cols` and `lbl_position` can follow pointers.
|
|
11
|
+
|
|
12
|
+
The controls stay above the groups so changing the number of rows does not move the slider while you drag it. The number beside **Columns** shows the current Data value, from 1 to 3.
|
|
13
|
+
|
|
14
|
+
**Try it:** click either checkbox and inspect the shared boolean in Data. For layout variants, use Settings (⚙) in the gallery footer.
|
|
15
|
+
|
|
16
|
+
</details>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = "Multiline text and labelbox";
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const pane = root.main({class: 'example-page stack'});
|
|
9
|
+
pane.h1("Multiline text and labelbox");
|
|
10
|
+
pane.p("Two multiline fields share one note. Switch live editing and move the label while keeping the text and selection.");
|
|
11
|
+
const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
12
|
+
controls.checkbox({id: "live", value: "^live", lbl: "Editing", label: "Update while editing"});
|
|
13
|
+
controls.checkbox({id: "locked", value: "^locked", lbl: "State", label: "Disable controls"});
|
|
14
|
+
const fields = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
15
|
+
fields.textBoxArea({id: "notes", value: "^notes", lbl: "Notes", rows: 4, cols: 24,
|
|
16
|
+
placeholder: "Write a few lines", live: "^live", disabled: "^locked", box_padding: "8px"});
|
|
17
|
+
fields.textBoxArea({id: "notes-copy", value: "^notes", lbl: "Same notes", rows: 3, cols: 24, live: "^live", disabled: "^locked"});
|
|
18
|
+
for (const [path, value] of [["notes", "First line\nSecond line"], ["locked", false], ["live", true]]) pane.dataSetter({destination_path: path, value});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** This page needs no named client methods. */
|
|
23
|
+
export class Logic {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 04 · Multiline text and labelbox
|
|
2
|
+
|
|
3
|
+
Two multiline fields share one note. Switch live editing and move the label while keeping the text and selection.
|
|
4
|
+
|
|
5
|
+
**Uses:** the 0.3 input collection, shared labelbox, Source, Data and bindings. No endpoint calls, stores or validation engine.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`textBoxArea` wraps a native textarea; `rows` and `cols` set its visible size. With live editing off, leave the field to commit. With it on, the second field follows each edit. Native `textarea` remains available in the earlier HTML lessons.
|
|
11
|
+
|
|
12
|
+
All controls use `lbl`, reactive `lbl_position` (TL by default), and the shared `lbl_`/`box_` style prefixes. Disable the controls to compare their common behavior. Label layout variants are in Settings (⚙) in the gallery footer.
|
|
13
|
+
|
|
14
|
+
</details>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = "Password and labelbox";
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const pane = root.main({class: 'example-page stack'});
|
|
9
|
+
pane.h1("Password and labelbox");
|
|
10
|
+
pane.p("Two password fields share a sample value. The browser masks the characters; changing either field updates the other.");
|
|
11
|
+
const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
12
|
+
controls.checkbox({id: "live", value: "^live", lbl: "Editing", label: "Update while editing"});
|
|
13
|
+
controls.checkbox({id: "locked", value: "^locked", lbl: "State", label: "Disable controls"});
|
|
14
|
+
const fields = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
15
|
+
fields.passwordbox({id: "secret", value: "^secret", lbl: "Sample password", live: "^live",
|
|
16
|
+
disabled: "^locked", placeholder: "Use a sample value", box_padding: "8px"});
|
|
17
|
+
fields.passwordbox({id: "secret-copy", value: "^secret", lbl: "Same sample password", live: "^live", disabled: "^locked"});
|
|
18
|
+
for (const [path, value] of [["secret", "sample-only"], ["locked", false], ["live", true]]) pane.dataSetter({destination_path: path, value});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** This page needs no named client methods. */
|
|
23
|
+
export class Logic {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 05 · Password and labelbox
|
|
2
|
+
|
|
3
|
+
Two password fields share a sample value. The browser masks the characters; changing either field updates the other.
|
|
4
|
+
|
|
5
|
+
**Uses:** the 0.3 input collection, shared labelbox, Source, Data and bindings. No endpoint calls, stores or validation engine.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`passwordbox` wraps a native password input. Masking changes its display, not the value in Data: this lesson uses a sample, and the inspector can read it. It has the same string, live editing and labelbox behavior as `textbox`.
|
|
11
|
+
|
|
12
|
+
All controls use `lbl`, reactive `lbl_position` (TL by default), and the shared `lbl_`/`box_` style prefixes. Disable the controls to compare their common behavior. Label layout variants are in Settings (⚙) in the gallery footer.
|
|
13
|
+
|
|
14
|
+
</details>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = "Single radio buttons";
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
|
|
7
|
+
main(root) {
|
|
8
|
+
const pane = root.main({class: 'example-page stack'});
|
|
9
|
+
pane.h1("Single radio buttons");
|
|
10
|
+
pane.p("Each radio binds its own boolean. A shared group makes the choices exclusive; a separate group stays independent.");
|
|
11
|
+
const controls = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
12
|
+
controls.checkbox({id: "locked", value: "^locked", lbl: "State", label: "Disable controls"});
|
|
13
|
+
const fields = pane.formlet({class: "card stack", width: "100%", max_width: "24rem"});
|
|
14
|
+
for (const [code, caption] of [["standard", "Standard"], ["express", "Express"], ["pickup", "Pick up"]]) {
|
|
15
|
+
fields.radioButton({id: code, value: `^${code}`, group: "delivery", label: caption,
|
|
16
|
+
lbl: caption, disabled: "^locked", box_padding: "4px"});
|
|
17
|
+
}
|
|
18
|
+
fields.radioButton({id: "independent", value: "^independent", group: "other", label: "Independent choice", lbl: "Another group", disabled: "^locked"});
|
|
19
|
+
const facts = pane.dl();
|
|
20
|
+
for (const code of ["standard", "express", "pickup", "independent"]) {
|
|
21
|
+
facts.dt(code); facts.dd(`^${code}`, {id: `${code}-value`});
|
|
22
|
+
}
|
|
23
|
+
for (const [path, value] of [["standard", true], ["express", false], ["pickup", false], ["independent", false], ["locked", false]]) pane.dataSetter({destination_path: path, value});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** This page needs no named client methods. */
|
|
28
|
+
export class Logic {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 06 · Single radio buttons
|
|
2
|
+
|
|
3
|
+
Each radio binds its own boolean. A shared group makes the choices exclusive; a separate group stays independent.
|
|
4
|
+
|
|
5
|
+
**Uses:** the 0.3 input collection, shared labelbox, Source, Data and bindings. No endpoint calls, stores or validation engine.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`radioButton` uses one boolean per control; `group` names the exclusive group within the current Source form (or page). Selecting Express writes `express=True` and clears the selected peer. Use arrow keys between choices or click the captions. Compare w03: `radioButtonText` stores one selected code instead. These are two different value contracts.
|
|
11
|
+
|
|
12
|
+
All controls use `lbl`, reactive `lbl_position` (TL by default), and the shared `lbl_`/`box_` style prefixes. Disable the controls to compare their common behavior. Label layout variants are in Settings (⚙) in the gallery footer.
|
|
13
|
+
|
|
14
|
+
</details>
|