custom-elements-ts 0.0.17 → 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.
Files changed (69) hide show
  1. package/.eslintrc.json +46 -0
  2. package/.github/workflows/ci.yml +49 -0
  3. package/.prettierrc +7 -0
  4. package/LICENSE +20 -0
  5. package/README.md +270 -35
  6. package/demos/counter/counter.element.html +1 -0
  7. package/demos/counter/counter.element.scss +234 -0
  8. package/demos/counter/counter.element.ts +68 -0
  9. package/demos/counter/index.html +205 -0
  10. package/demos/counter/index.ts +1 -0
  11. package/demos/site/code-example/code-example.element.scss +168 -0
  12. package/demos/site/code-example/code-example.element.ts +88 -0
  13. package/demos/site/event-log/event-log.element.scss +179 -0
  14. package/demos/site/event-log/event-log.element.ts +134 -0
  15. package/demos/site/favicon.svg +14 -0
  16. package/demos/site/index.html +346 -0
  17. package/demos/site/index.ts +13 -0
  18. package/demos/site/message/message.element.scss +75 -0
  19. package/demos/site/message/message.element.ts +76 -0
  20. package/demos/site/og-image.png +0 -0
  21. package/demos/site/styles/site.css +1023 -0
  22. package/demos/site/styles/tokens.css +56 -0
  23. package/demos/site/toast/toast.element.scss +110 -0
  24. package/demos/site/toast/toast.element.ts +63 -0
  25. package/demos/todo-dashboard/index.html +141 -0
  26. package/demos/todo-dashboard/index.ts +4 -0
  27. package/demos/todo-dashboard/todo-dashboard.element.scss +1145 -0
  28. package/demos/todo-dashboard/todo-dashboard.element.ts +332 -0
  29. package/demos/todo-dashboard/todo-filters.element.ts +54 -0
  30. package/demos/todo-dashboard/todo-item.element.ts +126 -0
  31. package/demos/todo-dashboard/todo-stats.element.ts +189 -0
  32. package/package.json +73 -24
  33. package/src/custom-element.ts +206 -0
  34. package/{esm5/index.d.ts → src/index.ts} +7 -5
  35. package/src/listen.ts +70 -0
  36. package/src/prop.ts +92 -0
  37. package/src/state.ts +129 -0
  38. package/src/template-runtime.ts +435 -0
  39. package/src/toggle.ts +66 -0
  40. package/src/tsconfig.json +24 -0
  41. package/src/util.ts +33 -0
  42. package/src/watch.ts +14 -0
  43. package/tests/basic.spec.ts +70 -0
  44. package/tests/custom-element.spec.ts +77 -0
  45. package/tests/dispatch.spec.ts +52 -0
  46. package/tests/init.spec.ts +94 -0
  47. package/tests/listen.spec.ts +118 -0
  48. package/tests/prop.spec.ts +118 -0
  49. package/tests/templating-runtime.spec.ts +575 -0
  50. package/tests/toggle.spec.ts +92 -0
  51. package/tests/watch.spec.ts +183 -0
  52. package/tools/build.js +119 -0
  53. package/tools/bundle.js +167 -0
  54. package/tools/rollup-config.js +70 -0
  55. package/tools/start.js +188 -0
  56. package/tsconfig.json +38 -0
  57. package/vite.config.mts +30 -0
  58. package/bundles/custom-elements-ts.umd.js +0 -359
  59. package/bundles/custom-elements-ts.umd.js.map +0 -1
  60. package/esm2015/custom-elements-ts.js +0 -283
  61. package/esm2015/custom-elements-ts.js.map +0 -1
  62. package/esm5/custom-element.d.ts +0 -12
  63. package/esm5/custom-elements-ts.js +0 -344
  64. package/esm5/custom-elements-ts.js.map +0 -1
  65. package/esm5/listen.d.ts +0 -17
  66. package/esm5/prop.d.ts +0 -2
  67. package/esm5/toggle.d.ts +0 -1
  68. package/esm5/util.d.ts +0 -4
  69. package/esm5/watch.d.ts +0 -1
package/package.json CHANGED
@@ -1,24 +1,73 @@
1
- {
2
- "name": "custom-elements-ts",
3
- "version": "0.0.17",
4
- "description": "Create native Custom Elements using Typescript without using any third party libraries",
5
- "main": "./bundles/custom-elements-ts.umd.js",
6
- "module": "./esm2015/custom-elements-ts.js",
7
- "esm5": "./esm5/custom-elements-ts.js",
8
- "esm2015": "./esm2015/custom-elements-ts.js",
9
- "typings": "index.d.ts",
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/geocine/custom-elements-ts.git"
13
- },
14
- "keywords": [
15
- "components",
16
- "custom",
17
- "dom",
18
- "elements",
19
- "web",
20
- "typescript"
21
- ],
22
- "license": "MIT",
23
- "homepage": "https://geocine.github.io/custom-elements-ts/"
24
- }
1
+ {
2
+ "name": "custom-elements-ts",
3
+ "version": "0.1.0",
4
+ "description": "Create native Custom Elements using Typescript without using any third party libraries",
5
+ "main": "index.js",
6
+ "module": "custom-element.js",
7
+ "scripts": {
8
+ "build": "node tools/build.js",
9
+ "start": "node tools/start.js",
10
+ "bundle": "node tools/bundle.js",
11
+ "publish:dist": "npm run bundle && npm publish dist --access public",
12
+ "publish:dry-run": "npm run bundle && npm publish dist --dry-run",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage --coverage.reporter=lcov",
16
+ "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
17
+ "lint": "eslint \"{src,tests}/**/*.ts\"",
18
+ "lint:fix": "eslint \"{src,tests}/**/*.ts\" --fix"
19
+ },
20
+ "keywords": [
21
+ "components",
22
+ "custom",
23
+ "dom",
24
+ "elements",
25
+ "web",
26
+ "typescript"
27
+ ],
28
+ "license": "MIT",
29
+ "devDependencies": {
30
+ "@rollup/plugin-node-resolve": "^15.2.3",
31
+ "@rollup/plugin-terser": "^0.4.4",
32
+ "@typescript-eslint/eslint-plugin": "^8.5.0",
33
+ "@typescript-eslint/parser": "^8.5.0",
34
+ "@vitest/coverage-v8": "2.1.9",
35
+ "@webcomponents/custom-elements": "1.2.2",
36
+ "coveralls": "^3.0.2",
37
+ "eslint": "^8.57.0",
38
+ "eslint-config-prettier": "^9.0.0",
39
+ "eslint-plugin-prettier": "^5.0.0",
40
+ "event-stream": "3.3.4",
41
+ "express": "^4.17.1",
42
+ "glob": "^7.1.6",
43
+ "happy-dom": "^14.12.3",
44
+ "magic-string": "0.25.3",
45
+ "prettier": "^3.0.0",
46
+ "rimraf": "^3.0.2",
47
+ "rollup": "^4.18.0",
48
+ "rollup-plugin-typescript2": "^0.36.0",
49
+ "typescript": "^5.5.4",
50
+ "vitest": "^2.0.5"
51
+ },
52
+ "typings": "index.d.ts",
53
+ "contributors": [
54
+ "Aivan Monceller",
55
+ "Arjay Elbore",
56
+ "Martin Llaneza"
57
+ ],
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/geocine/custom-elements-ts.git"
61
+ },
62
+ "homepage": "https://geocine.github.io/custom-elements-ts/",
63
+ "overrides": {
64
+ "handlebars": "4.7.8",
65
+ "tar": "6.2.1",
66
+ "minimist": "1.2.8",
67
+ "follow-redirects": "1.15.9",
68
+ "elliptic": "6.6.1",
69
+ "axios": "1.8.4",
70
+ "lodash": "4.17.21",
71
+ "set-value": "4.1.0"
72
+ }
73
+ }
@@ -0,0 +1,206 @@
1
+ import { addEventListeners, ListenerMetadata } from './listen';
2
+ import { initializeProps } from './prop';
3
+ import { isStateAttributeName } from './state';
4
+ import { renderIntoAnchor, RenderState, TemplateValue } from './template-runtime';
5
+ import { toKebabCase, toCamelCase } from './util';
6
+
7
+ export interface CustomElementMetadata {
8
+ tag?: string;
9
+ template?: string;
10
+ templateUrl?: string;
11
+ styleUrl?: string;
12
+ style?: string;
13
+ shadow?: boolean;
14
+ }
15
+
16
+ export interface KeyValue {
17
+ [key: string]: any;
18
+ }
19
+
20
+ export const CustomElement = (args: CustomElementMetadata) => {
21
+ return (target: any) => {
22
+ const tag: string = args.tag || toKebabCase(target.prototype.constructor.name);
23
+ const customElement: any = class extends (target as { new (): any }) {
24
+ private __connected: boolean = false;
25
+ private __hasMountedStatic: boolean = false;
26
+ private __renderAnchor?: Comment;
27
+ private __renderQueued: boolean = false;
28
+ private __renderState: RenderState = {};
29
+ private __suppressRender: boolean = false;
30
+
31
+ props: KeyValue = {};
32
+ __stateValues!: KeyValue;
33
+ __stateProxyCaches!: KeyValue;
34
+ protected static propsInit: KeyValue;
35
+ protected static stateInit: KeyValue;
36
+
37
+ protected static watchAttributes: KeyValue;
38
+ protected static listeners: ListenerMetadata[];
39
+
40
+ showShadowRoot: boolean;
41
+
42
+ static get observedAttributes() {
43
+ const stateInit = this.stateInit || {};
44
+ return Object.keys(this.propsInit || {})
45
+ .filter((x) => stateInit[x] === undefined)
46
+ .map((x) => toKebabCase(x));
47
+ }
48
+
49
+ constructor() {
50
+ super();
51
+ this.__stateValues ||= {};
52
+ this.__stateProxyCaches ||= {};
53
+ this.showShadowRoot =
54
+ args.shadow === undefined || args.shadow === null ? true : args.shadow;
55
+ if (!this.shadowRoot && this.showShadowRoot) {
56
+ this.attachShadow({ mode: 'open' });
57
+ }
58
+ }
59
+
60
+ attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
61
+ this.onAttributeChange(name, oldValue, newValue);
62
+ }
63
+
64
+ onAttributeChange(name: string, oldValue: string, newValue: string, set: boolean = true) {
65
+ if (oldValue !== newValue) {
66
+ if (set && !isStateAttributeName(this, name)) {
67
+ const propName = toCamelCase(name);
68
+ (this as any)[propName] = newValue;
69
+ }
70
+ const watchAttributes: KeyValue = (this.constructor as any).watchAttributes;
71
+ if (watchAttributes && watchAttributes[name]) {
72
+ const methodToCall: string = watchAttributes[name];
73
+ if (this.__connected) {
74
+ if (typeof (this as any)[methodToCall] === 'function') {
75
+ (this as any)[methodToCall]({ old: oldValue, new: newValue });
76
+ }
77
+ }
78
+ }
79
+ this.__scheduleRender();
80
+ }
81
+ }
82
+
83
+ connectedCallback() {
84
+ if (this.__connected) return;
85
+
86
+ this.__connected = true;
87
+ if (!this.__hasRender()) {
88
+ this.__renderStaticTemplate();
89
+ }
90
+
91
+ this.__suppressRender = true;
92
+ initializeProps(this);
93
+ this.__suppressRender = false;
94
+
95
+ if (this.__hasRender()) {
96
+ this.__mountStaticStyle();
97
+ this.__performRender();
98
+ }
99
+
100
+ const parentProto =
101
+ (Object.getPrototypeOf(Object.getPrototypeOf(this)) as {
102
+ connectedCallback?: () => void;
103
+ }) || null;
104
+ if (parentProto && typeof parentProto.connectedCallback === 'function') {
105
+ parentProto.connectedCallback.call(this);
106
+ }
107
+
108
+ addEventListeners(this);
109
+ }
110
+
111
+ disconnectedCallback() {
112
+ if (!this.__connected) return;
113
+ this.__connected = false;
114
+ this.__renderQueued = false;
115
+ this.__renderState.instance?.dispose();
116
+ this.__renderState.part?.dispose();
117
+ this.__renderState = {};
118
+
119
+ const parentProto =
120
+ (Object.getPrototypeOf(Object.getPrototypeOf(this)) as {
121
+ disconnectedCallback?: () => void;
122
+ }) || null;
123
+ if (parentProto && typeof parentProto.disconnectedCallback === 'function') {
124
+ parentProto.disconnectedCallback.call(this);
125
+ }
126
+ }
127
+
128
+ __renderStaticTemplate() {
129
+ if (this.__hasMountedStatic) return;
130
+ const template = document.createElement('template');
131
+ const style = `${args.style ? `<style>${args.style}</style>` : ''}`;
132
+ template.innerHTML = `${style}${args.template ? args.template : ''}`;
133
+ (this.showShadowRoot ? this.shadowRoot : this).appendChild(
134
+ document.importNode(template.content, true)
135
+ );
136
+ this.__hasMountedStatic = true;
137
+ }
138
+
139
+ __mountStaticStyle() {
140
+ if (this.__hasMountedStatic) return;
141
+ if (args.style) {
142
+ const style = document.createElement('style');
143
+ style.textContent = args.style;
144
+ (this.showShadowRoot ? this.shadowRoot : this).appendChild(style);
145
+ }
146
+ this.__renderAnchor = document.createComment('custom-elements-ts-render');
147
+ (this.showShadowRoot ? this.shadowRoot : this).appendChild(this.__renderAnchor);
148
+ this.__hasMountedStatic = true;
149
+ }
150
+
151
+ __notifyPropertyChange(propName: string, oldValue: any, newValue: any) {
152
+ const watchAttributes: KeyValue = (this.constructor as any).watchAttributes;
153
+ const watchName = toKebabCase(propName);
154
+ const methodToCall: string | undefined = watchAttributes?.[watchName];
155
+ if (methodToCall && this.__connected && typeof (this as any)[methodToCall] === 'function') {
156
+ (this as any)[methodToCall]({ old: oldValue, new: newValue });
157
+ }
158
+ this.__scheduleRender();
159
+ }
160
+
161
+ __scheduleRender() {
162
+ if (!this.__connected || this.__suppressRender || !this.__hasRender()) {
163
+ return;
164
+ }
165
+ if (this.__renderQueued) {
166
+ return;
167
+ }
168
+ this.__renderQueued = true;
169
+ queueMicrotask(() => {
170
+ if (!this.__connected || !this.__renderQueued) {
171
+ return;
172
+ }
173
+ this.__performRender();
174
+ });
175
+ }
176
+
177
+ __performRender() {
178
+ if (!this.__hasRender()) {
179
+ return;
180
+ }
181
+ this.__mountStaticStyle();
182
+ this.__renderQueued = false;
183
+ try {
184
+ const output = (this as any).render() as TemplateValue;
185
+ this.__renderState = renderIntoAnchor(
186
+ output,
187
+ this.__renderAnchor!,
188
+ this.__renderState,
189
+ this
190
+ );
191
+ } finally {
192
+ this.__renderQueued = false;
193
+ }
194
+ }
195
+
196
+ __hasRender() {
197
+ return typeof (this as any).render === 'function';
198
+ }
199
+ };
200
+
201
+ if (!customElements.get(tag)) {
202
+ customElements.define(tag, customElement);
203
+ }
204
+ return customElement;
205
+ };
206
+ };
@@ -1,5 +1,7 @@
1
- export * from './custom-element';
2
- export * from './watch';
3
- export * from './prop';
4
- export * from './toggle';
5
- export * from './listen';
1
+ export * from './custom-element';
2
+ export * from './watch';
3
+ export * from './prop';
4
+ export * from './toggle';
5
+ export * from './listen';
6
+ export { State } from './state';
7
+ export { html, TemplateResult, TemplateValue } from './template-runtime';
package/src/listen.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { toDotCase } from './util';
2
+ interface ListenerMetadata {
3
+ eventName: string;
4
+ handler: (e: Event) => void;
5
+ selector?: string;
6
+ }
7
+
8
+ interface CustomEventOptions {
9
+ bubbles?: boolean;
10
+ composed?: boolean;
11
+ detail?: any;
12
+ }
13
+
14
+ interface DispatchEmitter {
15
+ emit(options?: CustomEventOptions): void;
16
+ }
17
+
18
+ const Listen = (eventName: string, selector?: string) => {
19
+ return (target: any, methodName: string) => {
20
+ if (!target.constructor.listeners) {
21
+ target.constructor.listeners = [];
22
+ }
23
+ target.constructor.listeners.push({
24
+ selector: selector,
25
+ eventName: eventName,
26
+ handler: target[methodName],
27
+ });
28
+ };
29
+ };
30
+
31
+ const addEventListeners = (target: any) => {
32
+ if (target.constructor.listeners) {
33
+ const targetRoot: any = target.shadowRoot || target;
34
+ for (const listener of target.constructor.listeners as Array<ListenerMetadata>) {
35
+ let eventTarget: Element | EventTarget | null = target;
36
+ if (listener.selector) {
37
+ eventTarget = targetRoot.querySelector(listener.selector);
38
+ }
39
+ if (eventTarget) {
40
+ eventTarget.addEventListener(listener.eventName, (e: Event) => {
41
+ listener.handler.call(target, e);
42
+ });
43
+ }
44
+ }
45
+ }
46
+ };
47
+
48
+ const Dispatch = (eventName?: string) => {
49
+ return (target: HTMLElement, propertyName: string) => {
50
+ function get(this: EventTarget) {
51
+ const self: EventTarget = this as EventTarget;
52
+ return {
53
+ emit(options?: CustomEventOptions) {
54
+ const evtName = eventName ? eventName : toDotCase(propertyName);
55
+ self.dispatchEvent(new CustomEvent(evtName, options));
56
+ },
57
+ };
58
+ }
59
+ Object.defineProperty(target, propertyName, { get });
60
+ };
61
+ };
62
+
63
+ export {
64
+ Listen,
65
+ addEventListeners,
66
+ DispatchEmitter,
67
+ Dispatch,
68
+ CustomEventOptions,
69
+ ListenerMetadata,
70
+ };
package/src/prop.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { toKebabCase, tryParseInt } from './util';
2
+
3
+ export const Prop = (): any => {
4
+ return (target: any, propName: any) => {
5
+ const attrName = toKebabCase(propName);
6
+ function get(this: any) {
7
+ const hasOwn = Object.prototype.hasOwnProperty.call(this.props, propName);
8
+ if (hasOwn) {
9
+ return this.props[propName];
10
+ }
11
+ return this.getAttribute(attrName);
12
+ }
13
+ function set(this: any, value: any) {
14
+ if (this.__connected) {
15
+ const oldValue = this.props[propName];
16
+ this.props[propName] = coercePropValue(
17
+ value,
18
+ oldValue,
19
+ this.constructor.propsInit[propName]
20
+ );
21
+ const changed = !Object.is(oldValue, this.props[propName]);
22
+ const valueType = typeof value;
23
+ const shouldReflect =
24
+ valueType === 'string' || valueType === 'number' || valueType === 'boolean';
25
+ if (shouldReflect) {
26
+ this.setAttribute(attrName, value);
27
+ } else {
28
+ this.onAttributeChange(attrName, oldValue, value, false);
29
+ }
30
+ if (changed) {
31
+ this.__scheduleRender?.();
32
+ }
33
+ } else {
34
+ if (!this.hasAttribute(toKebabCase(propName))) {
35
+ this.constructor.propsInit[propName] = value;
36
+ }
37
+ }
38
+ }
39
+ if (!target.constructor.propsInit) {
40
+ target.constructor.propsInit = {};
41
+ }
42
+ target.constructor.propsInit[propName] = null;
43
+ Object.defineProperty(target, propName, { get, set });
44
+ };
45
+ };
46
+
47
+ const getProps = (target: any) => {
48
+ const watchAttributes = target.constructor.watchAttributes;
49
+ const plainAttributes = { ...watchAttributes };
50
+ Object.keys(plainAttributes).forEach((v) => (plainAttributes[v] = ''));
51
+ const cycleProps = { ...plainAttributes, ...target.constructor.propsInit };
52
+ const stateInit = target.constructor.stateInit || {};
53
+ return Object.keys(cycleProps).filter((prop) => stateInit[prop] === undefined);
54
+ };
55
+
56
+ export const initializeProps = (target: any) => {
57
+ const watchAttributes = target.constructor.watchAttributes;
58
+ for (const prop of getProps(target)) {
59
+ const attrName = toKebabCase(prop);
60
+ if (watchAttributes) {
61
+ const methodName = watchAttributes[attrName];
62
+ if (methodName === null || methodName === undefined) {
63
+ watchAttributes[attrName] = '';
64
+ } else {
65
+ const hasOwn = Object.prototype.hasOwnProperty.call(target.props, prop);
66
+ const attribValue = hasOwn ? target.props[prop] : target.getAttribute(attrName);
67
+ if (typeof target[methodName] === 'function') {
68
+ target[methodName]({ new: attribValue });
69
+ }
70
+ }
71
+ }
72
+ const defaultValue = target.constructor.propsInit[prop];
73
+ if (defaultValue !== null && defaultValue !== undefined) {
74
+ if (!target.hasAttribute(attrName)) {
75
+ target[prop] = defaultValue;
76
+ }
77
+ }
78
+ }
79
+ };
80
+
81
+ const coercePropValue = (value: any, oldValue: any, defaultValue: any): any => {
82
+ const expectedType = typeof (oldValue === undefined ? defaultValue : oldValue);
83
+ if (expectedType === 'boolean' && typeof value === 'string') {
84
+ if (value === '' || value === 'true') {
85
+ return true;
86
+ }
87
+ if (value === 'false') {
88
+ return false;
89
+ }
90
+ }
91
+ return tryParseInt(value);
92
+ };
package/src/state.ts ADDED
@@ -0,0 +1,129 @@
1
+ import { toKebabCase } from './util';
2
+
3
+ export interface StateMetadata {
4
+ [key: string]: null;
5
+ }
6
+
7
+ const proxyToRaw = new WeakMap<object, object>();
8
+
9
+ export const State = (): any => {
10
+ return (target: any, propName: string) => {
11
+ if (!target.constructor.stateInit) {
12
+ target.constructor.stateInit = {};
13
+ }
14
+ target.constructor.stateInit[propName] = null;
15
+
16
+ function get(this: any) {
17
+ return this.__stateValues ? this.__stateValues[propName] : undefined;
18
+ }
19
+
20
+ function set(this: any, value: any) {
21
+ if (!this.__stateValues) {
22
+ this.__stateValues = {};
23
+ }
24
+ if (!this.__stateProxyCaches) {
25
+ this.__stateProxyCaches = {};
26
+ }
27
+
28
+ const oldValue = this.__stateValues[propName];
29
+ const proxyCache = (this.__stateProxyCaches[propName] ||= new WeakMap<object, unknown>());
30
+ const newValue = createStateProxy(value, proxyCache, () => {
31
+ this.__notifyPropertyChange?.(
32
+ propName,
33
+ this.__stateValues[propName],
34
+ this.__stateValues[propName]
35
+ );
36
+ });
37
+
38
+ if (Object.is(oldValue, newValue)) {
39
+ return;
40
+ }
41
+
42
+ this.__stateValues[propName] = newValue;
43
+ if (this.__connected) {
44
+ this.__notifyPropertyChange?.(propName, oldValue, newValue);
45
+ }
46
+ }
47
+
48
+ Object.defineProperty(target, propName, { get, set });
49
+ };
50
+ };
51
+
52
+ const createStateProxy = (
53
+ value: unknown,
54
+ cache: WeakMap<object, unknown>,
55
+ notify: () => void
56
+ ): unknown => {
57
+ if (!isProxyable(value)) {
58
+ return value;
59
+ }
60
+
61
+ if (proxyToRaw.has(value)) {
62
+ return value;
63
+ }
64
+
65
+ const cached = cache.get(value);
66
+ if (cached) {
67
+ return cached;
68
+ }
69
+
70
+ const proxy = new Proxy(value, {
71
+ get(target, property, receiver) {
72
+ const result = Reflect.get(target, property, receiver);
73
+ return createStateProxy(result, cache, notify);
74
+ },
75
+ set(target, property, nextValue, receiver) {
76
+ const previous = Reflect.get(target, property, receiver);
77
+ const proxiedValue = createStateProxy(nextValue, cache, notify);
78
+ const changed = !Object.is(previous, proxiedValue);
79
+ const didSet = Reflect.set(target, property, proxiedValue, receiver);
80
+ if (didSet && changed) {
81
+ notify();
82
+ }
83
+ return didSet;
84
+ },
85
+ deleteProperty(target, property) {
86
+ const hadProperty = Object.prototype.hasOwnProperty.call(target, property);
87
+ const deleted = Reflect.deleteProperty(target, property);
88
+ if (deleted && hadProperty) {
89
+ notify();
90
+ }
91
+ return deleted;
92
+ },
93
+ });
94
+
95
+ proxyToRaw.set(proxy, value);
96
+ cache.set(value, proxy);
97
+ return proxy;
98
+ };
99
+
100
+ const isProxyable = (value: unknown): value is object => {
101
+ if (!value || typeof value !== 'object') {
102
+ return false;
103
+ }
104
+ if (typeof Node !== 'undefined' && value instanceof Node) {
105
+ return false;
106
+ }
107
+ if (
108
+ value instanceof Date ||
109
+ value instanceof Map ||
110
+ value instanceof Set ||
111
+ value instanceof WeakMap ||
112
+ value instanceof WeakSet
113
+ ) {
114
+ return false;
115
+ }
116
+ return Array.isArray(value) || Object.getPrototypeOf(value) === Object.prototype;
117
+ };
118
+
119
+ export const isStateName = (target: any, propName: string): boolean => {
120
+ return Boolean(target.constructor.stateInit?.[propName] !== undefined);
121
+ };
122
+
123
+ export const isStateAttributeName = (target: any, attrName: string): boolean => {
124
+ const stateInit: StateMetadata | undefined = target.constructor.stateInit;
125
+ if (!stateInit) {
126
+ return false;
127
+ }
128
+ return Object.keys(stateInit).some((propName) => toKebabCase(propName) === attrName);
129
+ };