mi-element 0.0.1 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024-present, commenthol and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,2 +1,90 @@
1
1
  # mi-element
2
2
 
3
+ > a lightweight alternative to write web components
4
+
5
+ mi-element provides further features to build web applications through
6
+ [Web Components][] like:
7
+
8
+ - controllers to hook into the components lifecycle
9
+ - ContextProvider, ContextConsumer for data provisioning from outside of a
10
+ component
11
+ - Store for managing shared state across components
12
+
13
+ # Usage
14
+
15
+ In your project:
16
+
17
+ ```
18
+ npm i mi-element
19
+ ```
20
+
21
+ ```js
22
+ /** @file ./mi-counter.js */
23
+
24
+ import { MiElement, define, refsById } from 'mi-element'
25
+
26
+ // define your Component
27
+ class MiCounter extends MiElement {
28
+ static template = `
29
+ <style>
30
+ :host { font-size: 1.25rem; }
31
+ </style>
32
+ <button id="decrement" aria-label="Decrement counter"> - </button>
33
+ <span id aria-label="Counter value">0</span>
34
+ <button id="increment" aria-label="Increment counter"> + </button>
35
+ `
36
+
37
+ // declare reactive attributes
38
+ static get attributes() {
39
+ return { count: 0 }
40
+ }
41
+
42
+ // called by connectedCallback()
43
+ render() {
44
+ // gather refs from template (here by id)
45
+ this.refs = refsById(this.renderRoot)
46
+ // apply event listeners
47
+ this.refs.button.decrement.addEventListener('click', () => this.count--)
48
+ this.refs.button.increment.addEventListener('click', () => this.count++)
49
+ }
50
+
51
+ // called on every change of an observed attributes via `this.requestUpdate()`
52
+ update() {
53
+ this.refs.span.textContent = this.count
54
+ }
55
+ }
56
+
57
+ // create the custom element
58
+ define('mi-counter', MiCounter)
59
+ ```
60
+
61
+ Now use in your HTML
62
+
63
+ ```html
64
+ <body>
65
+ <mi-counter></mi-counter>
66
+ <mi-counter count="-3"></mi-counter>
67
+ <script type="module" src="./mi-counter.js"></script>
68
+ </body>
69
+ ```
70
+
71
+ In `./example` you'll find a working sample of a Todo App. Check it out with
72
+ `npm run example`
73
+
74
+ # Documentation
75
+
76
+ - [lifecycle][docs-lifecycle] mi-element's lifecycle
77
+ - [controller][docs-controller] adding controllers to mi-element to hook into the lifecycle
78
+ - [context][docs-context] Implementation of the [Context Protocol][].
79
+ - [store][docs-store] Manage shared state in an application
80
+
81
+ # License
82
+
83
+ MIT licensed
84
+
85
+ [Context Protocol]: https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md
86
+ [Web Components]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks
87
+ [docs-lifecycle]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/lifecycle.md
88
+ [docs-controller]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/controller.md
89
+ [docs-context]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/context.md
90
+ [docs-store]: https://github.com/commenthol/mi-element/tree/main/packages/mi-element/docs/store.md
@@ -0,0 +1,62 @@
1
+ import { isSignalLike, createSignal } from './signal.js';
2
+
3
+ class ContextProvider {
4
+ constructor(host, context, initialValue) {
5
+ this.host = host, this.context = context, this.state = isSignalLike(initialValue) ? initialValue : createSignal(initialValue),
6
+ this.host.addController?.(this);
7
+ }
8
+ hostConnected() {
9
+ this.host.addEventListener("context-request", this.onContextRequest);
10
+ }
11
+ hostDisconnected() {
12
+ this.host.removeEventListener("context-request", this.onContextRequest);
13
+ }
14
+ set value(newValue) {
15
+ this.state.value = newValue;
16
+ }
17
+ get value() {
18
+ return this.state.value;
19
+ }
20
+ notify() {
21
+ this.state.notify();
22
+ }
23
+ onContextRequest=ev => {
24
+ if (ev.context !== this.context) return;
25
+ ev.stopPropagation();
26
+ const unsubscribe = ev.subscribe ? this.state.subscribe(ev.callback) : void 0;
27
+ ev.callback(this.value, unsubscribe);
28
+ };
29
+ }
30
+
31
+ class ContextRequestEvent extends Event {
32
+ constructor(context, callback, subscribe) {
33
+ super("context-request", {
34
+ bubbles: !0,
35
+ composed: !0
36
+ }), this.context = context, this.callback = callback, this.subscribe = subscribe;
37
+ }
38
+ }
39
+
40
+ class ContextConsumer {
41
+ constructor(host, context, options) {
42
+ const {subscribe: subscribe = !1, validate: validate = () => !0} = options || {};
43
+ this.host = host, this.context = context, this.subscribe = !!subscribe, this.validate = validate,
44
+ this.value = void 0, this.unsubscribe = void 0, this.host.addController?.(this);
45
+ }
46
+ hostConnected() {
47
+ this.dispatchRequest();
48
+ }
49
+ hostDisconnected() {
50
+ this.unsubscribe && (this.unsubscribe(), this.unsubscribe = void 0);
51
+ }
52
+ dispatchRequest() {
53
+ this.host.dispatchEvent(new ContextRequestEvent(this.context, this._callback.bind(this), this.subscribe));
54
+ }
55
+ _callback(value, unsubscribe) {
56
+ unsubscribe && (this.subscribe ? this.unsubscribe && (this.unsubscribe !== unsubscribe && this.unsubscribe(),
57
+ this.unsubscribe = unsubscribe) : unsubscribe()), this.validate(value) && (this.value = value,
58
+ this.host.requestUpdate());
59
+ }
60
+ }
61
+
62
+ export { ContextConsumer, ContextProvider, ContextRequestEvent };
@@ -0,0 +1,109 @@
1
+ class MiElement extends HTMLElement {
2
+ #attr={};
3
+ #attrLc=new Map;
4
+ #types=new Map;
5
+ #disposers=new Set;
6
+ #controllers=new Set;
7
+ #changedAttr={};
8
+ static shadowRootOptions={
9
+ mode: 'open'
10
+ };
11
+ constructor() {
12
+ super(), this.#attr = {
13
+ ...this.constructor.attributes
14
+ }, this.#observedAttributes();
15
+ }
16
+ #observedAttributes() {
17
+ for (const [name, val] of Object.entries(this.#attr)) this.#types.set(name, initialType(val)),
18
+ this.#attrLc.set(name.toLowerCase(), name), Object.defineProperty(this, name, {
19
+ enumerable: !0,
20
+ get() {
21
+ return this.#attr[name];
22
+ },
23
+ set(newValue) {
24
+ this.#attr[name] !== newValue && (this.#attr[name] = newValue, this.#changedAttr[name] = newValue,
25
+ this.requestUpdate());
26
+ }
27
+ });
28
+ }
29
+ #getName(name) {
30
+ return this.#attrLc.get(name) || name;
31
+ }
32
+ #getType(name) {
33
+ return this.#types.get(name);
34
+ }
35
+ connectedCallback() {
36
+ this.#controllers.forEach((controller => controller.hostConnected?.()));
37
+ const {shadowRootOptions: shadowRootOptions, template: template} = this.constructor;
38
+ this.renderRoot = shadowRootOptions ? this.shadowRoot ?? this.attachShadow(shadowRootOptions) : this,
39
+ this.addTemplate(template), this.render(), this.requestUpdate();
40
+ }
41
+ disconnectedCallback() {
42
+ this.#disposers.forEach((remover => remover())), this.#controllers.forEach((controller => controller.hostDisconnected?.()));
43
+ }
44
+ attributeChangedCallback(name, _oldValue, newValue) {
45
+ const attr = this.#getName(name), type = this.#getType(attr), _newValue = convertType(newValue, type);
46
+ this.#attr[attr] = this.#changedAttr[attr] = _newValue, 'Boolean' === type && 'false' === newValue && this.removeAttribute(name),
47
+ this.requestUpdate();
48
+ }
49
+ setAttribute(name, newValue) {
50
+ const attr = this.#getName(name);
51
+ if (!(attr in this.#attr)) return;
52
+ const type = this.#getType(attr);
53
+ this.#attr[attr] = this.#changedAttr[attr] = newValue, 'Boolean' === type ? !0 === newValue || '' === newValue ? super.setAttribute(name, '') : super.removeAttribute(name) : [ 'String', 'Number' ].includes(type) || !0 === newValue ? super.setAttribute(name, newValue) : this.requestUpdate();
54
+ }
55
+ requestUpdate() {
56
+ this.isConnected && requestAnimationFrame((() => {
57
+ this.shouldUpdate(this.#changedAttr) && this.update(this.#changedAttr), this.#changedAttr = {};
58
+ }));
59
+ }
60
+ addTemplate(template) {
61
+ template instanceof HTMLTemplateElement && this.renderRoot.appendChild(template.content.cloneNode(!0));
62
+ }
63
+ render() {}
64
+ shouldUpdate(_changedAttributes) {
65
+ return !0;
66
+ }
67
+ update(_changedAttributes) {}
68
+ on(eventName, listener, node = this) {
69
+ node.addEventListener(eventName, listener), this.#disposers.add((() => node.removeEventListener(eventName, listener)));
70
+ }
71
+ once(eventName, listener, node = this) {
72
+ node.addEventListener(eventName, listener, {
73
+ once: !0
74
+ });
75
+ }
76
+ dispose(listener) {
77
+ if ('function' != typeof listener) throw new TypeError('listener must be a function');
78
+ this.#disposers.add(listener);
79
+ }
80
+ addController(controller) {
81
+ this.#controllers.add(controller), this.isConnected && controller.hostConnected?.();
82
+ }
83
+ removeController(controller) {
84
+ this.#controllers.delete(controller);
85
+ }
86
+ }
87
+
88
+ const define = (name, element, options) => {
89
+ element.observedAttributes = (element.observedAttributes || Object.keys(element.attributes || [])).map((attr => attr.toLowerCase())),
90
+ renderTemplate(element), window.customElements.define(name, element, options);
91
+ }, renderTemplate = element => {
92
+ if ('string' != typeof element.template) return;
93
+ const el = document.createElement('template');
94
+ el.innerHTML = element.template, element.template = el;
95
+ }, initialType = value => toString.call(value).slice(8, -1), convertType = (any, type) => {
96
+ switch (type) {
97
+ case 'Number':
98
+ return (any => {
99
+ const n = Number(any);
100
+ return isNaN(n) ? any : n;
101
+ })(any);
102
+
103
+ case 'Boolean':
104
+ return 'false' !== any && ('' === any || !!any);
105
+ }
106
+ return any;
107
+ };
108
+
109
+ export { MiElement, convertType, define };
package/dist/escape.js ADDED
@@ -0,0 +1,9 @@
1
+ const escMap = {
2
+ '&': '&amp;',
3
+ '<': '&lt;',
4
+ '>': '&gt;',
5
+ "'": '&#39;',
6
+ '"': '&quot;'
7
+ }, escHtml = string => ('' + string).replace(/&amp;/g, '&').replace(/[&<>'"]/g, (tag => escMap[tag])), escAttr = string => ('' + string).replace(/['"]/g, (tag => escMap[tag])), esc = (strings, ...vars) => strings.map(((string, i) => string + escHtml(vars[i] ?? ''))).join('');
8
+
9
+ export { esc, escAttr, escHtml };
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { ContextConsumer, ContextProvider, ContextRequestEvent } from './context.js';
2
+
3
+ export { MiElement, convertType, define } from './element.js';
4
+
5
+ export { esc, escAttr, escHtml } from './escape.js';
6
+
7
+ export { refsById, refsBySelector } from './refs.js';
8
+
9
+ export { Signal, createSignal, isSignalLike } from './signal.js';
10
+
11
+ export { Store, subscribeToStore } from './store.js';
package/dist/refs.js ADDED
@@ -0,0 +1,15 @@
1
+ function refsById(container) {
2
+ const nodes = container.querySelectorAll?.('[id]') || [], found = {};
3
+ for (const node of nodes) found[kebabToCamelCase(node.getAttribute('id') || node.nodeName.toLowerCase())] = node;
4
+ return found;
5
+ }
6
+
7
+ const kebabToCamelCase = (str = "") => str.toLowerCase().replace(/[-_]\w/g, (m => m[1].toUpperCase()));
8
+
9
+ function refsBySelector(container, selectors) {
10
+ const found = {};
11
+ for (const [name, selector] of Object.entries(selectors)) found[name] = container.querySelector?.(selector);
12
+ return found;
13
+ }
14
+
15
+ export { kebabToCamelCase, refsById, refsBySelector };
package/dist/signal.js ADDED
@@ -0,0 +1,24 @@
1
+ class Signal {
2
+ _subscribers=new Set;
3
+ constructor(initialValue) {
4
+ this._value = initialValue;
5
+ }
6
+ get value() {
7
+ return this._value;
8
+ }
9
+ set value(newValue) {
10
+ this._value !== newValue && (this._value = newValue, this.notify());
11
+ }
12
+ notify() {
13
+ for (const callback of this._subscribers) callback(this._value);
14
+ }
15
+ subscribe(callback) {
16
+ return this._subscribers.add(callback), () => {
17
+ this._subscribers.delete(callback);
18
+ };
19
+ }
20
+ }
21
+
22
+ const createSignal = initialValue => new Signal(initialValue), isSignalLike = possibleSignal => 'function' == typeof possibleSignal?.subscribe && 'function' == typeof possibleSignal?.notify && 'value' in possibleSignal;
23
+
24
+ export { Signal, createSignal, isSignalLike };
package/dist/store.js ADDED
@@ -0,0 +1,28 @@
1
+ import { Signal, isSignalLike } from './signal.js';
2
+
3
+ class Store extends Signal {
4
+ constructor(actions, initialValue) {
5
+ super(initialValue);
6
+ for (const [action, dispatcher] of Object.entries(actions)) this[action] = data => {
7
+ this.value = dispatcher(data)(this.value);
8
+ };
9
+ }
10
+ }
11
+
12
+ const subscribeToStore = (element, store, propOrSignal) => {
13
+ if (propOrSignal instanceof Signal || isSignalLike(propOrSignal)) return void element.dispose(store.subscribe((value => {
14
+ propOrSignal.value = value;
15
+ })));
16
+ const keys = Array.isArray(propOrSignal) ? propOrSignal : propOrSignal.split('.').filter(Boolean), last = keys.pop();
17
+ if (!last) throw TypeError('need prop');
18
+ let tmp = element;
19
+ for (const key of keys) {
20
+ if ('object' != typeof tmp[key]) throw new TypeError(`object expected for property "${key}"`);
21
+ tmp = tmp[key];
22
+ }
23
+ element.dispose(store.subscribe((value => {
24
+ tmp[last] = value, element.requestUpdate();
25
+ })));
26
+ };
27
+
28
+ export { Store, subscribeToStore };
package/package.json CHANGED
@@ -1,13 +1,96 @@
1
1
  {
2
2
  "name": "mi-element",
3
- "version": "0.0.1",
4
- "description": "",
3
+ "version": "0.1.0",
4
+ "description": "Build lightweight reactive micro web-components",
5
5
  "keywords": [],
6
+ "homepage": "https://github.com/commenthol/mi-element/tree/main/packages/mi-element#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/commenthol/mi-element/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/commenthol/mi-element.git",
13
+ "directory": "packages/mi-element"
14
+ },
6
15
  "license": "MIT",
7
16
  "author": "commenthol <commenthol@gmail.com>",
17
+ "maintainers": [
18
+ "commenthol <commenthol@gmail.com>"
19
+ ],
8
20
  "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "default": "./dist/index.js",
24
+ "development": "./src/index.js",
25
+ "types": "./types/index.d.ts"
26
+ },
27
+ "./element": {
28
+ "default": "./dist/element.js",
29
+ "development": "./src/element.js",
30
+ "types": "./types/element.d.ts"
31
+ },
32
+ "./context": {
33
+ "default": "./dist/context.js",
34
+ "development": "./src/context.js",
35
+ "types": "./types/context.d.ts"
36
+ },
37
+ "./escape": {
38
+ "default": "./dist/escape.js",
39
+ "development": "./src/escape.js",
40
+ "types": "./types/escape.d.ts"
41
+ },
42
+ "./refs": {
43
+ "default": "./dist/refs.js",
44
+ "development": "./src/refs.js",
45
+ "types": "./types/refs.d.ts"
46
+ },
47
+ "./signal": {
48
+ "default": "./dist/signal.js",
49
+ "development": "./src/signal.js",
50
+ "types": "./types/signal.d.ts"
51
+ },
52
+ "./store": {
53
+ "default": "./dist/store.js",
54
+ "development": "./src/store.js",
55
+ "types": "./types/store.d.ts"
56
+ },
57
+ "./package.json": {
58
+ "default": "./package.json"
59
+ }
60
+ },
9
61
  "main": "src/index.js",
62
+ "files": [
63
+ "src",
64
+ "dist",
65
+ "types"
66
+ ],
67
+ "devDependencies": {
68
+ "@eslint/js": "^9.9.1",
69
+ "@rollup/plugin-terser": "^0.4.4",
70
+ "@testing-library/dom": "^10.4.0",
71
+ "@types/node": "^22.5.0",
72
+ "@vitest/browser": "^2.0.5",
73
+ "@vitest/coverage-istanbul": "^2.0.5",
74
+ "eslint": "^9.9.1",
75
+ "globals": "^15.9.0",
76
+ "npm-run-all2": "^6.2.2",
77
+ "playwright": "^1.46.1",
78
+ "prettier": "^3.3.3",
79
+ "rimraf": "^6.0.1",
80
+ "rollup": "^4.21.1",
81
+ "typescript": "^5.5.4",
82
+ "vite": "^5.4.2",
83
+ "vitest": "^2.0.5"
84
+ },
10
85
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1"
86
+ "all": "npm-run-all lint test build",
87
+ "build": "rimraf dist && rollup -c && gzip dist/index.min.js && ls -al dist && rimraf dist/index.min.*",
88
+ "example": "vite --open /example/",
89
+ "lint": "eslint",
90
+ "pretty": "prettier -w **/*.js",
91
+ "test": "vitest run --coverage",
92
+ "test:browser": "vitest --coverage",
93
+ "dev": "npm run test:browser",
94
+ "types": "tsc"
12
95
  }
13
- }
96
+ }
package/src/context.js ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * @see https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md
3
+ */
4
+
5
+ import { createSignal, isSignalLike } from './signal.js'
6
+
7
+ /**
8
+ * @typedef {import('./element.js').HostController} HostController
9
+ */
10
+ /**
11
+ * @typedef {string|Symbol} Context
12
+ */
13
+
14
+ const CONTEXT_REQUEST = 'context-request'
15
+
16
+ /**
17
+ * @implements {HostController}
18
+ */
19
+ export class ContextProvider {
20
+ /**
21
+ * @param {HTMLElement} host
22
+ * @param {Context} context
23
+ * @param {any} initialValue
24
+ */
25
+ constructor(host, context, initialValue) {
26
+ this.host = host
27
+ this.context = context
28
+ this.state = isSignalLike(initialValue)
29
+ ? initialValue
30
+ : createSignal(initialValue)
31
+ // @ts-expect-error
32
+ this.host.addController?.(this)
33
+ }
34
+
35
+ hostConnected() {
36
+ // @ts-expect-error
37
+ this.host.addEventListener(CONTEXT_REQUEST, this.onContextRequest)
38
+ }
39
+
40
+ hostDisconnected() {
41
+ // @ts-expect-error
42
+ this.host.removeEventListener(CONTEXT_REQUEST, this.onContextRequest)
43
+ }
44
+
45
+ /**
46
+ * @param {any} newValue
47
+ */
48
+ set value(newValue) {
49
+ this.state.value = newValue
50
+ }
51
+
52
+ /**
53
+ * @returns {any}
54
+ */
55
+ get value() {
56
+ return this.state.value
57
+ }
58
+
59
+ notify() {
60
+ this.state.notify()
61
+ }
62
+
63
+ /**
64
+ * @private
65
+ * @param {ContextRequestEvent} ev
66
+ */
67
+ onContextRequest = (ev) => {
68
+ if (ev.context !== this.context) {
69
+ // event has wrong context
70
+ return
71
+ }
72
+ ev.stopPropagation()
73
+ const unsubscribe = ev.subscribe
74
+ ? this.state.subscribe(ev.callback)
75
+ : undefined
76
+ ev.callback(this.value, unsubscribe)
77
+ }
78
+ }
79
+
80
+ export class ContextRequestEvent extends Event {
81
+ /**
82
+ * @param {Context} context
83
+ * @param {(value: any, unsubscribe?: () => void) => void} callback
84
+ * @param {boolean} [subscribe=false] subscribe to value changes
85
+ */
86
+ constructor(context, callback, subscribe) {
87
+ super(CONTEXT_REQUEST, { bubbles: true, composed: true })
88
+ this.context = context
89
+ this.callback = callback
90
+ this.subscribe = subscribe
91
+ }
92
+ }
93
+
94
+ /**
95
+ * @implements {HostController}
96
+ */
97
+ export class ContextConsumer {
98
+ /**
99
+ * @param {HTMLElement} host
100
+ * @param {Context} context
101
+ * @param {object} [options]
102
+ * @param {boolean} [options.subscribe=false] subscribe to value changes
103
+ * @param {(any) => boolean} [options.validate] validation function
104
+ */
105
+ constructor(host, context, options) {
106
+ const { subscribe = false, validate = () => true } = options || {}
107
+ this.host = host
108
+ this.context = context
109
+ this.subscribe = !!subscribe
110
+ this.validate = validate
111
+ // initial value yet unknown
112
+ this.value = undefined
113
+ // unsubscribe function
114
+ this.unsubscribe = undefined
115
+ // add the controller in case of a MiElement otherwise call hostConnected()
116
+ // and hostDisconnected() from the host element
117
+ // @ts-expect-error
118
+ this.host.addController?.(this)
119
+ }
120
+
121
+ hostConnected() {
122
+ this.dispatchRequest()
123
+ }
124
+
125
+ hostDisconnected() {
126
+ if (this.unsubscribe) {
127
+ this.unsubscribe()
128
+ this.unsubscribe = undefined
129
+ }
130
+ }
131
+
132
+ dispatchRequest() {
133
+ this.host.dispatchEvent(
134
+ new ContextRequestEvent(
135
+ this.context,
136
+ this._callback.bind(this),
137
+ this.subscribe
138
+ )
139
+ )
140
+ }
141
+
142
+ _callback(value, unsubscribe) {
143
+ if (unsubscribe) {
144
+ if (!this.subscribe) {
145
+ // unsubscribe as we didn't ask for subscription
146
+ unsubscribe()
147
+ } else if (this.unsubscribe) {
148
+ if (this.unsubscribe !== unsubscribe) {
149
+ // looks there was a previous provider
150
+ this.unsubscribe()
151
+ }
152
+ this.unsubscribe = unsubscribe
153
+ }
154
+ }
155
+ if (!this.validate(value)) {
156
+ return
157
+ }
158
+ this.value = value
159
+ // @ts-expect-error
160
+ this.host.requestUpdate()
161
+ }
162
+ }