@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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
export class Page extends BasePage {
|
|
3
|
+
static title = 'Formlet and shared labels';
|
|
4
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
main(root) {
|
|
7
|
+
const pane = root.main({class:'example-page stack'});
|
|
8
|
+
pane.h1('One layout, shared label settings');
|
|
9
|
+
pane.p('Change the columns. The fields keep their values and their own label overrides.');
|
|
10
|
+
const controls = pane.formlet({class:'card stack',max_width:'24rem'});
|
|
11
|
+
controls.html_label('Columns',{for:'columns'});
|
|
12
|
+
const select = controls.select({id:'columns',value:'^columns',width:'6rem'});
|
|
13
|
+
select.option('One',{value:'1'});select.option('Two',{value:'2'});
|
|
14
|
+
const fields = pane.formlet({id:'fields',columns:'^columns:=Number({})',datapath:'customer',
|
|
15
|
+
lbl_position:'L',lbl_text_align:'right',box_c_width:'100%',max_width:'44rem',gap:'10px 16px'});
|
|
16
|
+
fields.textbox({id:'first-name',value:'^.name',lbl:'Name'});
|
|
17
|
+
fields.textbox({value:'^.surname',lbl:'Surname'});
|
|
18
|
+
fields.textbox({value:'^.city',lbl:'City'});
|
|
19
|
+
fields.checkbox({value:'^.enabled',lbl:'Active',mode:'switch'});
|
|
20
|
+
fields.textBoxArea({value:'^.notes',lbl:'Notes (override)',lbl_position:'TL',rows:2});
|
|
21
|
+
pane.p('^customer.name',{id:'accepted-name'});
|
|
22
|
+
for(const [destination_path,value] of [['columns','2'],['customer.name','Ada'],['customer.surname','Lovelace'],
|
|
23
|
+
['customer.city','London'],['customer.enabled',true],['customer.notes','A shared layout']]) pane.dataSetter({destination_path,value});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class Logic {}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 19 · Formlet and shared labels
|
|
2
|
+
|
|
3
|
+
Arrange fields in columns and share label settings. Changing the layout preserves each field and its draft.
|
|
4
|
+
|
|
5
|
+
**Uses:** the 0.3 inputs and layout collections, labelbox and Source/Data bindings. No database, form-save service or endpoints.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`formlet` automatically sizes left labels to the widest label in each rendered column, without fixed label widths. It supplies a grid and shared `lbl_*` / `box_*` defaults. A field's own declaration wins. Defaults that contain pointers resolve in the formlet's Data scope. Layout updates preserve the same controls, focus and uncommitted text. `columns` accepts an integer or CSS track list; `col_min_width` selects a responsive grid, as demonstrated by GroupBox. Use the gallery settings to preview other label positions.
|
|
11
|
+
|
|
12
|
+
</details>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
export class Page extends BasePage {
|
|
3
|
+
static title = 'GroupBox and typed copy';
|
|
4
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
static locale = 'it-IT';
|
|
7
|
+
async main(root) {
|
|
8
|
+
const {Bag} = await import('@gramlot/gramlot');
|
|
9
|
+
const pane = root.main({class:'example-page stack'});
|
|
10
|
+
pane.h1('A group with its own Data scope');
|
|
11
|
+
pane.p('The copy icon writes TYTX. Use the arrow beside it to choose JSON instead.');
|
|
12
|
+
const group = pane.groupBox({id:'customer-group',lbl:'Customer',datapath:'customer',copy:true,max_width:'44rem'});
|
|
13
|
+
const fields = group.formlet({col_min_width:'18rem',lbl_position:'L',box_c_width:'100%'});
|
|
14
|
+
fields.textbox({value:'^.name',lbl:'Name'});
|
|
15
|
+
fields.dateTextBox({value:'^.date',lbl:'Date',format:'short'});
|
|
16
|
+
fields.numberTextBox({value:'^.amount',lbl:'Exact amount',dtype:'N'});
|
|
17
|
+
fields.checkbox({value:'^.enabled',lbl:'Active',mode:'switch'});
|
|
18
|
+
const other = pane.groupBox({lbl:'Outside the copied group',datapath:'other',lbl_variant:'underline',max_width:'24rem'});
|
|
19
|
+
other.textbox({value:'^.note',lbl:'Note',box_c_width:'100%'});
|
|
20
|
+
pane.p('JSON is values-only: use TYTX when types and Bag node attributes must survive the round trip.');
|
|
21
|
+
const typed = Bag.fromTytx('{"date":"2026-10-17::D","amount":"1234.50::N"}');
|
|
22
|
+
for(const [destination_path,value] of [['customer.name','Ada'],['customer.date',typed.getItem('date')],
|
|
23
|
+
['customer.amount',typed.getItem('amount')],['customer.enabled',true],['other.note','This text is not copied']]) pane.dataSetter({destination_path,value});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class Logic {}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 20 · GroupBox and typed copy
|
|
2
|
+
|
|
3
|
+
Group customer data under a heading. Copy TYTX with its types or choose plain JSON from the copy menu.
|
|
4
|
+
|
|
5
|
+
**Uses:** the 0.3 inputs and layout collections, labelbox and Source/Data bindings. No database, form-save service or endpoints.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`groupBox` supplies a heading around slotted Source children. `lbl_variant="underline"` gives a lighter heading. `copy=True` reads the current Bag at the group's explicit `datapath`, including nested data. The default clipboard text is TYTX, preserving supported types and node attributes. The menu's JSON choice uses Bag values as an ordinary object; it omits node attributes and can lose type distinctions. It does not copy rendered text, rejected drafts or unrelated Data. The browser must permit clipboard writing; a denied operation is reported beside the copy action without changing Data.
|
|
11
|
+
|
|
12
|
+
</details>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
export class Page extends BasePage {
|
|
3
|
+
static title = 'Checkbox as a switch';
|
|
4
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
main(root) {
|
|
7
|
+
const pane = root.main({class:'example-page stack'});
|
|
8
|
+
pane.h1('One boolean, two presentations');
|
|
9
|
+
pane.p('Click a caption or focus the switch and press Space. Both controls share the same Data.');
|
|
10
|
+
const fields = pane.formlet({columns:1,max_width:'26rem',lbl_position:'L'});
|
|
11
|
+
fields.checkbox({id:'notifications-switch',value:'^enabled',lbl:'Notifications',label:'Email updates',mode:'switch',switch_color:'^switch_color',readonly:'^readonly',disabled:'^disabled'});
|
|
12
|
+
fields.checkbox({id:'notifications-check',value:'^enabled',lbl:'Same boolean',label:'Email updates',readonly:'^readonly',disabled:'^disabled'});
|
|
13
|
+
fields.checkbox({value:'^readonly',lbl:'Read only',mode:'switch'});
|
|
14
|
+
fields.checkbox({value:'^disabled',lbl:'Disabled',mode:'switch'});
|
|
15
|
+
pane.p('^enabled',{id:'enabled-value'});
|
|
16
|
+
for(const [destination_path,value] of [['switch_color','purple'],['enabled',true],['readonly',false],['disabled',false]]) pane.dataSetter({destination_path,value});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class Logic {}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 21 · Checkbox as a switch
|
|
2
|
+
|
|
3
|
+
A switch and a checkbox share one boolean. Compare their appearance, captions and read-only state.
|
|
4
|
+
|
|
5
|
+
**Uses:** the 0.3 inputs and layout collections, labelbox and Source/Data bindings. No database, form-save service or endpoints.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`checkbox(mode="switch")` retains the existing boolean value, labelbox, validation and native checkbox editing. Its internal input exposes `role="switch"`; Space toggles it. `readonly` and `disabled` prevent changes. The default mode remains `checkbox`. The first switch uses `switch_color="^switch_color"`, initially purple. Change that Data value to change its active track color; set it to null to restore the theme. The off state and the other switches keep their theme colors. Otherwise colors follow the theme and the touch mode enlarges the clickable caption area.
|
|
11
|
+
|
|
12
|
+
</details>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
export class Page extends BasePage {
|
|
3
|
+
static title = 'Button commands';
|
|
4
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
main(root) {
|
|
7
|
+
const pane=root.main({class:'example-page stack'});
|
|
8
|
+
pane.h1('One shared command engine');
|
|
9
|
+
pane.p('Confirm the action, or click the delayed command several times: only the last burst is delivered.');
|
|
10
|
+
const actions=pane.div({class:'card stack',max_width:'26rem'});
|
|
11
|
+
actions.button('Confirm action',{id:'command-confirm',icon:'✓',action:"this.SET('result', 'Confirmed')",ask:'Run this command?'});
|
|
12
|
+
actions.button('Delayed command',{id:'command-delay',_delay:300,action:"this.SET('result', 'Delayed: ' + _counter)"});
|
|
13
|
+
actions.html_button('Plain HTML',{type:'button',id:'command-native',connect_onclick:"this.SET('result', 'Explicit HTML event')"});
|
|
14
|
+
pane.p('^result',{id:'command-result'});
|
|
15
|
+
pane.dataSetter({destination_path:'result',value:'No command yet'});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class Logic {}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 22 · Button commands
|
|
2
|
+
|
|
3
|
+
Compare the Gramlot command button with plain HTML. Try confirmation and a delayed command.
|
|
4
|
+
|
|
5
|
+
**Uses:** built-in command components, Source, Data and the shared action engine; the toggle example also uses inputs and layout. No endpoints or database.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`button` is a Web Component with an internal native button. It reuses action, ask, fire, nested controllers and _delay. `html_button` stays plain HTML, including native form submission. Here its explicit click connection updates Data. Gramlot command buttons do not implicitly submit forms.
|
|
11
|
+
|
|
12
|
+
</details>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
export class Page extends BasePage {
|
|
3
|
+
static title = 'Toggle button with LED';
|
|
4
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
main(root) {
|
|
7
|
+
const pane=root.main({class:'example-page stack'});
|
|
8
|
+
pane.h1('A boolean command with an LED');
|
|
9
|
+
const fields=pane.formlet({columns:1,max_width:'26rem',lbl_position:'L'});
|
|
10
|
+
fields.toggleButton('Enabled',{id:'toggle-command',value:'^enabled',readonly:'^readonly',disabled:'^disabled',action:"this.SET('result', this.GET('enabled') ? 'Enabled' : 'Disabled')"});
|
|
11
|
+
fields.checkbox({value:'^enabled',lbl:'Same Data',id:'toggle-checkbox'});
|
|
12
|
+
fields.checkbox({value:'^readonly',lbl:'Read only'});
|
|
13
|
+
fields.checkbox({value:'^disabled',lbl:'Disabled'});
|
|
14
|
+
pane.p('^result',{id:'toggle-result'});
|
|
15
|
+
for(const [destination_path,value] of [['enabled',false],['readonly',false],['disabled',false],['result','No command yet']]) pane.dataSetter({destination_path,value});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class Logic {}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 23 · Toggle button with LED
|
|
2
|
+
|
|
3
|
+
A command button and a checkbox share a boolean. The LED and aria-pressed follow Data.
|
|
4
|
+
|
|
5
|
+
**Uses:** built-in command components, Source, Data and the shared action engine; the toggle example also uses inputs and layout. No endpoints or database.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`toggleButton(value="^enabled")` writes a boolean through the shared Data adapter. The LED and aria-pressed expose the current state; Space and Enter activate the native internal button. The state changes before its optional action; an action confirmation does not undo the state change. Readonly and disabled block user toggling, while external Data can still change the state.
|
|
11
|
+
|
|
12
|
+
</details>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
export class Page extends BasePage {
|
|
3
|
+
static title = 'Dropdown commands and persistence';
|
|
4
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
5
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
6
|
+
main(root) {
|
|
7
|
+
const pane=root.main({class:'example-page stack'});
|
|
8
|
+
pane.h1('Execute, or execute and remember');
|
|
9
|
+
pane.p('Click a menu entry to execute it. Hold Shift while selecting it to also change the main action and caption. Reload to verify persistence.');
|
|
10
|
+
const actions=pane.div({class:'card stack',max_width:'26rem'});
|
|
11
|
+
const dropdown=actions.dropDownButton('Export',{id:'export-command',icon:'↓',persist:true,action:"this.SET('result', 'Default export')"});
|
|
12
|
+
const menu=dropdown.menu();
|
|
13
|
+
menu.menuline('Export CSV',{code:'csv',id:'export-csv',action:"this.SET('result', 'CSV export')"});
|
|
14
|
+
menu.menuline('Export PDF',{code:'pdf',id:'export-pdf',action:"this.SET('result', 'PDF export')"});
|
|
15
|
+
menu.menuline('-');
|
|
16
|
+
menu.menuline('Unavailable',{code:'unavailable',disabled:true,action:"this.SET('result', 'Unavailable')"});
|
|
17
|
+
pane.p('^result',{id:'dropdown-result'});
|
|
18
|
+
pane.dataSetter({destination_path:'result',value:'No command yet'});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class Logic {}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 24 · Dropdown commands and persistence
|
|
2
|
+
|
|
3
|
+
The main surface executes; the arrow opens the menu. Shift-select a command to run it and make it the default.
|
|
4
|
+
|
|
5
|
+
**Uses:** built-in command components, Source, Data and the shared action engine; the toggle example also uses inputs and layout. No endpoints or database.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>More detail</summary>
|
|
9
|
+
|
|
10
|
+
`dropDownButton` is one split command: the main surface never opens its menu. The arrow opens it; arrow keys, Home/End and Escape navigate or close it. Normal selection only executes; Shift+selection executes and changes the default action/caption. `persist=True` requires a stable id and saves the item code for this page URL in browser storage. Nothing executable is stored. On touch, ordinary selection executes; setting a default currently requires Shift. This is a local flat menu, without nested menus or button bars.
|
|
11
|
+
|
|
12
|
+
</details>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {Page as BasePage} from '@gramlot/gramlot/page';
|
|
2
|
+
|
|
3
|
+
export class Page extends BasePage {
|
|
4
|
+
static title = 'Validation rules and callbacks';
|
|
5
|
+
static devices = ['desktop', 'tablet', 'phone'];
|
|
6
|
+
static css = ['/themes/gramlot-base/theme.css'];
|
|
7
|
+
main(root) {
|
|
8
|
+
const pane = root.main({class:'example-page stack'});
|
|
9
|
+
pane.h1('Validation playground');
|
|
10
|
+
pane.p('Leave a field to validate it. Invalid drafts stay visible; the accepted values below change only after success.');
|
|
11
|
+
const group = pane.groupBox({id:'validation-group',datapath:'profile',copy:true,lbl:'Profile · field validations',max_width:'36rem'});
|
|
12
|
+
const fields = group.formlet({id:'validation-fields',lbl_position:'L',box_c_width:'100%'});
|
|
13
|
+
fields.textbox({id:'full-name',value:'^.full_name',lbl:'Name (2–30)',validate_notnull:true,validate_len:'2:30',validate_notnull_error:'Please enter a name.',validate_len_error:'Use 2–30 characters.'});
|
|
14
|
+
fields.textbox({id:'code',value:'^.code',lbl:'Code (2–6 letters)',validate_case:'upper',validate_regex:'(?:^[A-Z]{2,6}$)',validate_regex_error:'Use 2–6 letters.'});
|
|
15
|
+
fields.textbox({id:'fallback',value:'^.fallback',lbl:'Empty becomes Guest',validate_empty:'Guest'});
|
|
16
|
+
fields.numberTextBox({id:'quantity',value:'^.quantity',lbl:'Quantity: 0–10',validate_min:0,validate_max:10});
|
|
17
|
+
fields.checkbox({id:'strict',value:'^.strict',lbl:'Email',label:'Make the warning blocking'});
|
|
18
|
+
fields.textbox({id:'email',value:'^.email',lbl:'Email format',validate_email:true,validate_email_iswarning:'^.strict:=!{}'});
|
|
19
|
+
fields.checkbox({value:'^.check_length',lbl:'Length',label:'Require at least four characters'});
|
|
20
|
+
fields.textbox({id:'conditional',value:'^.conditional',lbl:'Conditional length',validate_len:'4:',validate_len_if:'enabled',validate_enabled:'^.check_length'});
|
|
21
|
+
fields.textbox({id:'nickname',value:'^.nickname',lbl:'Nickname',validate_call:'checkNickname',validate_onAccept:'accepted',validate_onReject:'rejected'});
|
|
22
|
+
fields.textbox({id:'remote-name',value:'^.remote_name',lbl:'Remote name',validate_remote:'check_name',validate_timeout:5000});
|
|
23
|
+
fields.textBoxArea({id:'notes',value:'^.notes',lbl:'Notes (max 40)',rows:2,validate_len:':40',validate_len_error:'Use at most 40 characters.'});
|
|
24
|
+
fields.dateTextBox({id:'appointment',value:'^.appointment',lbl:'Appointment',format:'short',validate_notnull:true,validate_notnull_error:'Choose a date or type a date expression.'});
|
|
25
|
+
fields.filteringSelect({id:'delivery',value:'^.delivery',lbl:'Delivery',values:'standard:Standard,express:Express,pickup:Pick up',validate_notnull:true,validate_notnull_error:'Choose a delivery option.'});
|
|
26
|
+
group.p('Try an empty name, quantity 11, a malformed email, or a long note. The local callback rejects admin; the asynchronous endpoint rejects taken.');
|
|
27
|
+
const accepted = pane.details({max_width:'36rem',open:true});
|
|
28
|
+
accepted.summary('Accepted Data · invalid drafts do not overwrite these values');
|
|
29
|
+
const values = accepted.dl();
|
|
30
|
+
for (const [label,path] of [['Name','full_name'],['Code','code'],['Quantity','quantity'],['Email','email'],['Notes','notes'],['Delivery code','delivery'],['Remote name','remote_name']]) {
|
|
31
|
+
values.dt(label);
|
|
32
|
+
values.dd('^profile.'+path, {id: {code:'code-result',email:'email-result',remote_name:'remote-result'}[path]});
|
|
33
|
+
}
|
|
34
|
+
values.dt('Appointment');
|
|
35
|
+
values.dd('^profile.appointment',{format:'short'});
|
|
36
|
+
accepted.p('^profile.validation_status',{id:'validation-status'});
|
|
37
|
+
for (const [destination_path,value] of [['full_name','Ada Lovelace'],['notes','Please ring the bell.'],['appointment',new Date('2026-10-17T00:00:00Z')],['delivery','standard'],['code','AB'],['fallback','Guest'],['quantity',2],['email',''],['strict',false],['check_length',false],['conditional','Ada'],['nickname','Ada'],['remote_name','AVAILABLE'],['validation_status','Edit a nickname.']]) pane.dataSetter({destination_path:'profile.'+destination_path,value});
|
|
38
|
+
}
|
|
39
|
+
check_name({value}) {
|
|
40
|
+
return value.toLowerCase() === 'taken' ? {errorcode:'reserved',message:'That name is reserved.'} : {value:value.toUpperCase()};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
Page.registerEndpoint('check_name');
|
|
44
|
+
|
|
45
|
+
export class Logic {
|
|
46
|
+
checkNickname(node, {value}) {
|
|
47
|
+
return value.toLowerCase() === 'admin' ? {errorcode:'reserved',message:'Choose a nickname other than admin.'} : true;
|
|
48
|
+
}
|
|
49
|
+
accepted(node, {value}) { node.SET('.validation_status', 'Accepted: ' + value); }
|
|
50
|
+
rejected(node, {result}) { node.SET('.validation_status', 'Rejected: ' + result.error); }
|
|
51
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# 25 · Validation rules and callbacks
|
|
2
|
+
|
|
3
|
+
A labelled groupBox contains a compact formlet with text, multiline text, number, date, selection and boolean controls. Try required values, length, normalization, conditional rules, warnings and asynchronous validation. The group copy icon copies the accepted `profile` Bag as TYTX; its menu also offers JSON. Invalid drafts are not copied. The expandable accepted-Data panel shows what actually changed.
|
|
4
|
+
|
|
5
|
+
**Uses:** shared input validation, Source conditions, named Logic and typed RPC. The endpoint also runs in the serverless Worker. No database or form-save service.
|
|
6
|
+
|
|
7
|
+
<details><summary>More detail</summary>
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
group = pane.groupBox(datapath="profile", copy=True, lbl="Profile")
|
|
11
|
+
pane = group.formlet(lbl_position="L")
|
|
12
|
+
pane.textbox(value="^.code", validate_case="upper", validate_regex="(?:^[A-Z]{2,6}$)")
|
|
13
|
+
pane.textbox(value="^.email", validate_email=True, validate_email_iswarning="^.strict:=!{}")
|
|
14
|
+
pane.textbox(value="^.nickname", validate_call="checkNickname")
|
|
15
|
+
pane.textbox(value="^.remote_name", validate_remote="check_name")
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Clear Name or Appointment to test required values. Write more than 40 characters in Notes. Delivery accepts a listed choice and cannot be empty. The groupBox declares `datapath="profile"` once; its formlet inherits it and fields use relative `^.field` pointers. The formlet automatically aligns left labels without fixed label widths.
|
|
19
|
+
|
|
20
|
+
Try `abc` in Code: the accepted value becomes `ABC`. Clear the fallback: it becomes Guest. Quantity rejects values outside 0–10. A malformed email is accepted with a warning unless the blocking switch is on. The length condition reads the resolved `validate_enabled` parameter as `enabled`.
|
|
21
|
+
|
|
22
|
+
The local callback reserves `admin`. The endpoint reserves `taken` and normalizes accepted names to uppercase. It returns a validation result; it does not write Data. While a request is pending, Data retains its last value. Editing again, changing the rules or removing the field makes the old response obsolete. Timeout and transport errors reject the candidate.
|
|
23
|
+
|
|
24
|
+
`validate_onAccept` runs after the Data write. `validate_onReject` sees the rejected candidate while Data still contains its previous value. Named Logic receives `(node, {value, result, validations, userChange, ...parameters})`; small inline conditions/bodies must originate in activated Source. Full syntax is documented in the core component guide.
|
|
25
|
+
|
|
26
|
+
</details>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Input components
|
|
2
|
+
|
|
3
|
+
This section uses only the first input components, their shared labelbox, Source,
|
|
4
|
+
Data and bindings. It revisits native forms and binding with `textbox`, `checkbox`,
|
|
5
|
+
`radioButtonText`, multiline/password fields, single radios, colors and sliders;
|
|
6
|
+
endpoint calls and stores are outside this section; the final lesson introduces basic validation.
|
|
7
|
+
|
|
8
|
+
The gallery activates the `inputs` collection once in its application configuration.
|
|
9
|
+
Python uses its generated JSON grammar; the browser loads its JavaScript implementation.
|
|
10
|
+
The widget names are the same in Python and JavaScript.
|
|
11
|
+
|
|
12
|
+
Start with Textbox, then compare the boolean checkbox and the single-choice radio
|
|
13
|
+
group. All controls use `lbl`, the same reactive `lbl_position` and the `lbl_`/`box_`
|
|
14
|
+
style prefixes. The controls belong to the shared inputs collection.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
Continue with **Filtering select** for searchable local code/caption pairs, then
|
|
18
|
+
**Callback select** for the same interaction backed by 100 stable random records
|
|
19
|
+
in a Data Bag. These lessons add named browser Logic, with no endpoint calls.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
**Combo box** allows free text as well as suggestions; **Checkbox group** stores
|
|
23
|
+
several selected codes together. Compare them with Filtering select and
|
|
24
|
+
RadioButtonText to understand the different value contracts.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
**Date calendar (w17):** one civil date shared by an inline calendar, a popup field and a text-only field. Compare `dateCalendar`, the default `dateTextBox(calendar=True)` and `calendar=False`. The same labelbox and compact sizes apply.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
**Required values and length (w18):** compare a rejected draft with the last accepted Data value. Shared validation covers required text, text length and a required number, with compact labelbox fields and a live-edit switch.
|
|
31
|
+
|
|
32
|
+
Use **Settings (⚙)** in the gallery footer to compare themes, light/dark and label positions across components. **As in the example** restores the lesson's own layout. The first lesson keeps a local position selector to explain its Data binding; later lessons focus on their particular input behavior.
|
|
33
|
+
|
|
34
|
+
**Formlet and shared labels (w19):** Arrange fields in columns and share label settings. Changing the layout preserves each field and its draft.
|
|
35
|
+
|
|
36
|
+
**GroupBox and typed copy (w20):** Group customer data under a heading. Copy TYTX with its types or choose plain JSON from the copy menu.
|
|
37
|
+
|
|
38
|
+
**Checkbox as a switch (w21):** A switch and a checkbox share one boolean. Compare their appearance, captions and read-only state.
|
|
39
|
+
|
|
40
|
+
- `w25`: complete local validation engine, conditional rules, warnings, Logic callbacks and RPC validation. DB/grid rules remain in their own integration scope.
|
|
@@ -47,7 +47,7 @@ export class Page extends BasePage {
|
|
|
47
47
|
}
|
|
48
48
|
for (const [key, path, exact] of [['name_exact', '.filter.name', true], ['name_all', '.filter.name', false],
|
|
49
49
|
['address_exact', '.filter.address', true], ['address_all', '.filter.address', false]]) {
|
|
50
|
-
filtered.p(`^.${key}`, {mask: `${key}:
|
|
50
|
+
filtered.p(`^.${key}`, {mask: `${key}: {}`, id: key});
|
|
51
51
|
filtered.dataController({func: 'countPath', watched: `^${path}`, counter: `.${key}`, _userChanges: exact});
|
|
52
52
|
pane.dataSetter({destination_path: `.${key}`, value: 0});
|
|
53
53
|
}
|
|
@@ -9,9 +9,9 @@ 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,
|
|
13
|
-
pane.div('Hover to read the title', {customer: '^.name', title: 'Customer:
|
|
14
|
-
pane.p('The template
|
|
12
|
+
pane.p('^.name', {mask: 'Hello, {}', id: 'greeting'});
|
|
13
|
+
pane.div('Hover to read the title', {customer: '^.name', title: '^.name:Customer: {customer}', id: 'card'});
|
|
14
|
+
pane.p('The pointer template reads customer from Source; the customer attribute remains projected.');
|
|
15
15
|
pane.h2('0.2.3: a separate HTML preview');
|
|
16
16
|
pane.html_label('Preview HTML', {for: 'html'});
|
|
17
17
|
pane.textarea({value: '^.html', live: true, id: 'html'});
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
One name appears in a greeting and a tooltip with different formatting. You learn how templates and masks reuse a bound value without copying text. The later **0.2.3** section previews HTML in a separate sandboxed frame.
|
|
4
4
|
|
|
5
|
-
**Uses:** Data binding,
|
|
5
|
+
**Uses:** Data binding, explicit pointer templates and text masks; sandboxed Data `srcdoc` only in the later part (0.2.3).
|
|
6
6
|
|
|
7
7
|
<details>
|
|
8
8
|
<summary>More detail</summary>
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
**Release stage:**
|
|
11
|
+
**Release stage:** maintained explicit pointer syntax; Data-srcdoc sandboxing was introduced in 0.2.3.
|
|
12
12
|
|
|
13
13
|
**Goal:** Reuse one Data value in text and attributes, then distinguish text from a sandboxed HTML document.
|
|
14
14
|
|
|
@@ -22,28 +22,28 @@ A template reads another attribute of its own Source node **after pointer resolu
|
|
|
22
22
|
|
|
23
23
|
```python
|
|
24
24
|
pane.div("Hover to read the title", customer="^.name",
|
|
25
|
-
title="Customer:
|
|
26
|
-
pane.p("^.name", mask="Hello,
|
|
25
|
+
title="^.name:Customer: {customer}")
|
|
26
|
+
pane.p("^.name", mask="Hello, {}")
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
`:` opens the pointer template. `{customer}` reads the current Source attribute; `customer` remains a normal projected DOM attribute. Ordinary `${customer}` is literal text. `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
|
|
|
33
33
|
## Try it and predict the result
|
|
34
34
|
|
|
35
35
|
1. Initially the greeting is “Hello, Ada”; hover the card and expect title “Customer: Ada”.
|
|
36
|
-
2. Type Grace: both follow the same Data value. Inspect the card: it has `title
|
|
36
|
+
2. Type Grace: both follow the same Data value. Inspect the card: it has both the formatted `title` and the resolved `customer` attribute.
|
|
37
37
|
3. Clear Name: the greeting becomes empty; the title retains its authored prefix.
|
|
38
38
|
4. Replace Preview HTML with `<h2>Preview two</h2><p>Only this frame changes.</p>`. The heading appears inside the frame; the outer page stays intact. Inspect the iframe's `sandbox` attribute: it is present with an empty value.
|
|
39
39
|
|
|
40
40
|
## Change it and check yourself
|
|
41
41
|
|
|
42
|
-
Change the mask to `Welcome,
|
|
42
|
+
Change the mask to `Welcome, {}` and the title to `^.name: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
|
|
|
46
|
-
Separate invalid-edit exercise: rename only the template
|
|
46
|
+
Separate invalid-edit exercise: rename only the template slot to `{missing}`, then reload. Expect a missing Source-attribute error. Restore it. Use `:=` for an expression with typed slots; ordinary `${...}` text is not interpolated or consumed.
|
|
47
47
|
|
|
48
48
|
## The same example across stages
|
|
49
49
|
|
|
@@ -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.
|
|
@@ -51,9 +51,9 @@ export class Page extends BasePage {
|
|
|
51
51
|
options.html_label('Attachment', {for: 'attachment'});
|
|
52
52
|
options.input({type: 'file', id: 'attachment', name: 'attachment'});
|
|
53
53
|
const actions = form.div({class: 'form-actions'});
|
|
54
|
-
actions.
|
|
55
|
-
actions.
|
|
56
|
-
actions.
|
|
54
|
+
actions.html_button('Button without an action', {type: 'button'});
|
|
55
|
+
actions.html_button('Reset values', {type: 'reset'});
|
|
56
|
+
actions.html_button('Submit unavailable', {type: 'submit', disabled: true});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
field(parent, label, key, kind, attrs = {}) {
|