@gramlot/gramlot-examples 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ npm install @gramlot/gramlot-examples
12
12
  [![PyPI](https://img.shields.io/pypi/v/gramlot-examples)](https://pypi.org/project/gramlot-examples/)
13
13
  [![npm](https://img.shields.io/npm/v/@gramlot/gramlot-examples)](https://www.npmjs.com/package/@gramlot/gramlot-examples)
14
14
 
15
- Current release: 0.2.9.
15
+ Current release: 0.2.11.
16
16
 
17
17
  The repositories of the family are described in
18
18
  [The Gramlot family](https://github.com/gramlot-org/gramlot/blob/main/docs/public/055-family.md)
@@ -26,8 +26,9 @@ same-name README:
26
26
 
27
27
  - `html_svg/`: thirteen HTML and SVG pages, without binding (`e01`–`e13`);
28
28
  - `binding/`: eleven pages whose DOM follows the Data (`b01`–`b11`);
29
- - `controllers/`: eight pages with formulas, controllers, named logic, events and
30
- the end-to-end story (`c01`–`c08`).
29
+ - `controllers/`: fourteen pages with formulas, controllers, named logic, events, the
30
+ end-to-end story, `dataRpc` calls to the endpoints of the page, `remote` fragments and
31
+ `dataRemote` values (`c01`–`c14`).
31
32
 
32
33
  Each `NN_name.js` exports `Page` and `Logic`; the Python page `NN_name.py` takes its
33
34
  `Logic` from the module beside it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramlot/gramlot-examples",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Gramlot example pages, the gallery page and its catalogue.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -14,7 +14,7 @@
14
14
  "prepack": "node scripts/build-browser.mjs"
15
15
  },
16
16
  "dependencies": {
17
- "@gramlot/gramlot": ">=0.2.12"
17
+ "@gramlot/gramlot": ">=0.2.16"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@genrojs/bag": ">=0.10.1",
@@ -239,6 +239,48 @@
239
239
  "folder": "08_end_to_end",
240
240
  "description": "Title, input, radios, checkbox, formula, SVG, button and freeze together.",
241
241
  "explanation": "Setters after the controls, live editing, a radio pair, a formula and controller driving text and SVG, a counting button and freeze."
242
+ },
243
+ {
244
+ "key": "c09",
245
+ "title": "Endpoint",
246
+ "folder": "09_endpoint",
247
+ "description": "dataRpc calls an endpoint of the page and writes the value at its result path.",
248
+ "explanation": "The page declares with_vat with @endpoint in Python and Page.registerEndpoint in JavaScript. The author attributes of the dataRpc are the arguments; _init=True makes the call when the page starts."
249
+ },
250
+ {
251
+ "key": "c10",
252
+ "title": "Call on change",
253
+ "folder": "10_call_on_change",
254
+ "description": "dataRpc follows a ^ argument, with _delay, _onCalling and _onResult.",
255
+ "explanation": "Each change of the quantity calls the endpoint 500 ms after the last edit. _onCalling and _onResult are inline code that write the status; the latest call wins."
256
+ },
257
+ {
258
+ "key": "c11",
259
+ "title": "Call on click",
260
+ "folder": "11_call_on_click",
261
+ "description": "A dataRpc nested in a button, with _onError for a failing and a protected endpoint.",
262
+ "explanation": "Each click calls the endpoint. An exception answers application_error; an endpoint with auth answers not_authenticated on a server that knows no identity. _onError receives the error."
263
+ },
264
+ {
265
+ "key": "c12",
266
+ "title": "Remote",
267
+ "folder": "12_remote",
268
+ "description": "remote makes the content of an element a fragment built by the server.",
269
+ "explanation": "The section declares remote('details', record_id='^.record_id'). The page builds the fragment with @source in Python and Page.registerSource in JavaScript; each change of the record requests it again."
270
+ },
271
+ {
272
+ "key": "c13",
273
+ "title": "Cached remote",
274
+ "folder": "13_cached_remote",
275
+ "description": "remote with _cachedRemote and _onRemote, under a variable datapath.",
276
+ "explanation": "The rules fragment depends on the category of the product. _cachedRemote makes no request when the category is the same; _onRemote is inline code that counts the mounted fragments and writes the status."
277
+ },
278
+ {
279
+ "key": "c14",
280
+ "title": "Data remote",
281
+ "folder": "14_data_remote",
282
+ "description": "dataRemote computes a Data value with an endpoint when the value is read.",
283
+ "explanation": "The menu path holds a resolver of the endpoint load_menu with cacheTime 5. The ^.menu paragraph reads it: the answer arrives with the Data event. A change of the table loads the menu again."
242
284
  }
243
285
  ]
244
286
  }
@@ -7,15 +7,17 @@ JavaScript, in three families:
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/): eight pages with formulas, controllers, named
10
+ - [`controllers/`](controllers/): fourteen pages with formulas, controllers, named
11
11
  logic in the `Logic` class of the page module, inline expressions, node methods, buttons,
12
- events and the end-to-end story.
12
+ events, the end-to-end story, `dataRpc` calls to the endpoints of the page, `remote`
13
+ fragments and `dataRemote` values.
13
14
 
14
15
  Each example is a file page: `NN_name.py`, its JavaScript equivalent `NN_name.js`,
15
16
  its README `NN_name.md`, and where needed a same-name stylesheet `NN_name.css`.
16
17
  `NN_name.js` exports `Page` and `Logic`: the JavaScript page uses both, the Python
17
18
  page takes `Logic` from it. The pages use only what the core `GramlotFileServer` serves:
18
- `Page.css`, the same-name stylesheet and the `Logic` of the page module. The gallery
19
+ `Page.css`, the same-name stylesheet, the `Logic` of the page module, the endpoints
20
+ and the Source fragments of the page. The gallery
19
21
  adds its own files: the frame script, a staged `<key>_aux.js` per example and its
20
22
  assets ([GE-010 §025](https://github.com/gramlot-org/gramlot-examples/blob/main/docs/010-gallery.md#ge-010-025)).
21
23
 
@@ -0,0 +1,29 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Endpoint';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'invoice'});
9
+ pane.h1('Endpoint');
10
+
11
+ const result = pane.section({class: 'card stack'});
12
+ result.h2('Total with VAT, computed by the server');
13
+ const facts = result.dl();
14
+ for (const [label, key] of [['Price', 'price'], ['VAT rate (%)', 'rate'], ['Total with VAT', 'gross']]) {
15
+ facts.dt(label);
16
+ facts.dd(`^.${key}`, {id: key});
17
+ }
18
+
19
+ pane.dataRpc({method: 'with_vat', result_path: '.gross', price: '=.price', rate: '=.rate', _init: true});
20
+ pane.dataSetter({destination_path: '.price', value: 12.5});
21
+ pane.dataSetter({destination_path: '.rate', value: 22});
22
+ }
23
+
24
+ with_vat({price, rate}) { return Math.round(price * (100 + rate)) / 100; }
25
+ }
26
+ Page.registerEndpoint('with_vat');
27
+
28
+ /** The page logic: this page names no methods. */
29
+ export class Logic {}
@@ -0,0 +1,12 @@
1
+ # 09 · Endpoint
2
+
3
+ An endpoint is a method of the page that the browser calls for a value. `dataRpc(method, result_path, **parameters)` calls it and writes the answer at `result_path`.
4
+
5
+ - **Declare the endpoint.** Python marks the method with `@endpoint`; JavaScript calls `Page.registerEndpoint('with_vat')` after the class.
6
+ - **Arguments.** The other attributes of the `dataRpc` are the arguments of the call: `price` and `rate`, read here from the Data with `=`. Python receives them as keywords, JavaScript as one object.
7
+ - **`**kwargs`.** `dataRpc` sends every attribute the author writes, and Python rejects a keyword the method does not declare. A Python endpoint called from `dataRpc` declares `**kwargs`.
8
+ - **`_init=True`** calls the endpoint once when the page starts, after the setters. The total is Data: the `dd` shows it through `^.gross`.
9
+
10
+ The call travels as one envelope on `POST /gramlot/rpc`; the page writes no `fetch`.
11
+
12
+ **Try it:** Open the browser network panel and reload: one call to `/gramlot/rpc` carries `with_vat` with price 12.5 and rate 22.
@@ -0,0 +1,37 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ export class Page extends BasePage {
4
+ static title = 'Call on change';
5
+ static css = ['/themes/gramlot-base/theme.css'];
6
+
7
+ main(root) {
8
+ const pane = root.div({class: 'example-page stack', datapath: 'quote'});
9
+ pane.h1('Call on change');
10
+
11
+ const form = pane.form({class: 'card stack'});
12
+ form.html_label('Quantity', {for: 'quantity'});
13
+ form.input({type: 'number', min: 0, value: '^.quantity', live: true, id: 'quantity'});
14
+
15
+ const result = pane.section({class: 'card stack'});
16
+ result.h2('Quote');
17
+ const facts = result.dl();
18
+ for (const [label, key] of [['Total', 'total'], ['Status', 'status']]) {
19
+ facts.dt(label);
20
+ facts.dd(`^.${key}`, {id: key});
21
+ }
22
+
23
+ pane.dataRpc({method: 'quote', result_path: '.total', quantity: '^.quantity', _delay: 500, _init: true,
24
+ _onCalling: "this.SET('.status', 'Asking the server for ' + quantity)",
25
+ _onResult: "this.SET('.status', 'Answered ' + result + ' for ' + quantity)"});
26
+ pane.dataSetter({destination_path: '.quantity', value: 1});
27
+ }
28
+
29
+ quote({quantity}) {
30
+ quantity ??= 0;
31
+ return quantity * (quantity >= 10 ? 8 : 10);
32
+ }
33
+ }
34
+ Page.registerEndpoint('quote');
35
+
36
+ /** The page logic: this page names no methods. */
37
+ export class Logic {}
@@ -0,0 +1,13 @@
1
+ # 10 · Call on change
2
+
3
+ A `dataRpc` follows the Data like a `dataController`: a `^` argument calls the endpoint again at every change. The endpoint `quote` prices 10 per piece, 8 from ten pieces on.
4
+
5
+ - **`quantity='^.quantity'`** triggers the call when the quantity changes and sends the new value.
6
+ - **`_delay=500`** calls 500 ms after the last change, once: typing `12` makes one call, not two.
7
+ - **`_onCalling`** is inline code that runs before the call, with the arguments: it writes the status.
8
+ - **`_onResult`** runs after the value is written at `result_path`, with the arguments and `result`.
9
+ - **Latest call wins.** A new trigger aborts the call in flight and its answer is dropped.
10
+
11
+ `_init=True` makes the first call when the page starts. The callbacks are inline code: the page needs a Content Security Policy that allows it.
12
+
13
+ **Try it:** Type 12 in the quantity and watch the status before and after the answer.
@@ -0,0 +1,36 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const SHOW_ERROR = "this.SET('.error', error.code + ': ' + error.message)";
4
+
5
+ export class Page extends BasePage {
6
+ static title = 'Call on click';
7
+ static css = ['/themes/gramlot-base/theme.css'];
8
+
9
+ main(root) {
10
+ const pane = root.div({class: 'example-page stack', datapath: 'stock'});
11
+ pane.h1('Call on click');
12
+
13
+ const calls = pane.section({class: 'card stack'});
14
+ calls.h2('A dataRpc nested in the button');
15
+ const reserve = calls.button('Reserve the item', {id: 'reserve'});
16
+ reserve.dataRpc({method: 'reserve', result_path: '.reserved', item: '=.item', _onError: SHOW_ERROR});
17
+ const cancel = calls.button('Cancel the order (auth)', {id: 'cancel'});
18
+ cancel.dataRpc({method: 'cancel', result_path: '.reserved', item: '=.item', _onError: SHOW_ERROR});
19
+ const facts = calls.dl();
20
+ for (const [label, key] of [['Item', 'item'], ['Reserved', 'reserved'], ['Error', 'error']]) {
21
+ facts.dt(label);
22
+ facts.dd(`^.${key}`, {id: key});
23
+ }
24
+
25
+ pane.dataSetter({destination_path: '.item', value: 'Lamp'});
26
+ }
27
+
28
+ reserve({item}) { throw new Error(`${item} is out of stock`); }
29
+
30
+ cancel() { return false; }
31
+ }
32
+ Page.registerEndpoint('reserve');
33
+ Page.registerEndpoint('cancel', {auth: 'admin'});
34
+
35
+ /** The page logic: this page names no methods. */
36
+ export class Logic {}
@@ -0,0 +1,11 @@
1
+ # 11 · Call on click
2
+
3
+ A `dataRpc` nested in a `button` is the click mechanism of the button: each click calls the endpoint. A button has one mechanism, as in example 06.
4
+
5
+ - **`_onError`** is inline code that runs when the call fails, with the arguments and `error`. `error.code` is the outcome, `error.message` the message of the server, `error.remoteName` the name of the exception (`LookupError` in Python, `Error` in JavaScript).
6
+ - **An endpoint that raises.** `reserve` raises: the outcome is `application_error` and nothing is written at `result_path`.
7
+ - **A protected endpoint.** `cancel` declares the rule `auth="admin"` (Python `@endpoint(auth="admin")`, JavaScript `Page.registerEndpoint('cancel', {auth: 'admin'})`). A server without the `auth` capability knows no identity: the call answers `not_authenticated` and the method does not run.
8
+
9
+ Without `_onError` the failure reaches the browser console as an unhandled rejection.
10
+
11
+ **Try it:** Press each button and read the error.
@@ -0,0 +1,33 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const RECORDS = {lamp: ['Lamp', 3], desk: ['Desk', 0], chair: ['Chair', 12]};
4
+
5
+ export class Page extends BasePage {
6
+ static title = 'Remote';
7
+ static css = ['/themes/gramlot-base/theme.css'];
8
+
9
+ main(root) {
10
+ const pane = root.div({class: 'example-page stack', datapath: 'catalog'});
11
+ pane.h1('Remote');
12
+
13
+ const form = pane.form({class: 'card stack'});
14
+ form.html_label('Record', {for: 'record'});
15
+ const choice = form.select({value: '^.record_id', id: 'record'});
16
+ for (const [key, [name]] of Object.entries(RECORDS)) choice.option(name, {value: key});
17
+
18
+ const details = pane.section('Waiting for the server', {class: 'card stack', id: 'details'});
19
+ details.remote('details', {record_id: '^.record_id'});
20
+
21
+ pane.dataSetter({destination_path: '.record_id', value: 'lamp'});
22
+ }
23
+
24
+ details(root, {record_id}) {
25
+ const [name, stock] = RECORDS[record_id];
26
+ root.h2(name, {id: 'name'});
27
+ root.p(`${stock} in stock`, {id: 'stock'});
28
+ }
29
+ }
30
+ Page.registerSource('details');
31
+
32
+ /** The page logic: this page names no methods. */
33
+ export class Logic {}
@@ -0,0 +1,13 @@
1
+ # 12 · Remote
2
+
3
+ `remote` makes the content of an element a Source fragment that a method of the page builds on the server. The node method `remote(name, **params)` writes it on the element.
4
+
5
+ - **Declare the fragment.** Python marks the method with `@source`; JavaScript calls `Page.registerSource('details')` after the class. The method receives the root of the fragment and the parameters, and populates the root.
6
+ - **Parameters.** `record_id='^.record_id'` is a parameter of the call. A `^` parameter requests the fragment again at every change; a `=` parameter is only read.
7
+ - **When.** The fragment is requested when the element starts. The authored content, `Waiting for the server`, stays until the first fragment arrives; each fragment replaces the content of the element.
8
+ - **`**kwargs`.** A Python fragment method declares `**kwargs`, as a Python endpoint does.
9
+ - **Latest wins.** A new request supersedes the one in flight; removing the element cancels its request.
10
+
11
+ The request travels as one envelope on `POST /gramlot/rpc`; the page writes no `fetch`.
12
+
13
+ **Try it:** Choose another record: the section shows the name and the stock that the server returns for it.
@@ -0,0 +1,44 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const PRODUCTS = {
4
+ lamp: {name: 'Lamp', category: 'lighting'},
5
+ spot: {name: 'Spotlight', category: 'lighting'},
6
+ chair: {name: 'Chair', category: 'seating'},
7
+ };
8
+ const RULES = {lighting: 'Ships in two days, bulbs not included.', seating: 'Ships in a week, assembly required.'};
9
+ const ON_REMOTE = "this.SET('requests', (this.GET('requests') ?? 0) + 1); this.SET('status', 'Mounted the ' + category + ' rules')";
10
+
11
+ export class Page extends BasePage {
12
+ static title = 'Cached remote';
13
+ static css = ['/themes/gramlot-base/theme.css'];
14
+
15
+ main(root) {
16
+ const pane = root.div({class: 'example-page stack'});
17
+ pane.h1('Cached remote');
18
+ pane.html_label('Product', {for: 'product'});
19
+ const choice = pane.select({id: 'product', value: '^current'});
20
+ for (const [key, product] of Object.entries(PRODUCTS)) choice.option(product.name, {value: `products.${key}`});
21
+
22
+ const card = pane.article({datapath: '^current', class: 'card stack'});
23
+ card.h2('^.name', {id: 'name'});
24
+ const rules = card.div('Waiting for the server', {id: 'rules'});
25
+ rules.remote('rules', {category: '^.category', _cachedRemote: true, _onRemote: ON_REMOTE});
26
+
27
+ const facts = pane.dl();
28
+ for (const [label, key] of [['Requests', 'requests'], ['Status', 'status']]) {
29
+ facts.dt(label);
30
+ facts.dd(`^${key}`, {id: key});
31
+ }
32
+
33
+ pane.dataSetter({destination_path: 'products', value: PRODUCTS});
34
+ pane.dataSetter({destination_path: 'current', value: 'products.lamp'});
35
+ }
36
+
37
+ rules(root, {category}) {
38
+ root.p(RULES[category], {id: 'rule'});
39
+ }
40
+ }
41
+ Page.registerSource('rules');
42
+
43
+ /** The page logic: this page names no methods. */
44
+ export class Logic {}
@@ -0,0 +1,12 @@
1
+ # 13 · Cached remote
2
+
3
+ The control attributes of `remote` act on the requests of the element. The `article` has a variable datapath, as in binding example 02; the rules fragment depends only on the category of the product.
4
+
5
+ - **A change of the Data context** requests the fragment again: choosing another product moves the datapath of the `article`.
6
+ - **`_cachedRemote=True`** makes no request when the parameters equal those of the last request. Lamp and Spotlight share the category `lighting`: switching between them makes no request.
7
+ - **`_onRemote`** is inline code that runs after the fragment is mounted, with the parameters as locals: here it counts the mounted fragments and writes the status.
8
+ - **`_if` and `_else`** are the other control attributes: a false `_if` makes no request and runs `_else`.
9
+
10
+ The callbacks are inline code: the page needs a Content Security Policy that allows it.
11
+
12
+ **Try it:** Choose Spotlight, then Chair. Requests stays at 1 for Spotlight and becomes 2 for Chair.
@@ -0,0 +1,31 @@
1
+ import {Page as BasePage} from '@gramlot/gramlot/page';
2
+
3
+ const MENUS = {dishes: ['Soup', 'Risotto', 'Fish'], drinks: ['Water', 'Lemonade', 'Coffee']};
4
+
5
+ export class Page extends BasePage {
6
+ static title = 'Data remote';
7
+ static css = ['/themes/gramlot-base/theme.css'];
8
+
9
+ main(root) {
10
+ const pane = root.div({class: 'example-page stack', datapath: 'kitchen'});
11
+ pane.h1('Data remote');
12
+
13
+ const form = pane.form({class: 'card stack'});
14
+ form.html_label('Table', {for: 'table'});
15
+ const choice = form.select({value: '^.table', id: 'table'});
16
+ for (const key of Object.keys(MENUS)) choice.option(key[0].toUpperCase() + key.slice(1), {value: key});
17
+
18
+ const result = pane.section({class: 'card stack'});
19
+ result.h2('Menu, loaded by the server when it is read');
20
+ result.p('^.menu', {id: 'menu'});
21
+
22
+ pane.dataRemote({destination_path: '.menu', method: 'load_menu', cacheTime: 5, table: '^.table'});
23
+ pane.dataSetter({destination_path: '.table', value: 'dishes'});
24
+ }
25
+
26
+ load_menu({table}) { return MENUS[table].join(', '); }
27
+ }
28
+ Page.registerEndpoint('load_menu');
29
+
30
+ /** The page logic: this page names no methods. */
31
+ export class Logic {}
@@ -0,0 +1,10 @@
1
+ # 14 · Data remote
2
+
3
+ `dataRemote(destination_path, method, cacheTime=None, **params)` makes a Data path a value computed by an endpoint of the page. The endpoint is called when the path is read, not when the page starts.
4
+
5
+ - **Declare the endpoint** as for `dataRpc`: Python `@endpoint`, with `**kwargs`; JavaScript `Page.registerEndpoint('load_menu')`.
6
+ - **A read never waits.** The paragraph `^.menu` reads the path: the read returns the current value, empty before the first call, and starts the call. The answer is written with the Data event, so the paragraph shows the menu.
7
+ - **`cacheTime`** is in seconds. `5` serves the stored value for five seconds, then the next read calls again. `None` or a negative value calls once; `0` calls at every read and writes the answer without the Data event.
8
+ - **`table='^.table'`** is an argument of the endpoint. After the first call, a change of a `^` argument loads the value again at once and the paragraph receives it.
9
+
10
+ **Try it:** Choose Drinks: the menu changes to the drinks that the server returns.
@@ -2,10 +2,10 @@
2
2
 
3
3
  The pages of this family run code when the Data changes.
4
4
 
5
- `dataFormula` computes a value and writes it to its result path. `dataController` runs code for its effects. A button runs a nested controller at every click; `connect_on<event>` attributes connect other DOM events. The code is inline in the page, or a method of the `Logic` class that the page module `NN_name.js` exports; the Python page takes it from there.
5
+ `dataFormula` computes a value and writes it to its result path. `dataController` runs code for its effects. `dataRpc` calls an endpoint of the page on the server and writes the answer to its result path. `remote` makes the content of an element a fragment built by the page on the server; `dataRemote` makes a Data path a value computed by an endpoint when it is read. A button runs a nested controller at every click; `connect_on<event>` attributes connect other DOM events. The code is inline in the page, or a method of the `Logic` class that the page module `NN_name.js` exports; the Python page takes it from there.
6
6
 
7
7
  Inline code needs a Content Security Policy that allows it. Named logic in the `Logic` class also runs under a strict policy.
8
8
 
9
- Start with **Formula** and **Controller**, then move the code to the `Logic` class with **Named logic**. The last page is the end-to-end story: every piece of the two families on one page.
9
+ Start with **Formula** and **Controller**, then move the code to the `Logic` class with **Named logic**. **End-to-end story** puts every piece of the two families on one page. Six pages call the server. **Endpoint**, **Call on change** and **Call on click** use `dataRpc`; **Remote** and **Cached remote** mount a fragment of the page with `remote`; **Data remote** computes a Data value with `dataRemote`.
10
10
 
11
- Each example has a Python page and its JavaScript equivalent. Both use the `Logic` class of the JavaScript module.
11
+ Each example has a Python page and its JavaScript equivalent. Both use the `Logic` class of the JavaScript module. An endpoint is a method of the page in each language: Python `@endpoint`, JavaScript `Page.registerEndpoint`. A fragment method is declared with Python `@source`, JavaScript `Page.registerSource`.