create-microact-app 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +95 -0
- package/package.json +21 -0
- package/templates/vanilla/.github/workflows/deploy.yml +38 -0
- package/templates/vanilla/index.html +13 -0
- package/templates/vanilla/node_modules/.package-lock.json +207 -0
- package/templates/vanilla/node_modules/@esbuild/darwin-x64/README.md +3 -0
- package/templates/vanilla/node_modules/@esbuild/darwin-x64/bin/esbuild +0 -0
- package/templates/vanilla/node_modules/@esbuild/darwin-x64/package.json +17 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/README.md +154 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.cjs.js +1749 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.cjs.js.map +1 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.esm.js +1743 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.esm.js.map +1 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.umd.js +2 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/dist/microact.umd.js.map +1 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/example/index.html +13 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/example/index.js +63 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/package.json +38 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/rollup.config.cjs +30 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/src/Component.js +831 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/src/DOMUpdater.js +320 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/src/EventBus.js +123 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/src/Router.js +253 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/src/UpdateScheduler.js +218 -0
- package/templates/vanilla/node_modules/@monygroupcorp/microact/src/index.js +6 -0
- package/templates/vanilla/node_modules/esbuild/LICENSE.md +21 -0
- package/templates/vanilla/node_modules/esbuild/README.md +3 -0
- package/templates/vanilla/node_modules/esbuild/bin/esbuild +0 -0
- package/templates/vanilla/node_modules/esbuild/install.js +287 -0
- package/templates/vanilla/node_modules/esbuild/lib/main.d.ts +660 -0
- package/templates/vanilla/node_modules/esbuild/lib/main.js +2393 -0
- package/templates/vanilla/node_modules/esbuild/package.json +42 -0
- package/templates/vanilla/node_modules/nanoid/LICENSE +20 -0
- package/templates/vanilla/node_modules/nanoid/README.md +39 -0
- package/templates/vanilla/node_modules/nanoid/async/index.browser.cjs +69 -0
- package/templates/vanilla/node_modules/nanoid/async/index.browser.js +34 -0
- package/templates/vanilla/node_modules/nanoid/async/index.cjs +71 -0
- package/templates/vanilla/node_modules/nanoid/async/index.d.ts +56 -0
- package/templates/vanilla/node_modules/nanoid/async/index.js +35 -0
- package/templates/vanilla/node_modules/nanoid/async/index.native.js +26 -0
- package/templates/vanilla/node_modules/nanoid/async/package.json +12 -0
- package/templates/vanilla/node_modules/nanoid/bin/nanoid.cjs +55 -0
- package/templates/vanilla/node_modules/nanoid/index.browser.cjs +72 -0
- package/templates/vanilla/node_modules/nanoid/index.browser.js +34 -0
- package/templates/vanilla/node_modules/nanoid/index.cjs +85 -0
- package/templates/vanilla/node_modules/nanoid/index.d.cts +91 -0
- package/templates/vanilla/node_modules/nanoid/index.d.ts +91 -0
- package/templates/vanilla/node_modules/nanoid/index.js +45 -0
- package/templates/vanilla/node_modules/nanoid/nanoid.js +1 -0
- package/templates/vanilla/node_modules/nanoid/non-secure/index.cjs +34 -0
- package/templates/vanilla/node_modules/nanoid/non-secure/index.d.ts +33 -0
- package/templates/vanilla/node_modules/nanoid/non-secure/index.js +21 -0
- package/templates/vanilla/node_modules/nanoid/non-secure/package.json +6 -0
- package/templates/vanilla/node_modules/nanoid/package.json +89 -0
- package/templates/vanilla/node_modules/nanoid/url-alphabet/index.cjs +7 -0
- package/templates/vanilla/node_modules/nanoid/url-alphabet/index.js +3 -0
- package/templates/vanilla/node_modules/nanoid/url-alphabet/package.json +6 -0
- package/templates/vanilla/node_modules/picocolors/LICENSE +15 -0
- package/templates/vanilla/node_modules/picocolors/README.md +21 -0
- package/templates/vanilla/node_modules/picocolors/package.json +25 -0
- package/templates/vanilla/node_modules/picocolors/picocolors.browser.js +4 -0
- package/templates/vanilla/node_modules/picocolors/picocolors.d.ts +5 -0
- package/templates/vanilla/node_modules/picocolors/picocolors.js +75 -0
- package/templates/vanilla/node_modules/picocolors/types.d.ts +51 -0
- package/templates/vanilla/node_modules/postcss/LICENSE +20 -0
- package/templates/vanilla/node_modules/postcss/README.md +29 -0
- package/templates/vanilla/node_modules/postcss/lib/at-rule.d.ts +140 -0
- package/templates/vanilla/node_modules/postcss/lib/at-rule.js +25 -0
- package/templates/vanilla/node_modules/postcss/lib/comment.d.ts +68 -0
- package/templates/vanilla/node_modules/postcss/lib/comment.js +13 -0
- package/templates/vanilla/node_modules/postcss/lib/container.d.ts +483 -0
- package/templates/vanilla/node_modules/postcss/lib/container.js +447 -0
- package/templates/vanilla/node_modules/postcss/lib/css-syntax-error.d.ts +248 -0
- package/templates/vanilla/node_modules/postcss/lib/css-syntax-error.js +133 -0
- package/templates/vanilla/node_modules/postcss/lib/declaration.d.ts +151 -0
- package/templates/vanilla/node_modules/postcss/lib/declaration.js +24 -0
- package/templates/vanilla/node_modules/postcss/lib/document.d.ts +69 -0
- package/templates/vanilla/node_modules/postcss/lib/document.js +33 -0
- package/templates/vanilla/node_modules/postcss/lib/fromJSON.d.ts +9 -0
- package/templates/vanilla/node_modules/postcss/lib/fromJSON.js +54 -0
- package/templates/vanilla/node_modules/postcss/lib/input.d.ts +227 -0
- package/templates/vanilla/node_modules/postcss/lib/input.js +265 -0
- package/templates/vanilla/node_modules/postcss/lib/lazy-result.d.ts +190 -0
- package/templates/vanilla/node_modules/postcss/lib/lazy-result.js +550 -0
- package/templates/vanilla/node_modules/postcss/lib/list.d.ts +60 -0
- package/templates/vanilla/node_modules/postcss/lib/list.js +58 -0
- package/templates/vanilla/node_modules/postcss/lib/map-generator.js +368 -0
- package/templates/vanilla/node_modules/postcss/lib/no-work-result.d.ts +46 -0
- package/templates/vanilla/node_modules/postcss/lib/no-work-result.js +138 -0
- package/templates/vanilla/node_modules/postcss/lib/node.d.ts +556 -0
- package/templates/vanilla/node_modules/postcss/lib/node.js +449 -0
- package/templates/vanilla/node_modules/postcss/lib/parse.d.ts +9 -0
- package/templates/vanilla/node_modules/postcss/lib/parse.js +42 -0
- package/templates/vanilla/node_modules/postcss/lib/parser.js +611 -0
- package/templates/vanilla/node_modules/postcss/lib/postcss.d.mts +69 -0
- package/templates/vanilla/node_modules/postcss/lib/postcss.d.ts +458 -0
- package/templates/vanilla/node_modules/postcss/lib/postcss.js +101 -0
- package/templates/vanilla/node_modules/postcss/lib/postcss.mjs +30 -0
- package/templates/vanilla/node_modules/postcss/lib/previous-map.d.ts +81 -0
- package/templates/vanilla/node_modules/postcss/lib/previous-map.js +144 -0
- package/templates/vanilla/node_modules/postcss/lib/processor.d.ts +115 -0
- package/templates/vanilla/node_modules/postcss/lib/processor.js +67 -0
- package/templates/vanilla/node_modules/postcss/lib/result.d.ts +205 -0
- package/templates/vanilla/node_modules/postcss/lib/result.js +42 -0
- package/templates/vanilla/node_modules/postcss/lib/root.d.ts +87 -0
- package/templates/vanilla/node_modules/postcss/lib/root.js +61 -0
- package/templates/vanilla/node_modules/postcss/lib/rule.d.ts +126 -0
- package/templates/vanilla/node_modules/postcss/lib/rule.js +27 -0
- package/templates/vanilla/node_modules/postcss/lib/stringifier.d.ts +46 -0
- package/templates/vanilla/node_modules/postcss/lib/stringifier.js +353 -0
- package/templates/vanilla/node_modules/postcss/lib/stringify.d.ts +9 -0
- package/templates/vanilla/node_modules/postcss/lib/stringify.js +11 -0
- package/templates/vanilla/node_modules/postcss/lib/symbols.js +5 -0
- package/templates/vanilla/node_modules/postcss/lib/terminal-highlight.js +70 -0
- package/templates/vanilla/node_modules/postcss/lib/tokenize.js +266 -0
- package/templates/vanilla/node_modules/postcss/lib/warn-once.js +13 -0
- package/templates/vanilla/node_modules/postcss/lib/warning.d.ts +147 -0
- package/templates/vanilla/node_modules/postcss/lib/warning.js +37 -0
- package/templates/vanilla/node_modules/postcss/package.json +88 -0
- package/templates/vanilla/node_modules/rollup/LICENSE.md +695 -0
- package/templates/vanilla/node_modules/rollup/README.md +125 -0
- package/templates/vanilla/node_modules/rollup/dist/bin/rollup +1715 -0
- package/templates/vanilla/node_modules/rollup/dist/es/getLogFilter.js +64 -0
- package/templates/vanilla/node_modules/rollup/dist/es/package.json +1 -0
- package/templates/vanilla/node_modules/rollup/dist/es/rollup.js +17 -0
- package/templates/vanilla/node_modules/rollup/dist/es/shared/node-entry.js +27273 -0
- package/templates/vanilla/node_modules/rollup/dist/es/shared/watch.js +4857 -0
- package/templates/vanilla/node_modules/rollup/dist/getLogFilter.d.ts +5 -0
- package/templates/vanilla/node_modules/rollup/dist/getLogFilter.js +69 -0
- package/templates/vanilla/node_modules/rollup/dist/loadConfigFile.d.ts +20 -0
- package/templates/vanilla/node_modules/rollup/dist/loadConfigFile.js +29 -0
- package/templates/vanilla/node_modules/rollup/dist/rollup.d.ts +1012 -0
- package/templates/vanilla/node_modules/rollup/dist/rollup.js +31 -0
- package/templates/vanilla/node_modules/rollup/dist/shared/fsevents-importer.js +37 -0
- package/templates/vanilla/node_modules/rollup/dist/shared/index.js +4571 -0
- package/templates/vanilla/node_modules/rollup/dist/shared/loadConfigFile.js +546 -0
- package/templates/vanilla/node_modules/rollup/dist/shared/rollup.js +27351 -0
- package/templates/vanilla/node_modules/rollup/dist/shared/watch-cli.js +561 -0
- package/templates/vanilla/node_modules/rollup/dist/shared/watch-proxy.js +87 -0
- package/templates/vanilla/node_modules/rollup/dist/shared/watch.js +316 -0
- package/templates/vanilla/node_modules/rollup/package.json +181 -0
- package/templates/vanilla/node_modules/source-map-js/LICENSE +28 -0
- package/templates/vanilla/node_modules/source-map-js/README.md +765 -0
- package/templates/vanilla/node_modules/source-map-js/lib/array-set.js +121 -0
- package/templates/vanilla/node_modules/source-map-js/lib/base64-vlq.js +140 -0
- package/templates/vanilla/node_modules/source-map-js/lib/base64.js +67 -0
- package/templates/vanilla/node_modules/source-map-js/lib/binary-search.js +111 -0
- package/templates/vanilla/node_modules/source-map-js/lib/mapping-list.js +79 -0
- package/templates/vanilla/node_modules/source-map-js/lib/quick-sort.js +132 -0
- package/templates/vanilla/node_modules/source-map-js/lib/source-map-consumer.d.ts +1 -0
- package/templates/vanilla/node_modules/source-map-js/lib/source-map-consumer.js +1188 -0
- package/templates/vanilla/node_modules/source-map-js/lib/source-map-generator.d.ts +1 -0
- package/templates/vanilla/node_modules/source-map-js/lib/source-map-generator.js +444 -0
- package/templates/vanilla/node_modules/source-map-js/lib/source-node.d.ts +1 -0
- package/templates/vanilla/node_modules/source-map-js/lib/source-node.js +413 -0
- package/templates/vanilla/node_modules/source-map-js/lib/util.js +594 -0
- package/templates/vanilla/node_modules/source-map-js/package.json +71 -0
- package/templates/vanilla/node_modules/source-map-js/source-map.d.ts +104 -0
- package/templates/vanilla/node_modules/source-map-js/source-map.js +8 -0
- package/templates/vanilla/node_modules/vite/LICENSE.md +3396 -0
- package/templates/vanilla/node_modules/vite/README.md +20 -0
- package/templates/vanilla/node_modules/vite/bin/openChrome.applescript +95 -0
- package/templates/vanilla/node_modules/vite/bin/vite.js +61 -0
- package/templates/vanilla/node_modules/vite/client.d.ts +281 -0
- package/templates/vanilla/node_modules/vite/dist/client/client.mjs +725 -0
- package/templates/vanilla/node_modules/vite/dist/client/client.mjs.map +1 -0
- package/templates/vanilla/node_modules/vite/dist/client/env.mjs +30 -0
- package/templates/vanilla/node_modules/vite/dist/client/env.mjs.map +1 -0
- package/templates/vanilla/node_modules/vite/dist/node/chunks/dep-7ec6f216.js +914 -0
- package/templates/vanilla/node_modules/vite/dist/node/chunks/dep-827b23df.js +66713 -0
- package/templates/vanilla/node_modules/vite/dist/node/chunks/dep-c423598f.js +561 -0
- package/templates/vanilla/node_modules/vite/dist/node/chunks/dep-f0c7dae0.js +7930 -0
- package/templates/vanilla/node_modules/vite/dist/node/chunks/dep-f1e8587f.js +7646 -0
- package/templates/vanilla/node_modules/vite/dist/node/cli.js +929 -0
- package/templates/vanilla/node_modules/vite/dist/node/constants.js +130 -0
- package/templates/vanilla/node_modules/vite/dist/node/index.d.ts +3548 -0
- package/templates/vanilla/node_modules/vite/dist/node/index.js +158 -0
- package/templates/vanilla/node_modules/vite/dist/node-cjs/publicUtils.cjs +4555 -0
- package/templates/vanilla/node_modules/vite/index.cjs +34 -0
- package/templates/vanilla/node_modules/vite/package.json +173 -0
- package/templates/vanilla/node_modules/vite/types/customEvent.d.ts +35 -0
- package/templates/vanilla/node_modules/vite/types/hmrPayload.d.ts +61 -0
- package/templates/vanilla/node_modules/vite/types/hot.d.ts +32 -0
- package/templates/vanilla/node_modules/vite/types/importGlob.d.ts +97 -0
- package/templates/vanilla/node_modules/vite/types/importMeta.d.ts +26 -0
- package/templates/vanilla/node_modules/vite/types/metadata.d.ts +10 -0
- package/templates/vanilla/node_modules/vite/types/package.json +4 -0
- package/templates/vanilla/package-lock.json +589 -0
- package/templates/vanilla/package.json +17 -0
- package/templates/vanilla/src/components/App.js +60 -0
- package/templates/vanilla/src/components/Card.js +21 -0
- package/templates/vanilla/src/components/Hero.js +15 -0
- package/templates/vanilla/src/components/InteractiveDemo.js +59 -0
- package/templates/vanilla/src/main.js +9 -0
- package/templates/vanilla/src/style/main.css +172 -0
- package/templates/vanilla/vite.config.js +8 -0
- package/templates/web3/.env.example +15 -0
- package/templates/web3/.github/workflows/deploy.yml +38 -0
- package/templates/web3/README.md +33 -0
- package/templates/web3/contracts/foundry.toml +11 -0
- package/templates/web3/contracts/script/Deploy.s.sol +13 -0
- package/templates/web3/contracts/src/Counter.sol +21 -0
- package/templates/web3/index.html +13 -0
- package/templates/web3/package.json +25 -0
- package/templates/web3/scripts/chain-start.mjs +305 -0
- package/templates/web3/scripts/chain-stop.mjs +34 -0
- package/templates/web3/scripts/deploy.mjs +155 -0
- package/templates/web3/scripts/setup.mjs +42 -0
- package/templates/web3/src/components/App.js +49 -0
- package/templates/web3/src/components/CounterCard.js +111 -0
- package/templates/web3/src/main.js +54 -0
- package/templates/web3/src/style/main.css +345 -0
- package/templates/web3/vite.config.js +29 -0
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
import { eventBus } from './EventBus.js';
|
|
2
|
+
import { DOMUpdater } from './DOMUpdater.js';
|
|
3
|
+
import { getUpdateScheduler } from './UpdateScheduler.js';
|
|
4
|
+
|
|
5
|
+
export class Component {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.element = null;
|
|
8
|
+
this.state = {};
|
|
9
|
+
this.mounted = false;
|
|
10
|
+
this.boundEvents = new Map();
|
|
11
|
+
// Cleanup registry for tracking all cleanup functions
|
|
12
|
+
this._cleanupRegistry = new Set();
|
|
13
|
+
// Child components registry for automatic cleanup
|
|
14
|
+
this._children = new Map();
|
|
15
|
+
// Event subscriptions registry for automatic cleanup
|
|
16
|
+
this._subscriptions = new Set();
|
|
17
|
+
// DOM updater for granular updates
|
|
18
|
+
this._domUpdater = new DOMUpdater();
|
|
19
|
+
// Element reference cache
|
|
20
|
+
this._refs = new Map();
|
|
21
|
+
// Context storage
|
|
22
|
+
this._context = new Map();
|
|
23
|
+
// Parent component reference for context traversal
|
|
24
|
+
this._parent = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize state with default values
|
|
29
|
+
* @param {Object} initialState
|
|
30
|
+
*/
|
|
31
|
+
setState(newState) {
|
|
32
|
+
const oldState = {...this.state};
|
|
33
|
+
this.state = { ...this.state, ...newState };
|
|
34
|
+
|
|
35
|
+
// Only update if we should based on state changes
|
|
36
|
+
if (this.shouldUpdate(oldState, this.state)) {
|
|
37
|
+
this.update();
|
|
38
|
+
this.onStateUpdate(oldState, this.state);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Determines if the component should update based on state changes
|
|
44
|
+
* Override in child classes for custom comparison logic
|
|
45
|
+
* @param {Object} oldState - Previous state
|
|
46
|
+
* @param {Object} newState - New state
|
|
47
|
+
* @returns {boolean} - Whether component should update
|
|
48
|
+
*/
|
|
49
|
+
shouldUpdate(oldState, newState) {
|
|
50
|
+
// Default shallow comparison of top-level state properties
|
|
51
|
+
// Check if any properties have changed
|
|
52
|
+
if (!oldState || !newState) return true;
|
|
53
|
+
|
|
54
|
+
// Check if object references are the same
|
|
55
|
+
if (oldState === newState) return false;
|
|
56
|
+
|
|
57
|
+
// Do a shallow comparison of properties
|
|
58
|
+
const oldKeys = Object.keys(oldState);
|
|
59
|
+
const newKeys = Object.keys(newState);
|
|
60
|
+
|
|
61
|
+
// If they have different number of keys, they changed
|
|
62
|
+
if (oldKeys.length !== newKeys.length) return true;
|
|
63
|
+
|
|
64
|
+
// Check if any key's value has changed
|
|
65
|
+
return oldKeys.some(key => oldState[key] !== newState[key]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Lifecycle hook called after state is updated but before rendering
|
|
70
|
+
* Override in child classes to handle state updates
|
|
71
|
+
* @param {Object} oldState
|
|
72
|
+
* @param {Object} newState
|
|
73
|
+
*/
|
|
74
|
+
onStateUpdate(oldState, newState) {
|
|
75
|
+
// Default implementation does nothing
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Mount component to DOM
|
|
80
|
+
* @param {HTMLElement} container
|
|
81
|
+
*/
|
|
82
|
+
mount(element) {
|
|
83
|
+
try {
|
|
84
|
+
this.element = element;
|
|
85
|
+
this.mounted = true;
|
|
86
|
+
|
|
87
|
+
// Apply styles if they exist
|
|
88
|
+
if (this.constructor.styles) {
|
|
89
|
+
const styleElement = document.createElement('style');
|
|
90
|
+
styleElement.textContent = this.constructor.styles;
|
|
91
|
+
document.head.appendChild(styleElement);
|
|
92
|
+
this.styleElement = styleElement;
|
|
93
|
+
|
|
94
|
+
// Register cleanup for style element
|
|
95
|
+
this.registerCleanup(() => {
|
|
96
|
+
if (this.styleElement && this.styleElement.parentNode) {
|
|
97
|
+
this.styleElement.remove();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.update();
|
|
103
|
+
if (this.onMount) {
|
|
104
|
+
this.onMount();
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this._handleError(error, { phase: 'mount' });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove component from DOM
|
|
113
|
+
*/
|
|
114
|
+
unmount() {
|
|
115
|
+
if (!this.mounted) return;
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
// Execute all registered cleanup functions
|
|
120
|
+
this._executeCleanup();
|
|
121
|
+
|
|
122
|
+
// Unbind all events
|
|
123
|
+
this.unbindEvents();
|
|
124
|
+
|
|
125
|
+
// Clear all refs
|
|
126
|
+
this.invalidateRefs();
|
|
127
|
+
|
|
128
|
+
// Remove element if it exists
|
|
129
|
+
if (this.element) {
|
|
130
|
+
this.element.innerHTML = '';
|
|
131
|
+
}
|
|
132
|
+
this.element = null;
|
|
133
|
+
|
|
134
|
+
// Call lifecycle method
|
|
135
|
+
this.mounted = false;
|
|
136
|
+
if (this.onUnmount) {
|
|
137
|
+
this.onUnmount();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Bind DOM events based on this.events()
|
|
143
|
+
*/
|
|
144
|
+
bindEvents() {
|
|
145
|
+
// First unbind any existing events
|
|
146
|
+
this.unbindEvents();
|
|
147
|
+
|
|
148
|
+
if (!this.element || typeof this.events !== 'function') {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const events = this.events();
|
|
153
|
+
if (!events) return;
|
|
154
|
+
|
|
155
|
+
const entries = events instanceof Map ? Array.from(events.entries()) : Object.entries(events);
|
|
156
|
+
|
|
157
|
+
for (const [eventSelector, handlerReference] of entries) {
|
|
158
|
+
if (!eventSelector) continue;
|
|
159
|
+
|
|
160
|
+
const descriptor = eventSelector.trim();
|
|
161
|
+
if (!descriptor) continue;
|
|
162
|
+
|
|
163
|
+
const [eventName, ...selectorParts] = descriptor.split(/\s+/);
|
|
164
|
+
if (!eventName) {
|
|
165
|
+
console.warn(`[Component] Invalid event descriptor '${eventSelector}' in component '${this.constructor.name}'.`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const selector = selectorParts.join(' ');
|
|
169
|
+
|
|
170
|
+
const handler = this._resolveEventHandler(handlerReference, eventSelector);
|
|
171
|
+
if (!handler) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (selector) {
|
|
176
|
+
// Delegated event
|
|
177
|
+
const eventHandler = (e) => {
|
|
178
|
+
const target = e?.target;
|
|
179
|
+
if (!target) return;
|
|
180
|
+
const matchedTarget = typeof target.closest === 'function'
|
|
181
|
+
? target.closest(selector)
|
|
182
|
+
: (target.matches(selector) ? target : null);
|
|
183
|
+
if (matchedTarget && this.element.contains(matchedTarget)) {
|
|
184
|
+
handler(e);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
this.element.addEventListener(eventName, eventHandler);
|
|
188
|
+
this.boundEvents.set(eventSelector, eventHandler);
|
|
189
|
+
} else {
|
|
190
|
+
// Direct event
|
|
191
|
+
this.element.addEventListener(eventName, handler);
|
|
192
|
+
this.boundEvents.set(eventSelector, handler);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolve event handler definitions passed via this.events()
|
|
199
|
+
* Supports both handler name strings and direct function references.
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
_resolveEventHandler(handlerReference, eventSelector) {
|
|
203
|
+
let handler = handlerReference;
|
|
204
|
+
|
|
205
|
+
if (typeof handlerReference === 'string') {
|
|
206
|
+
handler = this[handlerReference];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (typeof handler !== 'function') {
|
|
210
|
+
console.warn(`[Component] Event handler '${handlerReference}' for event '${eventSelector}' is not a function on component '${this.constructor.name}'.`);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Bind handler to component instance when possible
|
|
215
|
+
if (typeof handler.bind === 'function') {
|
|
216
|
+
return handler.bind(this);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return (...args) => handler.apply(this, args);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Unbind all DOM events
|
|
224
|
+
*/
|
|
225
|
+
unbindEvents() {
|
|
226
|
+
if (!this.element) {
|
|
227
|
+
this.boundEvents.clear();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const [eventSelector, handler] of this.boundEvents.entries()) {
|
|
232
|
+
const [eventName] = eventSelector.trim().split(/\s+/);
|
|
233
|
+
if (!eventName) continue;
|
|
234
|
+
this.element.removeEventListener(eventName, handler);
|
|
235
|
+
}
|
|
236
|
+
this.boundEvents.clear();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Update component after state change
|
|
241
|
+
*/
|
|
242
|
+
update() {
|
|
243
|
+
if (!this.element) return;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// Get new content
|
|
247
|
+
const newContent = this.render();
|
|
248
|
+
|
|
249
|
+
// Always update on first render or when content changes
|
|
250
|
+
// First render is detected by checking if innerHTML is empty
|
|
251
|
+
if (!this.element.innerHTML || this.element.innerHTML !== newContent) {
|
|
252
|
+
// Try granular update first (preserves focus/scroll)
|
|
253
|
+
const granularSuccess = this._domUpdater.updateGranular(this.element, newContent);
|
|
254
|
+
|
|
255
|
+
if (!granularSuccess) {
|
|
256
|
+
// Fall back to full replacement for complex structural changes
|
|
257
|
+
// Note: This will destroy child components, so they need to be re-mounted
|
|
258
|
+
this.element.innerHTML = newContent;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Invalidate refs cache after DOM update
|
|
262
|
+
this.invalidateRefs();
|
|
263
|
+
|
|
264
|
+
// Mount child components
|
|
265
|
+
this._mountChildren();
|
|
266
|
+
|
|
267
|
+
// Bind events if events() method exists
|
|
268
|
+
if (this.events && typeof this.events === 'function') {
|
|
269
|
+
this.bindEvents();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
this._handleError(error, { phase: 'update' });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Mount declared child components.
|
|
279
|
+
* This method is called after the parent component's DOM is updated.
|
|
280
|
+
* @private
|
|
281
|
+
*/
|
|
282
|
+
_mountChildren() {
|
|
283
|
+
if (typeof this.children !== 'function') {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const childrenToMount = this.children();
|
|
287
|
+
if (!childrenToMount) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const selector in childrenToMount) {
|
|
292
|
+
const container = this.getRef(selector);
|
|
293
|
+
if (container) {
|
|
294
|
+
const child = childrenToMount[selector];
|
|
295
|
+
|
|
296
|
+
// If a different component is already in this container, unmount it.
|
|
297
|
+
if (container.component && container.component !== child) {
|
|
298
|
+
container.component.unmount();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// If the child is not already mounted in this specific container, mount it.
|
|
302
|
+
if (child.element !== container) {
|
|
303
|
+
child.mount(container);
|
|
304
|
+
container.component = child; // Associate component with element for cleanup.
|
|
305
|
+
this.registerCleanup(() => child.unmount());
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Declares child components for the component.
|
|
313
|
+
* @returns {Object.<string, Component>} A map where keys are CSS selectors
|
|
314
|
+
* for the container elements and values are the child component instances.
|
|
315
|
+
* e.g., { '#child-container': this.childComponent }
|
|
316
|
+
*/
|
|
317
|
+
children() {
|
|
318
|
+
return {};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Schedule a component update with optional priority
|
|
323
|
+
*
|
|
324
|
+
* This method allows components to opt-in to requestAnimationFrame batching
|
|
325
|
+
* for better performance when multiple components update simultaneously.
|
|
326
|
+
*
|
|
327
|
+
* @param {Object} options - Update options
|
|
328
|
+
* @param {boolean} options.immediate - If true, update immediately (bypass batching).
|
|
329
|
+
* Use for critical updates (user input, errors).
|
|
330
|
+
* Default: false (batched)
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* // Batched update (default) - good for price updates, balance updates
|
|
334
|
+
* this.scheduleUpdate();
|
|
335
|
+
*
|
|
336
|
+
* // Immediate update - good for user input, error displays
|
|
337
|
+
* this.scheduleUpdate({ immediate: true });
|
|
338
|
+
*/
|
|
339
|
+
scheduleUpdate(options = {}) {
|
|
340
|
+
if (options.immediate) {
|
|
341
|
+
// Critical update - execute immediately
|
|
342
|
+
this.update();
|
|
343
|
+
} else {
|
|
344
|
+
// Non-critical update - queue for batching
|
|
345
|
+
const scheduler = getUpdateScheduler();
|
|
346
|
+
scheduler.queue(this);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Lifecycle methods (to be overridden by child classes)
|
|
351
|
+
onMount() {}
|
|
352
|
+
onUnmount() {}
|
|
353
|
+
onUpdate(oldState) {}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Error handler lifecycle hook
|
|
357
|
+
* Override in child classes to handle errors
|
|
358
|
+
* @param {Error} error - The error that occurred
|
|
359
|
+
* @param {Object} errorInfo - Additional error information
|
|
360
|
+
*/
|
|
361
|
+
onError(error, errorInfo) {
|
|
362
|
+
// Default implementation does nothing
|
|
363
|
+
// Child classes can override to handle errors
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Methods to be implemented by child classes
|
|
367
|
+
render() {
|
|
368
|
+
try {
|
|
369
|
+
return this.template ? this.template() : '';
|
|
370
|
+
} catch (error) {
|
|
371
|
+
this._handleError(error, { phase: 'render' });
|
|
372
|
+
return '<div class="component-error">Error rendering component</div>';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Handle errors and propagate to nearest ErrorBoundary
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
_handleError(error, errorInfo = {}) {
|
|
381
|
+
// Call component's error handler if it exists
|
|
382
|
+
if (this.onError) {
|
|
383
|
+
try {
|
|
384
|
+
this.onError(error, errorInfo);
|
|
385
|
+
} catch (handlerError) {
|
|
386
|
+
console.error('[Component] Error in onError handler:', handlerError);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Propagate to parent ErrorBoundary if it exists
|
|
391
|
+
// Check by constructor name to avoid circular dependency
|
|
392
|
+
let parent = this._parent;
|
|
393
|
+
while (parent) {
|
|
394
|
+
if (parent.constructor && parent.constructor.name === 'ErrorBoundary') {
|
|
395
|
+
if (parent._errorHandler) {
|
|
396
|
+
parent._errorHandler(error, { ...errorInfo, component: this.constructor.name });
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
parent = parent._parent;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// If no ErrorBoundary found, log to console
|
|
404
|
+
console.error(`[Component] Unhandled error in ${this.constructor.name}:`, error, errorInfo);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
events() {
|
|
408
|
+
return {};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Register a cleanup function to be called on unmount
|
|
413
|
+
* @param {Function} cleanupFn - Function to call during cleanup
|
|
414
|
+
* @returns {Function} - Unregister function to remove this cleanup
|
|
415
|
+
*/
|
|
416
|
+
registerCleanup(cleanupFn) {
|
|
417
|
+
if (typeof cleanupFn !== 'function') {
|
|
418
|
+
console.warn('[Component] registerCleanup called with non-function:', cleanupFn);
|
|
419
|
+
return () => {};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this._cleanupRegistry.add(cleanupFn);
|
|
423
|
+
|
|
424
|
+
// Return unregister function
|
|
425
|
+
return () => {
|
|
426
|
+
this._cleanupRegistry.delete(cleanupFn);
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Execute all registered cleanup functions
|
|
432
|
+
* @private
|
|
433
|
+
*/
|
|
434
|
+
_executeCleanup() {
|
|
435
|
+
this._cleanupRegistry.forEach(cleanupFn => {
|
|
436
|
+
try {
|
|
437
|
+
cleanupFn();
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error('[Component] Error during cleanup:', error);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
this._cleanupRegistry.clear();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Unmount all child components
|
|
447
|
+
* @private
|
|
448
|
+
*/
|
|
449
|
+
_unmountChildren() {
|
|
450
|
+
for (const [key, child] of this._children.entries()) {
|
|
451
|
+
try {
|
|
452
|
+
if (child && typeof child.unmount === 'function') {
|
|
453
|
+
child.unmount();
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.error(`[Component] Error unmounting child "${key}":`, error);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
this._children.clear();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Create and track a child component
|
|
464
|
+
* @param {string} key - Unique key for this child component
|
|
465
|
+
* @param {Component} childComponent - Child component instance
|
|
466
|
+
* @returns {Component} - The child component
|
|
467
|
+
*/
|
|
468
|
+
createChild(key, childComponent) {
|
|
469
|
+
if (this._children.has(key)) {
|
|
470
|
+
console.warn(`[Component] Child with key "${key}" already exists, unmounting previous instance`);
|
|
471
|
+
const previous = this._children.get(key);
|
|
472
|
+
if (previous && typeof previous.unmount === 'function') {
|
|
473
|
+
previous.unmount();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Set parent reference for context traversal
|
|
478
|
+
childComponent._parent = this;
|
|
479
|
+
|
|
480
|
+
// Inherit parent context
|
|
481
|
+
this._context.forEach((value, key) => {
|
|
482
|
+
childComponent._context.set(key, value);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
this._children.set(key, childComponent);
|
|
486
|
+
|
|
487
|
+
// Register cleanup to unmount child
|
|
488
|
+
this.registerCleanup(() => {
|
|
489
|
+
if (childComponent && typeof childComponent.unmount === 'function') {
|
|
490
|
+
childComponent.unmount();
|
|
491
|
+
}
|
|
492
|
+
this._children.delete(key);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return childComponent;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Wrapper for setTimeout that automatically registers cleanup
|
|
500
|
+
* @param {Function} callback - Function to call after delay
|
|
501
|
+
* @param {number} delay - Delay in milliseconds
|
|
502
|
+
* @returns {number} - Timer ID (can be used with clearTimeout)
|
|
503
|
+
*/
|
|
504
|
+
setTimeout(callback, delay) {
|
|
505
|
+
const timerId = window.setTimeout(callback, delay);
|
|
506
|
+
|
|
507
|
+
// Register cleanup to clear the timer
|
|
508
|
+
this.registerCleanup(() => {
|
|
509
|
+
window.clearTimeout(timerId);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
return timerId;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Wrapper for setInterval that automatically registers cleanup
|
|
517
|
+
* @param {Function} callback - Function to call repeatedly
|
|
518
|
+
* @param {number} delay - Interval in milliseconds
|
|
519
|
+
* @returns {number} - Timer ID (can be used with clearInterval)
|
|
520
|
+
*/
|
|
521
|
+
setInterval(callback, delay) {
|
|
522
|
+
const timerId = window.setInterval(callback, delay);
|
|
523
|
+
|
|
524
|
+
// Register cleanup to clear the interval
|
|
525
|
+
this.registerCleanup(() => {
|
|
526
|
+
window.clearInterval(timerId);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
return timerId;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Subscribe to an event with automatic cleanup on unmount
|
|
534
|
+
* @param {string} eventName - Name of the event to subscribe to
|
|
535
|
+
* @param {Function} callback - Callback function to call when event is emitted
|
|
536
|
+
* @returns {Function} - Unsubscribe function (also auto-called on unmount)
|
|
537
|
+
*/
|
|
538
|
+
subscribe(eventName, callback) {
|
|
539
|
+
// Subscribe to the event
|
|
540
|
+
const unsubscribe = eventBus.on(eventName, callback);
|
|
541
|
+
|
|
542
|
+
// Track the subscription for automatic cleanup
|
|
543
|
+
this._subscriptions.add(unsubscribe);
|
|
544
|
+
|
|
545
|
+
// Register cleanup to unsubscribe
|
|
546
|
+
this.registerCleanup(() => {
|
|
547
|
+
unsubscribe();
|
|
548
|
+
this._subscriptions.delete(unsubscribe);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
return unsubscribe;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Subscribe to an event for one-time use with automatic cleanup
|
|
556
|
+
* @param {string} eventName - Name of the event to subscribe to
|
|
557
|
+
* @param {Function} callback - Callback function to call when event is emitted (once)
|
|
558
|
+
* @returns {Function} - Unsubscribe function (also auto-called on unmount or after first call)
|
|
559
|
+
*/
|
|
560
|
+
subscribeOnce(eventName, callback) {
|
|
561
|
+
// Subscribe to the event once
|
|
562
|
+
const unsubscribe = eventBus.once(eventName, callback);
|
|
563
|
+
|
|
564
|
+
// Track the subscription for automatic cleanup
|
|
565
|
+
this._subscriptions.add(unsubscribe);
|
|
566
|
+
|
|
567
|
+
// Register cleanup
|
|
568
|
+
this.registerCleanup(() => {
|
|
569
|
+
unsubscribe();
|
|
570
|
+
this._subscriptions.delete(unsubscribe);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
return unsubscribe;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Hook to subscribe to store state changes
|
|
578
|
+
* Automatically updates component when selected store state changes
|
|
579
|
+
*
|
|
580
|
+
* STATE OWNERSHIP RULES:
|
|
581
|
+
* - UI-only state (focus, hover, temporary UI state) → use this.state
|
|
582
|
+
* - Shared/global state (balances, price, wallet, contract data) → use store
|
|
583
|
+
* - Derived state → use selectors
|
|
584
|
+
*
|
|
585
|
+
* Usage examples:
|
|
586
|
+
* // Using a selector method
|
|
587
|
+
* const isPhase2 = this.useStore(tradingStore, () => tradingStore.selectIsPhase2());
|
|
588
|
+
*
|
|
589
|
+
* // Using a direct state selector
|
|
590
|
+
* const price = this.useStore(tradingStore, (state) => state.price.current);
|
|
591
|
+
*
|
|
592
|
+
* // With update callback
|
|
593
|
+
* this.useStore(tradingStore, () => tradingStore.selectIsPhase2(), (newValue, oldValue) => {
|
|
594
|
+
* console.log('Phase 2 changed:', newValue);
|
|
595
|
+
* });
|
|
596
|
+
*
|
|
597
|
+
* @param {Store} store - Store instance to subscribe to
|
|
598
|
+
* @param {Function} selector - Function that selects state (can be store method or state selector)
|
|
599
|
+
* @param {Function} onUpdate - Optional callback when selected state changes
|
|
600
|
+
* @returns {any} - Current selected state value
|
|
601
|
+
*/
|
|
602
|
+
useStore(store, selector, onUpdate) {
|
|
603
|
+
if (!store || typeof selector !== 'function') {
|
|
604
|
+
console.warn('[Component] useStore called with invalid arguments');
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Store the last value for comparison
|
|
609
|
+
if (!this._storeValues) {
|
|
610
|
+
this._storeValues = new Map();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const selectorKey = selector.toString(); // Use function string as key (not perfect but works)
|
|
614
|
+
let lastValue = this._storeValues.get(selectorKey);
|
|
615
|
+
|
|
616
|
+
// Get initial value
|
|
617
|
+
const getCurrentValue = () => {
|
|
618
|
+
// Try calling as store method first (e.g., tradingStore.selectIsPhase2())
|
|
619
|
+
try {
|
|
620
|
+
const result = selector.call(store);
|
|
621
|
+
if (result !== undefined) {
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
} catch (e) {
|
|
625
|
+
// Not a method, try as state selector
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Try as state selector (e.g., (state) => state.price)
|
|
629
|
+
return selector(store.getState());
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const currentValue = getCurrentValue();
|
|
633
|
+
this._storeValues.set(selectorKey, currentValue);
|
|
634
|
+
|
|
635
|
+
// Subscribe to store changes
|
|
636
|
+
const unsubscribe = store.subscribe(() => {
|
|
637
|
+
const newValue = getCurrentValue();
|
|
638
|
+
const oldValue = lastValue;
|
|
639
|
+
|
|
640
|
+
// Only update if value actually changed (shallow comparison)
|
|
641
|
+
if (this._hasValueChanged(oldValue, newValue)) {
|
|
642
|
+
// Update stored value
|
|
643
|
+
this._storeValues.set(selectorKey, newValue);
|
|
644
|
+
lastValue = newValue;
|
|
645
|
+
|
|
646
|
+
// Call optional update callback
|
|
647
|
+
if (typeof onUpdate === 'function') {
|
|
648
|
+
onUpdate(newValue, oldValue);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Trigger component update
|
|
652
|
+
this.update();
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Register cleanup to unsubscribe
|
|
657
|
+
this.registerCleanup(() => {
|
|
658
|
+
unsubscribe();
|
|
659
|
+
if (this._storeValues) {
|
|
660
|
+
this._storeValues.delete(selectorKey);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
return currentValue;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Check if two values have changed (shallow comparison)
|
|
669
|
+
* @private
|
|
670
|
+
*/
|
|
671
|
+
_hasValueChanged(oldValue, newValue) {
|
|
672
|
+
// Primitive comparison
|
|
673
|
+
if (oldValue === newValue) return false;
|
|
674
|
+
|
|
675
|
+
// Null/undefined handling
|
|
676
|
+
if (oldValue == null || newValue == null) return oldValue !== newValue;
|
|
677
|
+
|
|
678
|
+
// Object comparison (shallow)
|
|
679
|
+
if (typeof oldValue === 'object' && typeof newValue === 'object') {
|
|
680
|
+
const oldKeys = Object.keys(oldValue);
|
|
681
|
+
const newKeys = Object.keys(newValue);
|
|
682
|
+
|
|
683
|
+
if (oldKeys.length !== newKeys.length) return true;
|
|
684
|
+
|
|
685
|
+
return oldKeys.some(key => oldValue[key] !== newValue[key]);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Get a cached element reference by name and selector
|
|
693
|
+
* @param {string} name - Reference name (for caching)
|
|
694
|
+
* @param {string} selector - CSS selector to find element
|
|
695
|
+
* @returns {HTMLElement|null} - Element or null if not found
|
|
696
|
+
*/
|
|
697
|
+
_getRefCached(name, selector) {
|
|
698
|
+
if (!this.element) return null;
|
|
699
|
+
|
|
700
|
+
// Check cache first
|
|
701
|
+
if (this._refs.has(name)) {
|
|
702
|
+
const cached = this._refs.get(name);
|
|
703
|
+
// Verify element is still in DOM
|
|
704
|
+
if (cached && this.element.contains(cached)) {
|
|
705
|
+
return cached;
|
|
706
|
+
}
|
|
707
|
+
// Stale reference, remove from cache
|
|
708
|
+
this._refs.delete(name);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Query DOM if not cached
|
|
712
|
+
const element = this.element.querySelector(selector);
|
|
713
|
+
if (element) {
|
|
714
|
+
this._refs.set(name, element);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return element;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Manually update a cached reference
|
|
722
|
+
* @param {string} name - Reference name
|
|
723
|
+
* @param {HTMLElement} element - Element to cache
|
|
724
|
+
*/
|
|
725
|
+
updateRef(name, element) {
|
|
726
|
+
if (element) {
|
|
727
|
+
this._refs.set(name, element);
|
|
728
|
+
} else {
|
|
729
|
+
this._refs.delete(name);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Invalidate all cached references
|
|
735
|
+
* Should be called when DOM structure changes significantly
|
|
736
|
+
*/
|
|
737
|
+
invalidateRefs() {
|
|
738
|
+
this._refs.clear();
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Invalidate a specific cached reference
|
|
743
|
+
* @param {string} name - Reference name to invalidate
|
|
744
|
+
*/
|
|
745
|
+
invalidateRef(name) {
|
|
746
|
+
this._refs.delete(name);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Get a single DOM element within this component's element
|
|
751
|
+
* Convenience wrapper around querySelector with null-safety check
|
|
752
|
+
* @param {string} selector - CSS selector to query
|
|
753
|
+
* @returns {HTMLElement|null} The first matching element or null
|
|
754
|
+
*/
|
|
755
|
+
getRef(selector) {
|
|
756
|
+
if (!this.element) {
|
|
757
|
+
console.warn(`[Component] getRef called on ${this.constructor.name} before element exists`);
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
return this.element.querySelector(selector);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Get multiple DOM elements within this component's element
|
|
765
|
+
* Convenience wrapper around querySelectorAll with null-safety check
|
|
766
|
+
* @param {string} selector - CSS selector to query
|
|
767
|
+
* @returns {Array<HTMLElement>} Array of matching elements (empty array if none found)
|
|
768
|
+
*/
|
|
769
|
+
getRefs(selector) {
|
|
770
|
+
if (!this.element) {
|
|
771
|
+
console.warn(`[Component] getRefs called on ${this.constructor.name} before element exists`);
|
|
772
|
+
return [];
|
|
773
|
+
}
|
|
774
|
+
// Convert NodeList to Array for easier manipulation
|
|
775
|
+
return Array.from(this.element.querySelectorAll(selector));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Provide a context value to child components
|
|
780
|
+
* @param {string} key - Context key
|
|
781
|
+
* @param {any} value - Context value
|
|
782
|
+
*/
|
|
783
|
+
provideContext(key, value) {
|
|
784
|
+
this._context.set(key, value);
|
|
785
|
+
|
|
786
|
+
// Propagate to existing children
|
|
787
|
+
this._children.forEach(child => {
|
|
788
|
+
if (child && typeof child._context !== 'undefined') {
|
|
789
|
+
child._context.set(key, value);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Get a context value, searching up the component tree
|
|
796
|
+
* @param {string} key - Context key
|
|
797
|
+
* @returns {any} - Context value or undefined if not found
|
|
798
|
+
*/
|
|
799
|
+
getContext(key) {
|
|
800
|
+
// Check own context first
|
|
801
|
+
if (this._context.has(key)) {
|
|
802
|
+
return this._context.get(key);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Search up the tree
|
|
806
|
+
let parent = this._parent;
|
|
807
|
+
while (parent) {
|
|
808
|
+
if (parent._context && parent._context.has(key)) {
|
|
809
|
+
return parent._context.get(key);
|
|
810
|
+
}
|
|
811
|
+
parent = parent._parent;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Remove a context value
|
|
819
|
+
* @param {string} key - Context key to remove
|
|
820
|
+
*/
|
|
821
|
+
removeContext(key) {
|
|
822
|
+
this._context.delete(key);
|
|
823
|
+
|
|
824
|
+
// Remove from children
|
|
825
|
+
this._children.forEach(child => {
|
|
826
|
+
if (child && typeof child._context !== 'undefined') {
|
|
827
|
+
child._context.delete(key);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|