@gramlot/gramlot-examples 0.2.16 → 0.3.1
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 +8 -5
- package/package.json +4 -3
- package/src/gramlot_examples/catalog.json +156 -0
- 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 +55 -17
- package/src/gramlot_examples/gallery/gallery.css +52 -4
- package/src/gramlot_examples/index.js +3 -2
- package/src/gramlot_examples/pages/README.md +5 -2
- 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/16_attribute_projection.js +2 -2
- package/src/gramlot_examples/pages/controllers/16_attribute_projection.md +7 -7
- package/src/gramlot_examples/pages/html_svg/06_forms.js +3 -3
|
@@ -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.
|
|
@@ -10,8 +10,8 @@ export class Page extends BasePage {
|
|
|
10
10
|
pane.html_label('Name', {for: 'name'});
|
|
11
11
|
pane.input({value: '^.name', live: true, id: 'name'});
|
|
12
12
|
pane.p('^.name', {mask: 'Hello, {}', id: 'greeting'});
|
|
13
|
-
pane.div('Hover to read the title', {customer: '^.name', title: 'Customer:
|
|
14
|
-
pane.p('The template
|
|
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:
|
|
25
|
+
title="^.name:Customer: {customer}")
|
|
26
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, {}` and the title to
|
|
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
|
|
|
@@ -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 = {}) {
|