@loadbare/widgets 0.4.0 → 0.5.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 CHANGED
@@ -6,9 +6,9 @@ Inventory:
6
6
 
7
7
  | Widget | Is |
8
8
  | --- | --- |
9
- | [`lb-input`](docs/lb-input.md) | an editable cell wrapping an `<input>` |
10
- | [`lb-select`](docs/lb-select.md) | a `<select>` whose options the page writes |
11
- | [`lb-options`](docs/lb-options.md) | a `<select>` whose options come from a query |
9
+ | [`lb-input`](docs/lb-input.md) | a control wrapping an `<input>` |
10
+ | [`lb-select`](docs/lb-select.md) | a control wrapping a `<select>` whose options the page writes |
11
+ | [`lb-options`](docs/lb-options.md) | a control wrapping a `<select>` whose options come from a query |
12
12
  | [`lb-picker`](docs/lb-picker.md) | `lb-options` with the row template built in |
13
13
  | [`lb-table`](docs/lb-table.md) | a `<table>` supplying its own scaffolding |
14
14
  | [`lb-unknown-page`](docs/lb-unknown-page.md) | the chrome's dialog for a URL that names no page |
@@ -26,7 +26,7 @@ Then list the package in the application's `imports.ts`:
26
26
  export default ["@loadbare/widgets"];
27
27
  ```
28
28
 
29
- You can now include the widgets in your HTML files.
29
+ The application's HTML files can then use the widgets.
30
30
 
31
31
  Apart from the line in `src/imports.ts`, no other import statements
32
32
  are required. The loadbare builder automatically bundles the HTML and
@@ -0,0 +1,57 @@
1
+ /** The native controls a widget wraps. */
2
+ type Inner = HTMLInputElement | HTMLSelectElement;
3
+ /**
4
+ * A form-associated custom element wrapping one native control, which makes
5
+ * the widget itself the control: the hub sets its `value` when a column
6
+ * lands, reads it back when a request gathers, and commits it on `change`
7
+ * when it carries `lb-request`. Nothing about a request is the widget's.
8
+ *
9
+ * The native control inside is an implementation detail. Its `change` is
10
+ * stopped and fired again on the widget, so a listener sees one control and
11
+ * one event per edit.
12
+ *
13
+ * Not a widget itself, so the file carries no `.browser` and no `lb-` name:
14
+ * a consumer's builder passes it over, and each widget that extends it
15
+ * brings it along.
16
+ */
17
+ export declare abstract class WrappedControl extends HTMLElement {
18
+ static formAssociated: boolean;
19
+ /** `<input>` or `<select>`: what this widget wraps. */
20
+ protected abstract readonly inner: "input" | "select";
21
+ /**
22
+ * Null where the DOM implementation offers no form value, which includes
23
+ * jsdom: the widget still works as a control, and a native form submission
24
+ * leaves it out.
25
+ */
26
+ private readonly internals;
27
+ /** Whether the widget has connected once. */
28
+ private connected;
29
+ constructor();
30
+ /**
31
+ * A value that landed before this widget's definition did is only a
32
+ * stamp, since the hub sets `value` on a control and the element was not
33
+ * one yet. It is taken up the first time the widget connects, and never
34
+ * again: after that the hub sets `value` itself, and a later move must not
35
+ * undo an edit.
36
+ *
37
+ * The native control follows the widget's `form` attribute, so a form
38
+ * this widget joins by id resets the control inside it too.
39
+ */
40
+ connectedCallback(): void;
41
+ /** The native control this widget wraps, once it is there. */
42
+ protected get control(): Inner | null;
43
+ get value(): string;
44
+ set value(value: string);
45
+ /**
46
+ * The native control is the form's too, and the form resets it by HTML's
47
+ * own algorithm. This widget comes first in tree order, so it tells the
48
+ * form its value once the control has been reset.
49
+ */
50
+ formResetCallback(): void;
51
+ /** The inner control changed, before the widget fires its own `change`. */
52
+ protected changed(): void;
53
+ /** Tell the form owner the value, where the implementation allows it. */
54
+ protected formValue(): void;
55
+ }
56
+ export {};
57
+ //# sourceMappingURL=control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../widgets/control.ts"],"names":[],"mappings":"AAIA,0CAA0C;AAC1C,KAAK,KAAK,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAElD;;;;;;;;;;;;;GAaG;AACH,8BAAsB,cAAe,SAAQ,WAAW;IACtD,MAAM,CAAC,cAAc,UAAQ;IAE7B,uDAAuD;IACvD,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;IAEtD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IAEpD,6CAA6C;IAC7C,OAAO,CAAC,SAAS,CAAS;;IAa1B;;;;;;;;;OASG;IACH,iBAAiB,IAAI,IAAI;IASzB,8DAA8D;IAC9D,SAAS,KAAK,OAAO,IAAI,KAAK,GAAG,IAAI,CAEpC;IAED,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAUtB;IAED;;;;OAIG;IACH,iBAAiB,IAAI,IAAI;IAIzB,2EAA2E;IAC3E,SAAS,CAAC,OAAO,IAAI,IAAI;IAIzB,yEAAyE;IACzE,SAAS,CAAC,SAAS,IAAI,IAAI;CAG5B"}
@@ -0,0 +1,101 @@
1
+ /// <reference lib="dom" />
2
+ import { ATTR_COLUMN_VALUE } from "@loadbare/app/constants";
3
+ /**
4
+ * A form-associated custom element wrapping one native control, which makes
5
+ * the widget itself the control: the hub sets its `value` when a column
6
+ * lands, reads it back when a request gathers, and commits it on `change`
7
+ * when it carries `lb-request`. Nothing about a request is the widget's.
8
+ *
9
+ * The native control inside is an implementation detail. Its `change` is
10
+ * stopped and fired again on the widget, so a listener sees one control and
11
+ * one event per edit.
12
+ *
13
+ * Not a widget itself, so the file carries no `.browser` and no `lb-` name:
14
+ * a consumer's builder passes it over, and each widget that extends it
15
+ * brings it along.
16
+ */
17
+ export class WrappedControl extends HTMLElement {
18
+ static formAssociated = true;
19
+ /**
20
+ * Null where the DOM implementation offers no form value, which includes
21
+ * jsdom: the widget still works as a control, and a native form submission
22
+ * leaves it out.
23
+ */
24
+ internals;
25
+ /** Whether the widget has connected once. */
26
+ connected = false;
27
+ constructor() {
28
+ super();
29
+ this.internals = internalsOf(this);
30
+ this.addEventListener("change", (e) => {
31
+ if (e.target === this || e.target !== this.control)
32
+ return;
33
+ e.stopPropagation();
34
+ this.changed();
35
+ this.dispatchEvent(new Event("change", { bubbles: true }));
36
+ });
37
+ }
38
+ /**
39
+ * A value that landed before this widget's definition did is only a
40
+ * stamp, since the hub sets `value` on a control and the element was not
41
+ * one yet. It is taken up the first time the widget connects, and never
42
+ * again: after that the hub sets `value` itself, and a later move must not
43
+ * undo an edit.
44
+ *
45
+ * The native control follows the widget's `form` attribute, so a form
46
+ * this widget joins by id resets the control inside it too.
47
+ */
48
+ connectedCallback() {
49
+ const form = this.getAttribute("form");
50
+ if (form !== null)
51
+ this.control?.setAttribute("form", form);
52
+ if (this.connected)
53
+ return;
54
+ this.connected = true;
55
+ const stamped = this.getAttribute(ATTR_COLUMN_VALUE);
56
+ if (stamped !== null)
57
+ this.value = stamped;
58
+ }
59
+ /** The native control this widget wraps, once it is there. */
60
+ get control() {
61
+ return this.querySelector(this.inner);
62
+ }
63
+ get value() {
64
+ return this.control?.value ?? "";
65
+ }
66
+ set value(value) {
67
+ const control = this.control;
68
+ if (!control) {
69
+ console.error(`${this.localName}: no <${this.inner}> to receive the value`);
70
+ return;
71
+ }
72
+ control.value = value;
73
+ this.formValue();
74
+ }
75
+ /**
76
+ * The native control is the form's too, and the form resets it by HTML's
77
+ * own algorithm. This widget comes first in tree order, so it tells the
78
+ * form its value once the control has been reset.
79
+ */
80
+ formResetCallback() {
81
+ queueMicrotask(() => this.formValue());
82
+ }
83
+ /** The inner control changed, before the widget fires its own `change`. */
84
+ changed() {
85
+ this.formValue();
86
+ }
87
+ /** Tell the form owner the value, where the implementation allows it. */
88
+ formValue() {
89
+ this.internals?.setFormValue(this.value);
90
+ }
91
+ }
92
+ function internalsOf(el) {
93
+ try {
94
+ const internals = el.attachInternals();
95
+ return typeof internals.setFormValue === "function" ? internals : null;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ //# sourceMappingURL=control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control.js","sourceRoot":"","sources":["../widgets/control.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAK5D;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAgB,cAAe,SAAQ,WAAW;IACtD,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC;IAK7B;;;;OAIG;IACc,SAAS,CAA0B;IAEpD,6CAA6C;IACrC,SAAS,GAAG,KAAK,CAAC;IAE1B;QACE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;YACpC,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO;gBAAE,OAAO;YAC3D,CAAC,CAAC,eAAe,EAAE,CAAC;YACpB,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,IAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;OASG;IACH,iBAAiB;QACf,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,IAAI,KAAK,IAAI;YAAE,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;QACrD,IAAI,OAAO,KAAK,IAAI;YAAE,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC;IAC7C,CAAC;IAED,8DAA8D;IAC9D,IAAc,OAAO;QACnB,OAAO,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxC,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC;IACnC,CAAC;IAED,IAAI,KAAK,CAAC,KAAa;QACrB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CACX,GAAG,IAAI,CAAC,SAAS,SAAS,IAAI,CAAC,KAAK,wBAAwB,CAC7D,CAAC;YACF,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAED;;;;OAIG;IACH,iBAAiB;QACf,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,2EAA2E;IACjE,OAAO;QACf,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAED,yEAAyE;IAC/D,SAAS;QACjB,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;;AAGH,SAAS,WAAW,CAAC,EAAe;IAClC,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC;QACvC,OAAO,OAAO,SAAS,CAAC,YAAY,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { ATTR_COLUMN_VALUE } from \"@loadbare/app/constants\";\n\n/** The native controls a widget wraps. */\ntype Inner = HTMLInputElement | HTMLSelectElement;\n\n/**\n * A form-associated custom element wrapping one native control, which makes\n * the widget itself the control: the hub sets its `value` when a column\n * lands, reads it back when a request gathers, and commits it on `change`\n * when it carries `lb-request`. Nothing about a request is the widget's.\n *\n * The native control inside is an implementation detail. Its `change` is\n * stopped and fired again on the widget, so a listener sees one control and\n * one event per edit.\n *\n * Not a widget itself, so the file carries no `.browser` and no `lb-` name:\n * a consumer's builder passes it over, and each widget that extends it\n * brings it along.\n */\nexport abstract class WrappedControl extends HTMLElement {\n static formAssociated = true;\n\n /** `<input>` or `<select>`: what this widget wraps. */\n protected abstract readonly inner: \"input\" | \"select\";\n\n /**\n * Null where the DOM implementation offers no form value, which includes\n * jsdom: the widget still works as a control, and a native form submission\n * leaves it out.\n */\n private readonly internals: ElementInternals | null;\n\n /** Whether the widget has connected once. */\n private connected = false;\n\n constructor() {\n super();\n this.internals = internalsOf(this);\n this.addEventListener(\"change\", (e) => {\n if (e.target === this || e.target !== this.control) return;\n e.stopPropagation();\n this.changed();\n this.dispatchEvent(new Event(\"change\", { bubbles: true }));\n });\n }\n\n /**\n * A value that landed before this widget's definition did is only a\n * stamp, since the hub sets `value` on a control and the element was not\n * one yet. It is taken up the first time the widget connects, and never\n * again: after that the hub sets `value` itself, and a later move must not\n * undo an edit.\n *\n * The native control follows the widget's `form` attribute, so a form\n * this widget joins by id resets the control inside it too.\n */\n connectedCallback(): void {\n const form = this.getAttribute(\"form\");\n if (form !== null) this.control?.setAttribute(\"form\", form);\n if (this.connected) return;\n this.connected = true;\n const stamped = this.getAttribute(ATTR_COLUMN_VALUE);\n if (stamped !== null) this.value = stamped;\n }\n\n /** The native control this widget wraps, once it is there. */\n protected get control(): Inner | null {\n return this.querySelector(this.inner);\n }\n\n get value(): string {\n return this.control?.value ?? \"\";\n }\n\n set value(value: string) {\n const control = this.control;\n if (!control) {\n console.error(\n `${this.localName}: no <${this.inner}> to receive the value`,\n );\n return;\n }\n control.value = value;\n this.formValue();\n }\n\n /**\n * The native control is the form's too, and the form resets it by HTML's\n * own algorithm. This widget comes first in tree order, so it tells the\n * form its value once the control has been reset.\n */\n formResetCallback(): void {\n queueMicrotask(() => this.formValue());\n }\n\n /** The inner control changed, before the widget fires its own `change`. */\n protected changed(): void {\n this.formValue();\n }\n\n /** Tell the form owner the value, where the implementation allows it. */\n protected formValue(): void {\n this.internals?.setFormValue(this.value);\n }\n}\n\nfunction internalsOf(el: HTMLElement): ElementInternals | null {\n try {\n const internals = el.attachInternals();\n return typeof internals.setFormValue === \"function\" ? internals : null;\n } catch {\n return null;\n }\n}\n"]}
@@ -1,45 +1,17 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_ACTION, ATTR_VALUE, LB_EVENT_NAME, } from "@loadbare/app/constants";
2
+ import { WrappedControl } from "./control.js";
3
3
  /**
4
- * A leaf cell wrapping an <input>. The hub writes one attribute name for
5
- * every widget type, so the value lands in `lb-value` and the widget
6
- * forwards it to the control it owns.
4
+ * A control wrapping an `<input>`. The hub sets its `value` from the column
5
+ * its `lb-column` names, and gathers it back under the same name.
7
6
  *
8
- * The other direction of the same cell: `lb-action` names what a change
9
- * sends, carrying the input's value. The hub scopes it like any request, so
10
- * the reserved `lb-row-update` reaches the server with the list and key this
11
- * input sits in, and `values` holding this input's cell alone. Without
12
- * `lb-action` a change sends nothing.
13
- *
14
- * The developer has to ask, because committing per field is one editing model
15
- * and not the only one. An input inside an `lb-row-insert` or `lb-row-update`
16
- * form is read again by the form on submit, so a widget that sent on its own
17
- * would write the same edit twice and defeat the batch the form is for.
7
+ * It issues nothing on its own. With `lb-request` it commits on `change`, and
8
+ * the hub sends that request with this input's column alone: an
9
+ * `lb-row-update` reaches the server with the query and key of the row it
10
+ * sits in. Without `lb-request` an edit waits for the form or row around it,
11
+ * because committing per field is one editing model and not the only one.
18
12
  */
19
- class LbInput extends HTMLElement {
20
- static observedAttributes = [ATTR_VALUE];
21
- attributeChangedCallback(_name, _old, value) {
22
- const input = this.querySelector("input");
23
- if (!input) {
24
- console.error(`lb-input: no <input> to receive the value`);
25
- return;
26
- }
27
- input.value = value;
28
- }
29
- connectedCallback() {
30
- this.addEventListener("change", () => {
31
- const action = this.getAttribute(ATTR_ACTION);
32
- if (!action)
33
- return;
34
- const input = this.querySelector("input");
35
- if (!input) {
36
- console.error(`lb-input: change with no <input>, ignoring`);
37
- return;
38
- }
39
- const detail = { action, value: input.value };
40
- this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
41
- });
42
- }
13
+ class LbInput extends WrappedControl {
14
+ inner = "input";
43
15
  }
44
16
  customElements.define("lb-input", LbInput);
45
17
  //# sourceMappingURL=lb-input.browser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lb-input.browser.js","sourceRoot":"","sources":["../widgets/lb-input.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EACL,WAAW,EACX,UAAU,EACV,aAAa,GACd,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,OAAQ,SAAQ,WAAW;IAC/B,MAAM,CAAC,kBAAkB,GAAG,CAAC,UAAU,CAAC,CAAC;IAEzC,wBAAwB,CAAC,KAAa,EAAE,IAAY,EAAE,KAAa;QACjE,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QACD,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,GAAG,EAAE;YACnC,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;YAC9C,IAAI,CAAC,MAAM;gBAAE,OAAO;YACpB,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;gBAC5D,OAAO;YACT,CAAC;YACD,MAAM,MAAM,GAAe,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;YAC1D,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAC1D,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;;AAGH,cAAc,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_ACTION,\n ATTR_VALUE,\n LB_EVENT_NAME,\n} from \"@loadbare/app/constants\";\nimport type { HubRequest } from \"@loadbare/app/types\";\n\n/**\n * A leaf cell wrapping an <input>. The hub writes one attribute name for\n * every widget type, so the value lands in `lb-value` and the widget\n * forwards it to the control it owns.\n *\n * The other direction of the same cell: `lb-action` names what a change\n * sends, carrying the input's value. The hub scopes it like any request, so\n * the reserved `lb-row-update` reaches the server with the list and key this\n * input sits in, and `values` holding this input's cell alone. Without\n * `lb-action` a change sends nothing.\n *\n * The developer has to ask, because committing per field is one editing model\n * and not the only one. An input inside an `lb-row-insert` or `lb-row-update`\n * form is read again by the form on submit, so a widget that sent on its own\n * would write the same edit twice and defeat the batch the form is for.\n */\n\nclass LbInput extends HTMLElement {\n static observedAttributes = [ATTR_VALUE];\n\n attributeChangedCallback(_name: string, _old: string, value: string) {\n const input = this.querySelector(\"input\");\n if (!input) {\n console.error(`lb-input: no <input> to receive the value`);\n return;\n }\n input.value = value;\n }\n\n connectedCallback() {\n this.addEventListener(\"change\", () => {\n const action = this.getAttribute(ATTR_ACTION);\n if (!action) return;\n const input = this.querySelector(\"input\");\n if (!input) {\n console.error(`lb-input: change with no <input>, ignoring`);\n return;\n }\n const detail: HubRequest = { action, value: input.value };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\n });\n }\n}\n\ncustomElements.define(\"lb-input\", LbInput);\n"]}
1
+ {"version":3,"file":"lb-input.browser.js","sourceRoot":"","sources":["../widgets/lb-input.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,OAAQ,SAAQ,cAAc;IACf,KAAK,GAAG,OAAO,CAAC;CACpC;AAED,cAAc,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * A control wrapping an `<input>`. The hub sets its `value` from the column\n * its `lb-column` names, and gathers it back under the same name.\n *\n * It issues nothing on its own. With `lb-request` it commits on `change`, and\n * the hub sends that request with this input's column alone: an\n * `lb-row-update` reaches the server with the query and key of the row it\n * sits in. Without `lb-request` an edit waits for the form or row around it,\n * because committing per field is one editing model and not the only one.\n */\nclass LbInput extends WrappedControl {\n protected readonly inner = \"input\";\n}\n\ncustomElements.define(\"lb-input\", LbInput);\n"]}
@@ -1,9 +1,18 @@
1
- import type { ListHost, Row } from "@loadbare/app/types";
2
- export declare class LbOptions extends HTMLElement implements ListHost {
3
- static observedAttributes: string[];
4
- attributeChangedCallback(_name: string, _old: string, value: string): void;
1
+ import type { Row, RowsHost } from "@loadbare/app/types";
2
+ import { WrappedControl } from "./control.js";
3
+ export declare class LbOptions extends WrappedControl implements RowsHost {
4
+ protected readonly inner = "select";
5
5
  /**
6
- * The hub reconciles the list and calls this for placement only, so this
6
+ * The value last set or chosen, which the options may not include yet.
7
+ * Null when nothing chose one, so the select's own default stands.
8
+ */
9
+ private chosen;
10
+ get value(): string;
11
+ set value(value: string);
12
+ formResetCallback(): void;
13
+ protected changed(): void;
14
+ /**
15
+ * The hub reconciles the rows and calls this for placement only, so this
7
16
  * widget carries no copy of what a row is or when one arrives.
8
17
  */
9
18
  lbPlaceRow(el: Element, row: Row, template: HTMLTemplateElement): void;
@@ -11,6 +20,5 @@ export declare class LbOptions extends HTMLElement implements ListHost {
11
20
  lbRowsLanded(): void;
12
21
  /** The group with this label, or a new one in front of the template. */
13
22
  private groupFor;
14
- connectedCallback(): void;
15
23
  }
16
24
  //# sourceMappingURL=lb-options.browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lb-options.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAc,QAAQ,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AA6BrE,qBAAa,SAAU,SAAQ,WAAY,YAAW,QAAQ;IAC5D,MAAM,CAAC,kBAAkB,WAAgB;IAEzC,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAKnE;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAUtE,0EAA0E;IAC1E,YAAY,IAAI,IAAI;IAepB,wEAAwE;IACxE,OAAO,CAAC,QAAQ;IAUhB,iBAAiB;CAoBlB"}
1
+ {"version":3,"file":"lb-options.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AA8B9C,qBAAa,SAAU,SAAQ,cAAe,YAAW,QAAQ;IAC/D,SAAS,CAAC,QAAQ,CAAC,KAAK,YAAY;IAEpC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAuB;IAErC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAGtB;IAED,iBAAiB,IAAI,IAAI;IAKzB,SAAS,CAAC,OAAO,IAAI,IAAI;IAKzB;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAUtE,0EAA0E;IAC1E,YAAY,IAAI,IAAI;IAgBpB,wEAAwE;IACxE,OAAO,CAAC,QAAQ;CASjB"}
@@ -1,8 +1,9 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_ACTION, ATTR_KEY_VALUE, ATTR_QUERY_PARM, ATTR_VALUE, LB_EVENT_NAME, } from "@loadbare/app/constants";
2
+ import { ATTR_KEY_VALUE } from "@loadbare/app/constants";
3
+ import { WrappedControl } from "./control.js";
3
4
  /**
4
- * A `<select>` whose choices come from a list, and the case that shows why
5
- * placement belongs to the widget rather than to the framework.
5
+ * A `<select>` whose choices are the rows of a query, and the case that
6
+ * shows why placement belongs to the widget rather than to the framework.
6
7
  *
7
8
  * The rows arrive flat and keyed. Nothing on the wire knows about
8
9
  * `<optgroup>`, and nothing needs to: a group is a column like any other, and
@@ -13,12 +14,13 @@ import { ATTR_ACTION, ATTR_KEY_VALUE, ATTR_QUERY_PARM, ATTR_VALUE, LB_EVENT_NAME
13
14
  * does read.
14
15
  *
15
16
  * The identity of a row is the value of the option, so the `lb-key-value`
16
- * the hub stamps on each option is copied to its `value`, and the developer
17
- * names the key column once, with `lb-key` on the template.
17
+ * the hub stamps on each option is copied to its `value`. The server declares
18
+ * the key, and the page never names it.
18
19
  *
19
- * It receives a value like any other cell, which selects the option whose
20
- * key it is. A picker that writes a query parm sends nothing: the hub reads
21
- * its choice off the bubbling `change` and lands the URL's back on it.
20
+ * It is a control like `lb-select`: `lb-column` sets which option is
21
+ * selected, from the row around it, and `lb-request` commits the choice on
22
+ * `change`. A value can arrive before the options it names, so the widget
23
+ * keeps the value it was last given and selects it again whenever rows land.
22
24
  *
23
25
  * Exported because `lb-picker` is this widget with its row template
24
26
  * supplied by the definition instead of by the page. Nothing about the
@@ -26,15 +28,30 @@ import { ATTR_ACTION, ATTR_KEY_VALUE, ATTR_QUERY_PARM, ATTR_VALUE, LB_EVENT_NAME
26
28
  */
27
29
  /** Named on the row template: the column whose distinct values become groups. */
28
30
  const GROUP_COLUMN = "data-group";
29
- export class LbOptions extends HTMLElement {
30
- static observedAttributes = [ATTR_VALUE];
31
- attributeChangedCallback(_name, _old, value) {
32
- const select = this.querySelector("select");
33
- if (select)
34
- select.value = value;
31
+ export class LbOptions extends WrappedControl {
32
+ inner = "select";
33
+ /**
34
+ * The value last set or chosen, which the options may not include yet.
35
+ * Null when nothing chose one, so the select's own default stands.
36
+ */
37
+ chosen = null;
38
+ get value() {
39
+ return this.chosen ?? super.value;
40
+ }
41
+ set value(value) {
42
+ this.chosen = value === null || value === undefined ? null : String(value);
43
+ super.value = value;
44
+ }
45
+ formResetCallback() {
46
+ this.chosen = null;
47
+ super.formResetCallback();
48
+ }
49
+ changed() {
50
+ this.chosen = this.control?.value ?? null;
51
+ super.changed();
35
52
  }
36
53
  /**
37
- * The hub reconciles the list and calls this for placement only, so this
54
+ * The hub reconciles the rows and calls this for placement only, so this
38
55
  * widget carries no copy of what a row is or when one arrives.
39
56
  */
40
57
  lbPlaceRow(el, row, template) {
@@ -49,7 +66,7 @@ export class LbOptions extends HTMLElement {
49
66
  /** Everything derived from the rows, once the whole result has landed. */
50
67
  lbRowsLanded() {
51
68
  // The identity of the row is the value of the option, so nothing on the
52
- // page has to say so twice.
69
+ // page has to say so.
53
70
  for (const option of this.querySelectorAll(`option[${ATTR_KEY_VALUE}]`)) {
54
71
  option.setAttribute("value", option.getAttribute(ATTR_KEY_VALUE));
55
72
  }
@@ -58,10 +75,11 @@ export class LbOptions extends HTMLElement {
58
75
  if (!group.querySelector("option"))
59
76
  group.remove();
60
77
  }
61
- // A value that landed before its option did selects it now.
62
- const value = this.getAttribute(ATTR_VALUE);
63
- if (value !== null)
64
- this.querySelector("select").value = value;
78
+ // A value that arrived before its option selects it now.
79
+ const select = this.querySelector("select");
80
+ if (select && this.chosen !== null)
81
+ select.value = this.chosen;
82
+ this.formValue();
65
83
  }
66
84
  /** The group with this label, or a new one in front of the template. */
67
85
  groupFor(select, template, label) {
@@ -74,25 +92,6 @@ export class LbOptions extends HTMLElement {
74
92
  select.insertBefore(group, template);
75
93
  return group;
76
94
  }
77
- connectedCallback() {
78
- this.addEventListener("change", () => {
79
- const select = this.querySelector("select");
80
- if (!select) {
81
- console.error(`lb-options: change with no <select>, ignoring`);
82
- return;
83
- }
84
- const name = this.getAttribute(ATTR_ACTION);
85
- if (!name && this.hasAttribute(ATTR_QUERY_PARM))
86
- return;
87
- if (!name) {
88
- console.error(`lb-options: no ${ATTR_ACTION}, nothing to send`);
89
- return;
90
- }
91
- // The hub scopes it, so a choice inside a row says which row.
92
- const detail = { action: name, value: select.value };
93
- this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
94
- });
95
- }
96
95
  }
97
96
  customElements.define("lb-options", LbOptions);
98
97
  //# sourceMappingURL=lb-options.browser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lb-options.browser.js","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_ACTION,\n ATTR_KEY_VALUE,\n ATTR_QUERY_PARM,\n ATTR_VALUE,\n LB_EVENT_NAME,\n} from \"@loadbare/app/constants\";\nimport type { HubRequest, ListHost, Row } from \"@loadbare/app/types\";\n\n/**\n * A `<select>` whose choices come from a list, and the case that shows why\n * placement belongs to the widget rather than to the framework.\n *\n * The rows arrive flat and keyed. Nothing on the wire knows about\n * `<optgroup>`, and nothing needs to: a group is a column like any other, and\n * turning a repeated column value into an element is exactly the local\n * knowledge a widget has and the hub does not. So the attribute that names\n * the column is this widget's own, in HTML's `data-` namespace rather than\n * the hub's: the hub never reads it, and `lb-` is reserved for what the hub\n * does read.\n *\n * The identity of a row is the value of the option, so the `lb-key-value`\n * the hub stamps on each option is copied to its `value`, and the developer\n * names the key column once, with `lb-key` on the template.\n *\n * It receives a value like any other cell, which selects the option whose\n * key it is. A picker that writes a query parm sends nothing: the hub reads\n * its choice off the bubbling `change` and lands the URL's back on it.\n *\n * Exported because `lb-picker` is this widget with its row template\n * supplied by the definition instead of by the page. Nothing about the\n * behavior changes, so nothing about the behavior is written twice.\n */\n/** Named on the row template: the column whose distinct values become groups. */\nconst GROUP_COLUMN = \"data-group\";\n\nexport class LbOptions extends HTMLElement implements ListHost {\n static observedAttributes = [ATTR_VALUE];\n\n attributeChangedCallback(_name: string, _old: string, value: string) {\n const select = this.querySelector(\"select\");\n if (select) select.value = value;\n }\n\n /**\n * The hub reconciles the list and calls this for placement only, so this\n * widget carries no copy of what a row is or when one arrives.\n */\n lbPlaceRow(el: Element, row: Row, template: HTMLTemplateElement): void {\n const select = this.querySelector(\"select\")!;\n const groupColumn = template.getAttribute(GROUP_COLUMN);\n if (!groupColumn) {\n select.insertBefore(el, template);\n return;\n }\n this.groupFor(select, template, String(row[groupColumn] ?? \"\")).append(el);\n }\n\n /** Everything derived from the rows, once the whole result has landed. */\n lbRowsLanded(): void {\n // The identity of the row is the value of the option, so nothing on the\n // page has to say so twice.\n for (const option of this.querySelectorAll(`option[${ATTR_KEY_VALUE}]`)) {\n option.setAttribute(\"value\", option.getAttribute(ATTR_KEY_VALUE)!);\n }\n // A group is derived, so it goes when its last row does.\n for (const group of this.querySelectorAll(\"optgroup\")) {\n if (!group.querySelector(\"option\")) group.remove();\n }\n // A value that landed before its option did selects it now.\n const value = this.getAttribute(ATTR_VALUE);\n if (value !== null) this.querySelector(\"select\")!.value = value;\n }\n\n /** The group with this label, or a new one in front of the template. */\n private groupFor(select: Element, template: Element, label: string): Element {\n for (const group of select.querySelectorAll(\"optgroup\")) {\n if (group.label === label) return group;\n }\n const group = document.createElement(\"optgroup\");\n group.label = label;\n select.insertBefore(group, template);\n return group;\n }\n\n connectedCallback() {\n this.addEventListener(\"change\", () => {\n const select = this.querySelector(\"select\");\n if (!select) {\n console.error(`lb-options: change with no <select>, ignoring`);\n return;\n }\n const name = this.getAttribute(ATTR_ACTION);\n if (!name && this.hasAttribute(ATTR_QUERY_PARM)) return;\n if (!name) {\n console.error(`lb-options: no ${ATTR_ACTION}, nothing to send`);\n return;\n }\n // The hub scopes it, so a choice inside a row says which row.\n const detail: HubRequest = { action: name, value: select.value };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\n });\n }\n}\n\ncustomElements.define(\"lb-options\", LbOptions);\n"]}
1
+ {"version":3,"file":"lb-options.browser.js","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { ATTR_KEY_VALUE } from \"@loadbare/app/constants\";\nimport type { Row, RowsHost } from \"@loadbare/app/types\";\n\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * A `<select>` whose choices are the rows of a query, and the case that\n * shows why placement belongs to the widget rather than to the framework.\n *\n * The rows arrive flat and keyed. Nothing on the wire knows about\n * `<optgroup>`, and nothing needs to: a group is a column like any other, and\n * turning a repeated column value into an element is exactly the local\n * knowledge a widget has and the hub does not. So the attribute that names\n * the column is this widget's own, in HTML's `data-` namespace rather than\n * the hub's: the hub never reads it, and `lb-` is reserved for what the hub\n * does read.\n *\n * The identity of a row is the value of the option, so the `lb-key-value`\n * the hub stamps on each option is copied to its `value`. The server declares\n * the key, and the page never names it.\n *\n * It is a control like `lb-select`: `lb-column` sets which option is\n * selected, from the row around it, and `lb-request` commits the choice on\n * `change`. A value can arrive before the options it names, so the widget\n * keeps the value it was last given and selects it again whenever rows land.\n *\n * Exported because `lb-picker` is this widget with its row template\n * supplied by the definition instead of by the page. Nothing about the\n * behavior changes, so nothing about the behavior is written twice.\n */\n/** Named on the row template: the column whose distinct values become groups. */\nconst GROUP_COLUMN = \"data-group\";\n\nexport class LbOptions extends WrappedControl implements RowsHost {\n protected readonly inner = \"select\";\n\n /**\n * The value last set or chosen, which the options may not include yet.\n * Null when nothing chose one, so the select's own default stands.\n */\n private chosen: string | null = null;\n\n get value(): string {\n return this.chosen ?? super.value;\n }\n\n set value(value: string) {\n this.chosen = value === null || value === undefined ? null : String(value);\n super.value = value;\n }\n\n formResetCallback(): void {\n this.chosen = null;\n super.formResetCallback();\n }\n\n protected changed(): void {\n this.chosen = this.control?.value ?? null;\n super.changed();\n }\n\n /**\n * The hub reconciles the rows and calls this for placement only, so this\n * widget carries no copy of what a row is or when one arrives.\n */\n lbPlaceRow(el: Element, row: Row, template: HTMLTemplateElement): void {\n const select = this.querySelector(\"select\")!;\n const groupColumn = template.getAttribute(GROUP_COLUMN);\n if (!groupColumn) {\n select.insertBefore(el, template);\n return;\n }\n this.groupFor(select, template, String(row[groupColumn] ?? \"\")).append(el);\n }\n\n /** Everything derived from the rows, once the whole result has landed. */\n lbRowsLanded(): void {\n // The identity of the row is the value of the option, so nothing on the\n // page has to say so.\n for (const option of this.querySelectorAll(`option[${ATTR_KEY_VALUE}]`)) {\n option.setAttribute(\"value\", option.getAttribute(ATTR_KEY_VALUE)!);\n }\n // A group is derived, so it goes when its last row does.\n for (const group of this.querySelectorAll(\"optgroup\")) {\n if (!group.querySelector(\"option\")) group.remove();\n }\n // A value that arrived before its option selects it now.\n const select = this.querySelector(\"select\");\n if (select && this.chosen !== null) select.value = this.chosen;\n this.formValue();\n }\n\n /** The group with this label, or a new one in front of the template. */\n private groupFor(select: Element, template: Element, label: string): Element {\n for (const group of select.querySelectorAll(\"optgroup\")) {\n if (group.label === label) return group;\n }\n const group = document.createElement(\"optgroup\");\n group.label = label;\n select.insertBefore(group, template);\n return group;\n }\n}\n\ncustomElements.define(\"lb-options\", LbOptions);\n"]}
@@ -1,4 +1,4 @@
1
1
  <label
2
2
  >{{label}}
3
- <select lb-slot></select
3
+ <select lb-exp-slot></select
4
4
  ></label>
@@ -6,9 +6,10 @@ import { LbOptions } from "./lb-options.browser.js";
6
6
  *
7
7
  * A row template is markup, so writing it is the page author's job — until
8
8
  * every row is one option and the only thing that varies is which columns it
9
- * reads. Those are words, and words are parameters: `exp-key`, `exp-cell` and
9
+ * reads. Those are words, and words are parameters: `exp-column` and
10
10
  * `exp-group` fill the placeholders in the template the definition already
11
- * has, at build time, and the page writes one tag with no children.
11
+ * has, at build time, and the page writes one tag with no children. The key
12
+ * is the server's, declared with the query, so the page names none.
12
13
  *
13
14
  * It is the same trade as anywhere else in expansion. A page author who wants
14
15
  * a second element in the row, or an option built from two columns, writes
@@ -16,9 +17,9 @@ import { LbOptions } from "./lb-options.browser.js";
16
17
  * not.
17
18
  *
18
19
  * The behavior is inherited whole, because it is the same behavior: rows land
19
- * in groups, the key becomes the value, and a change sends the declared
20
- * action. A distinct class exists only because `customElements.define`
21
- * requires one constructor per name.
20
+ * in groups, the key becomes the value, and the widget is a control. A
21
+ * distinct class exists only because `customElements.define` requires one
22
+ * constructor per name.
22
23
  */
23
24
  class LbPicker extends LbOptions {
24
25
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lb-picker.browser.js","sourceRoot":"","sources":["../widgets/lb-picker.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,QAAS,SAAQ,SAAS;CAAG;AAEnC,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { LbOptions } from \"./lb-options.browser.js\";\n\n/**\n * The same `<select>` as `lb-options`, with the row template supplied by\n * the definition instead of by the page.\n *\n * A row template is markup, so writing it is the page author's job — until\n * every row is one option and the only thing that varies is which columns it\n * reads. Those are words, and words are parameters: `exp-key`, `exp-cell` and\n * `exp-group` fill the placeholders in the template the definition already\n * has, at build time, and the page writes one tag with no children.\n *\n * It is the same trade as anywhere else in expansion. A page author who wants\n * a second element in the row, or an option built from two columns, writes\n * `lb-options` and the template themselves. This one is for when they do\n * not.\n *\n * The behavior is inherited whole, because it is the same behavior: rows land\n * in groups, the key becomes the value, and a change sends the declared\n * action. A distinct class exists only because `customElements.define`\n * requires one constructor per name.\n */\nclass LbPicker extends LbOptions {}\n\ncustomElements.define(\"lb-picker\", LbPicker);\n"]}
1
+ {"version":3,"file":"lb-picker.browser.js","sourceRoot":"","sources":["../widgets/lb-picker.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,QAAS,SAAQ,SAAS;CAAG;AAEnC,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { LbOptions } from \"./lb-options.browser.js\";\n\n/**\n * The same `<select>` as `lb-options`, with the row template supplied by\n * the definition instead of by the page.\n *\n * A row template is markup, so writing it is the page author's job — until\n * every row is one option and the only thing that varies is which columns it\n * reads. Those are words, and words are parameters: `exp-column` and\n * `exp-group` fill the placeholders in the template the definition already\n * has, at build time, and the page writes one tag with no children. The key\n * is the server's, declared with the query, so the page names none.\n *\n * It is the same trade as anywhere else in expansion. A page author who wants\n * a second element in the row, or an option built from two columns, writes\n * `lb-options` and the template themselves. This one is for when they do\n * not.\n *\n * The behavior is inherited whole, because it is the same behavior: rows land\n * in groups, the key becomes the value, and the widget is a control. A\n * distinct class exists only because `customElements.define` requires one\n * constructor per name.\n */\nclass LbPicker extends LbOptions {}\n\ncustomElements.define(\"lb-picker\", LbPicker);\n"]}
@@ -1,7 +1,7 @@
1
1
  <label
2
2
  >{{label}}
3
3
  <select>
4
- <template lb-key="{{key}}" data-group="{{group}}">
5
- <option lb-cell="{{cell}}"></option>
4
+ <template data-group="{{group}}">
5
+ <option lb-column="{{column}}"></option>
6
6
  </template></select
7
7
  ></label>
@@ -1,46 +1,17 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_ACTION, ATTR_QUERY_PARM, ATTR_VALUE, LB_EVENT_NAME, } from "@loadbare/app/constants";
2
+ import { WrappedControl } from "./control.js";
3
3
  /**
4
- * A leaf cell wrapping a <select>. It receives its value like any other cell,
5
- * and on change it sends the action the developer declared for it — the
6
- * request vocabulary and the binding vocabulary running in both directions on
7
- * one widget.
4
+ * A control wrapping a `<select>` whose options the page writes. The hub
5
+ * sets its `value` from the column its `lb-column` names, and gathers it back
6
+ * under the same name.
8
7
  *
9
- * The choice is the interaction, so this is the case where an action carries
10
- * a value. The server still decides what that choice means.
11
- *
12
- * A select that writes a query parm sends nothing: the hub reads its choice
13
- * off the bubbling `change` and puts it in the address bar.
8
+ * The choice is the interaction, so this is the control that most often
9
+ * carries `lb-request`: it commits on `change`, and the hub sends that request
10
+ * with this select's column alone. The server still decides what the choice
11
+ * means.
14
12
  */
15
- class LbSelect extends HTMLElement {
16
- static observedAttributes = [ATTR_VALUE];
17
- attributeChangedCallback(_name, _old, value) {
18
- const select = this.querySelector("select");
19
- if (!select) {
20
- console.error(`lb-select: no <select> to receive the value`);
21
- return;
22
- }
23
- select.value = value;
24
- }
25
- connectedCallback() {
26
- this.addEventListener("change", () => {
27
- const select = this.querySelector("select");
28
- if (!select) {
29
- console.error(`lb-select: change with no <select>, ignoring`);
30
- return;
31
- }
32
- const name = this.getAttribute(ATTR_ACTION);
33
- if (!name && this.hasAttribute(ATTR_QUERY_PARM))
34
- return;
35
- if (!name) {
36
- console.error(`lb-select: no ${ATTR_ACTION}, nothing to send`);
37
- return;
38
- }
39
- // The hub scopes it, so a choice inside a row says which row.
40
- const detail = { action: name, value: select.value };
41
- this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
42
- });
43
- }
13
+ class LbSelect extends WrappedControl {
14
+ inner = "select";
44
15
  }
45
16
  customElements.define("lb-select", LbSelect);
46
17
  //# sourceMappingURL=lb-select.browser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lb-select.browser.js","sourceRoot":"","sources":["../widgets/lb-select.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EACL,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,GACd,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;;GAWG;AACH,MAAM,QAAS,SAAQ,WAAW;IAChC,MAAM,CAAC,kBAAkB,GAAG,CAAC,UAAU,CAAC,CAAC;IAEzC,wBAAwB,CAAC,KAAa,EAAE,IAAY,EAAE,KAAa;QACjE,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QACD,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,GAAG,EAAE;YACnC,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;gBAC9D,OAAO;YACT,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;YAC5C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC;gBAAE,OAAO;YACxD,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,CAAC,KAAK,CAAC,iBAAiB,WAAW,mBAAmB,CAAC,CAAC;gBAC/D,OAAO;YACT,CAAC;YACD,8DAA8D;YAC9D,MAAM,MAAM,GAAe,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;YACjE,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAC1D,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;;AAGH,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_ACTION,\n ATTR_QUERY_PARM,\n ATTR_VALUE,\n LB_EVENT_NAME,\n} from \"@loadbare/app/constants\";\nimport type { HubRequest } from \"@loadbare/app/types\";\n\n/**\n * A leaf cell wrapping a <select>. It receives its value like any other cell,\n * and on change it sends the action the developer declared for it — the\n * request vocabulary and the binding vocabulary running in both directions on\n * one widget.\n *\n * The choice is the interaction, so this is the case where an action carries\n * a value. The server still decides what that choice means.\n *\n * A select that writes a query parm sends nothing: the hub reads its choice\n * off the bubbling `change` and puts it in the address bar.\n */\nclass LbSelect extends HTMLElement {\n static observedAttributes = [ATTR_VALUE];\n\n attributeChangedCallback(_name: string, _old: string, value: string) {\n const select = this.querySelector(\"select\");\n if (!select) {\n console.error(`lb-select: no <select> to receive the value`);\n return;\n }\n select.value = value;\n }\n\n connectedCallback() {\n this.addEventListener(\"change\", () => {\n const select = this.querySelector(\"select\");\n if (!select) {\n console.error(`lb-select: change with no <select>, ignoring`);\n return;\n }\n const name = this.getAttribute(ATTR_ACTION);\n if (!name && this.hasAttribute(ATTR_QUERY_PARM)) return;\n if (!name) {\n console.error(`lb-select: no ${ATTR_ACTION}, nothing to send`);\n return;\n }\n // The hub scopes it, so a choice inside a row says which row.\n const detail: HubRequest = { action: name, value: select.value };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\n });\n }\n}\n\ncustomElements.define(\"lb-select\", LbSelect);\n"]}
1
+ {"version":3,"file":"lb-select.browser.js","sourceRoot":"","sources":["../widgets/lb-select.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,QAAS,SAAQ,cAAc;IAChB,KAAK,GAAG,QAAQ,CAAC;CACrC;AAED,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * A control wrapping a `<select>` whose options the page writes. The hub\n * sets its `value` from the column its `lb-column` names, and gathers it back\n * under the same name.\n *\n * The choice is the interaction, so this is the control that most often\n * carries `lb-request`: it commits on `change`, and the hub sends that request\n * with this select's column alone. The server still decides what the choice\n * means.\n */\nclass LbSelect extends WrappedControl {\n protected readonly inner = \"select\";\n}\n\ncustomElements.define(\"lb-select\", LbSelect);\n"]}
@@ -1,4 +1,4 @@
1
1
  <label
2
2
  >{{label}}
3
- <select lb-slot></select
3
+ <select lb-exp-slot></select
4
4
  ></label>