@wsxjs/wsx-core 0.0.7 → 0.0.9

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.
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { h, type JSXChildren } from "./jsx-factory";
11
- import { reactive, createState, reactiveWithDebug } from "./utils/reactive";
11
+ import { BaseComponent, type BaseComponentConfig } from "./base-component";
12
12
  import { createLogger } from "./utils/logger";
13
13
 
14
14
  const logger = createLogger("LightComponent");
@@ -16,35 +16,17 @@ const logger = createLogger("LightComponent");
16
16
  /**
17
17
  * Light DOM Component 配置接口
18
18
  */
19
- export interface LightComponentConfig {
20
- styles?: string; // CSS内容
21
- styleName?: string; // 样式名称,用于缓存
22
- debug?: boolean; // 是否启用响应式调试模式
23
- [key: string]: unknown;
24
- }
19
+ export type LightComponentConfig = BaseComponentConfig;
25
20
 
26
21
  /**
27
22
  * Light DOM WSX Web Component 基类
28
23
  */
29
- export abstract class LightComponent extends HTMLElement {
30
- protected config: LightComponentConfig;
31
- protected connected: boolean = false;
32
- private _isDebugEnabled: boolean = false;
33
- private _reactiveStates = new Map<string, any>();
34
-
35
- /**
36
- * 子类应该重写这个方法来定义观察的属性
37
- * @returns 要观察的属性名数组
38
- */
39
- static get observedAttributes(): string[] {
40
- return [];
41
- }
24
+ export abstract class LightComponent extends BaseComponent {
25
+ protected config!: LightComponentConfig; // Initialized by BaseComponent constructor
42
26
 
43
27
  constructor(config: LightComponentConfig = {}) {
44
- super();
45
-
46
- this.config = config;
47
- this._isDebugEnabled = config.debug ?? false;
28
+ super(config);
29
+ // BaseComponent already created this.config with getter for styles
48
30
  }
49
31
 
50
32
  /**
@@ -61,15 +43,23 @@ export abstract class LightComponent extends HTMLElement {
61
43
  this.connected = true;
62
44
  try {
63
45
  // 应用CSS样式到组件自身
64
- if (this.config.styles) {
46
+ // CRITICAL: _defineProperty for class properties executes AFTER super() but BEFORE constructor body
47
+ // However, in practice, _defineProperty may execute AFTER the constructor body
48
+ // So we need to check _autoStyles directly first, then fallback to config.styles getter
49
+ // The getter will dynamically check _autoStyles when accessed
50
+ const stylesToApply = this._autoStyles || this.config.styles;
51
+ if (stylesToApply) {
65
52
  const styleName = this.config.styleName || this.constructor.name;
66
- this.applyScopedStyles(styleName, this.config.styles);
53
+ this.applyScopedStyles(styleName, stylesToApply);
67
54
  }
68
55
 
69
56
  // 渲染JSX内容到Light DOM
70
57
  const content = this.render();
71
58
  this.appendChild(content);
72
59
 
60
+ // 初始化事件监听器
61
+ this.initializeEventListeners();
62
+
73
63
  // 调用子类的初始化钩子
74
64
  this.onConnected?.();
75
65
  } catch (error) {
@@ -82,33 +72,13 @@ export abstract class LightComponent extends HTMLElement {
82
72
  * Web Component生命周期:从DOM断开
83
73
  */
84
74
  disconnectedCallback(): void {
75
+ this.connected = false;
76
+ this.cleanup(); // 清理资源(包括防抖定时器)
85
77
  this.cleanupReactiveStates();
86
78
  this.cleanupStyles();
87
79
  this.onDisconnected?.();
88
80
  }
89
81
 
90
- /**
91
- * Web Component生命周期:属性变化
92
- */
93
- attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
94
- this.onAttributeChanged?.(name, oldValue, newValue);
95
- }
96
-
97
- /**
98
- * 可选生命周期钩子:组件已连接
99
- */
100
- protected onConnected?(): void;
101
-
102
- /**
103
- * 可选生命周期钩子:组件已断开
104
- */
105
- protected onDisconnected?(): void;
106
-
107
- /**
108
- * 可选生命周期钩子:属性已更改
109
- */
110
- protected onAttributeChanged?(name: string, oldValue: string, newValue: string): void;
111
-
112
82
  /**
113
83
  * 查找组件内的元素
114
84
  *
@@ -129,53 +99,6 @@ export abstract class LightComponent extends HTMLElement {
129
99
  return HTMLElement.prototype.querySelectorAll.call(this, selector) as NodeListOf<T>;
130
100
  }
131
101
 
132
- /**
133
- * 创建响应式对象
134
- *
135
- * @param obj 要变为响应式的对象
136
- * @param debugName 调试名称(可选)
137
- * @returns 响应式代理对象
138
- */
139
- protected reactive<T extends object>(obj: T, debugName?: string): T {
140
- const reactiveFn = this._isDebugEnabled ? reactiveWithDebug : reactive;
141
- const name = debugName || `${this.constructor.name}.reactive`;
142
-
143
- return this._isDebugEnabled
144
- ? reactiveFn(obj, () => this.scheduleRerender(), name)
145
- : reactiveFn(obj, () => this.scheduleRerender());
146
- }
147
-
148
- /**
149
- * 创建响应式状态
150
- *
151
- * @param key 状态标识符
152
- * @param initialValue 初始值
153
- * @returns [getter, setter] 元组
154
- */
155
- protected useState<T>(
156
- key: string,
157
- initialValue: T
158
- ): [() => T, (value: T | ((prev: T) => T)) => void] {
159
- if (!this._reactiveStates.has(key)) {
160
- const [getter, setter] = createState(initialValue, () => this.scheduleRerender());
161
- this._reactiveStates.set(key, { getter, setter });
162
- }
163
-
164
- const state = this._reactiveStates.get(key);
165
- return [state.getter, state.setter];
166
- }
167
-
168
- /**
169
- * 调度重渲染
170
- * 这个方法被响应式系统调用,开发者通常不需要直接调用
171
- */
172
- protected scheduleRerender(): void {
173
- // 确保组件已连接到 DOM
174
- if (this.connected) {
175
- this.rerender();
176
- }
177
- }
178
-
179
102
  /**
180
103
  * 重新渲染组件
181
104
  */
@@ -187,36 +110,97 @@ export abstract class LightComponent extends HTMLElement {
187
110
  return;
188
111
  }
189
112
 
190
- // 清空现有内容(包括样式元素)
191
- this.innerHTML = "";
113
+ // 1. 捕获焦点状态(在 DOM 替换之前)
114
+ const focusState = this.captureFocusState();
115
+ // 保存到实例变量,供 render() 使用(如果需要)
116
+ this._pendingFocusState = focusState;
192
117
 
193
- // 重新应用样式(必须在内容之前添加,确保样式优先)
194
- if (this.config.styles) {
195
- const styleName = this.config.styleName || this.constructor.name;
196
- // 直接创建并添加样式元素,不检查是否存在(因为 innerHTML = "" 已经清空了)
197
- const styleElement = document.createElement("style");
198
- styleElement.setAttribute("data-wsx-light-component", styleName);
199
- styleElement.textContent = this.config.styles;
200
- // 使用 prepend 或 insertBefore 确保样式在第一个位置
201
- // 由于 innerHTML = "" 后 firstChild 是 null,使用 appendChild 然后调整顺序
202
- this.appendChild(styleElement);
203
- }
204
-
205
- // 重新渲染JSX内容
206
118
  try {
119
+ // 重新渲染JSX内容
207
120
  const content = this.render();
208
- this.appendChild(content);
209
121
 
210
- // 确保样式元素在内容之前(如果样式存在)
211
- if (this.config.styles && this.children.length > 1) {
212
- const styleElement = this.querySelector(
213
- `style[data-wsx-light-component="${this.config.styleName || this.constructor.name}"]`
214
- );
215
- if (styleElement && styleElement !== this.firstChild) {
216
- // 将样式元素移到第一个位置
122
+ // 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
123
+ // 这样可以确保值在元素添加到 DOM 之前就是正确的
124
+ if (focusState && focusState.key && focusState.value !== undefined) {
125
+ // content 树中查找目标元素
126
+ const target = content.querySelector(
127
+ `[data-wsx-key="${focusState.key}"]`
128
+ ) as HTMLElement;
129
+
130
+ if (target) {
131
+ if (
132
+ target instanceof HTMLInputElement ||
133
+ target instanceof HTMLTextAreaElement
134
+ ) {
135
+ target.value = focusState.value;
136
+ }
137
+ }
138
+ }
139
+
140
+ // 确保样式元素存在
141
+ const stylesToApply = this._autoStyles || this.config.styles;
142
+ if (stylesToApply) {
143
+ const styleName = this.config.styleName || this.constructor.name;
144
+ let styleElement = this.querySelector(
145
+ `style[data-wsx-light-component="${styleName}"]`
146
+ ) as HTMLStyleElement;
147
+
148
+ if (!styleElement) {
149
+ // 创建样式元素
150
+ styleElement = document.createElement("style");
151
+ styleElement.setAttribute("data-wsx-light-component", styleName);
152
+ styleElement.textContent = stylesToApply;
217
153
  this.insertBefore(styleElement, this.firstChild);
154
+ } else if (styleElement.textContent !== stylesToApply) {
155
+ // 更新样式内容
156
+ styleElement.textContent = stylesToApply;
218
157
  }
219
158
  }
159
+
160
+ // 使用 requestAnimationFrame 批量执行 DOM 操作,减少重绘
161
+ // 在同一帧中完成添加和移除,避免中间状态
162
+ requestAnimationFrame(() => {
163
+ // 先添加新内容
164
+ this.appendChild(content);
165
+
166
+ // 立即移除旧内容(在同一帧中,浏览器会批量处理)
167
+ const oldChildren = Array.from(this.children).filter((child) => {
168
+ // 保留新添加的内容
169
+ if (child === content) {
170
+ return false;
171
+ }
172
+ // 保留样式元素(如果存在)
173
+ if (
174
+ stylesToApply &&
175
+ child instanceof HTMLStyleElement &&
176
+ child.getAttribute("data-wsx-light-component") ===
177
+ (this.config.styleName || this.constructor.name)
178
+ ) {
179
+ return false;
180
+ }
181
+ return true;
182
+ });
183
+ oldChildren.forEach((child) => child.remove());
184
+
185
+ // 确保样式元素在第一个位置
186
+ if (stylesToApply && this.children.length > 1) {
187
+ const styleElement = this.querySelector(
188
+ `style[data-wsx-light-component="${this.config.styleName || this.constructor.name}"]`
189
+ );
190
+ if (styleElement && styleElement !== this.firstChild) {
191
+ this.insertBefore(styleElement, this.firstChild);
192
+ }
193
+ }
194
+
195
+ // 恢复焦点状态(在 DOM 替换之后)
196
+ // 值已经在添加到 DOM 之前恢复了,这里只需要恢复焦点和光标位置
197
+ // 使用另一个 requestAnimationFrame 确保 DOM 已完全更新
198
+ requestAnimationFrame(() => {
199
+ this.restoreFocusState(focusState);
200
+ // 清除待处理的焦点状态
201
+ this._pendingFocusState = null;
202
+ });
203
+ });
220
204
  } catch (error) {
221
205
  logger.error(`[${this.constructor.name}] Error in rerender:`, error);
222
206
  this.renderError(error);
@@ -266,34 +250,6 @@ export abstract class LightComponent extends HTMLElement {
266
250
  this.insertBefore(styleElement, this.firstChild);
267
251
  }
268
252
 
269
- /**
270
- * 获取配置值
271
- *
272
- * @param key - 配置键
273
- * @param defaultValue - 默认值
274
- * @returns 配置值
275
- */
276
- protected getConfig<T>(key: string, defaultValue?: T): T {
277
- return (this.config[key] as T) ?? (defaultValue as T);
278
- }
279
-
280
- /**
281
- * 设置配置值
282
- *
283
- * @param key - 配置键
284
- * @param value - 配置值
285
- */
286
- protected setConfig(key: string, value: unknown): void {
287
- this.config[key] = value;
288
- }
289
-
290
- /**
291
- * 清理响应式状态
292
- */
293
- private cleanupReactiveStates(): void {
294
- this._reactiveStates.clear();
295
- }
296
-
297
253
  /**
298
254
  * 清理组件样式
299
255
  */
@@ -304,46 +260,6 @@ export abstract class LightComponent extends HTMLElement {
304
260
  existingStyle.remove();
305
261
  }
306
262
  }
307
-
308
- /**
309
- * 获取属性值
310
- *
311
- * @param name - 属性名
312
- * @param defaultValue - 默认值
313
- * @returns 属性值
314
- */
315
- protected getAttr(name: string, defaultValue = ""): string {
316
- return this.getAttribute(name) || defaultValue;
317
- }
318
-
319
- /**
320
- * 设置属性值
321
- *
322
- * @param name - 属性名
323
- * @param value - 属性值
324
- */
325
- protected setAttr(name: string, value: string): void {
326
- this.setAttribute(name, value);
327
- }
328
-
329
- /**
330
- * 移除属性
331
- *
332
- * @param name - 属性名
333
- */
334
- protected removeAttr(name: string): void {
335
- this.removeAttribute(name);
336
- }
337
-
338
- /**
339
- * 检查是否有属性
340
- *
341
- * @param name - 属性名
342
- * @returns 是否存在
343
- */
344
- protected hasAttr(name: string): boolean {
345
- return this.hasAttribute(name);
346
- }
347
263
  }
348
264
 
349
265
  // 导出JSX助手
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Reactive Decorators for WSX Components
3
+ *
4
+ * Provides @state property decorator to mark properties as reactive state.
5
+ * WebComponent and LightComponent already have reactive() and useState() methods.
6
+ * Babel plugin handles @state initialization at compile time.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * class Counter extends WebComponent {
11
+ * @state private count = 0;
12
+ * @state private user = { name: "John" };
13
+ * }
14
+ * ```
15
+ */
16
+
17
+ /**
18
+ * State property decorator
19
+ *
20
+ * Marks a property as reactive state. Babel plugin processes this decorator at compile time
21
+ * and generates initialization code in the constructor.
22
+ *
23
+ * Automatically uses reactive() for objects/arrays and useState() for primitives.
24
+ *
25
+ * @param target - The class prototype
26
+ * @param propertyKey - The property name
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * class MyComponent extends WebComponent {
31
+ * @state private count = 0; // Auto-initialized by Babel plugin
32
+ * @state private user = { name: "John" }; // Auto-initialized by Babel plugin
33
+ * }
34
+ * ```
35
+ */
36
+ export function state(target: unknown, propertyKey: string | symbol | unknown): void {
37
+ /**
38
+ * @state decorator is a compile-time marker for Babel plugin.
39
+ * Babel plugin will:
40
+ * 1. Detect @state decorator on properties
41
+ * 2. Extract initial value from AST
42
+ * 3. Remove @state decorator
43
+ * 4. Generate initialization code in constructor (this.state = this.reactive(...) or useState)
44
+ *
45
+ * This runtime function only performs basic validation.
46
+ * If Babel plugin is not configured, this will throw an error.
47
+ */
48
+
49
+ // Normalize propertyKey
50
+ let normalizedPropertyKey: string | symbol;
51
+ if (typeof propertyKey === "string" || typeof propertyKey === "symbol") {
52
+ normalizedPropertyKey = propertyKey;
53
+ } else {
54
+ const propertyKeyStr = String(propertyKey);
55
+ if (propertyKeyStr === "[object Object]") {
56
+ // Invalid propertyKey - Babel plugin was not configured
57
+ throw new Error(
58
+ `@state decorator: Invalid propertyKey detected. ` +
59
+ `\n\n` +
60
+ `The @state decorator MUST be processed by Babel plugin at compile time. ` +
61
+ `It appears the Babel plugin is not configured in your build setup.` +
62
+ `\n\n` +
63
+ `To fix this, please:` +
64
+ `\n1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin` +
65
+ `\n2. Configure it in vite.config.ts:` +
66
+ `\n import { wsx } from '@wsxjs/wsx-vite-plugin';` +
67
+ `\n export default defineConfig({ plugins: [wsx()] });` +
68
+ `\n3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):` +
69
+ `\n npm install --save-dev @wsxjs/wsx-tsconfig` +
70
+ `\n Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }` +
71
+ `\n Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }` +
72
+ `\n\n` +
73
+ `See: https://github.com/wsxjs/wsxjs#setup for more details.`
74
+ );
75
+ }
76
+ normalizedPropertyKey = propertyKeyStr;
77
+ }
78
+
79
+ // Basic validation: ensure target is valid
80
+ if (target == null) {
81
+ const propertyKeyStr =
82
+ typeof normalizedPropertyKey === "string"
83
+ ? normalizedPropertyKey
84
+ : normalizedPropertyKey.toString();
85
+ throw new Error(
86
+ `@state decorator: Cannot access property "${propertyKeyStr}". ` +
87
+ `Target is ${target === null ? "null" : "undefined"}.` +
88
+ `\n\n` +
89
+ `The @state decorator MUST be processed by Babel plugin at compile time. ` +
90
+ `It appears the Babel plugin is not configured in your build setup.` +
91
+ `\n\n` +
92
+ `To fix this, please:` +
93
+ `\n1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin` +
94
+ `\n2. Configure it in vite.config.ts:` +
95
+ `\n import { wsx } from '@wsxjs/wsx-vite-plugin';` +
96
+ `\n export default defineConfig({ plugins: [wsx()] });` +
97
+ `\n3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):` +
98
+ `\n npm install --save-dev @wsxjs/wsx-tsconfig` +
99
+ `\n Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }` +
100
+ `\n Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }` +
101
+ `\n\n` +
102
+ `See: https://github.com/wsxjs/wsxjs#setup for more details.`
103
+ );
104
+ }
105
+
106
+ if (typeof target !== "object") {
107
+ const propertyKeyStr =
108
+ typeof normalizedPropertyKey === "string"
109
+ ? normalizedPropertyKey
110
+ : normalizedPropertyKey.toString();
111
+ throw new Error(
112
+ `@state decorator: Cannot be used on "${propertyKeyStr}". ` +
113
+ `@state is for properties only, not methods.`
114
+ );
115
+ }
116
+
117
+ // Validate that property has an initial value
118
+ const descriptor = Object.getOwnPropertyDescriptor(target, normalizedPropertyKey);
119
+ if (descriptor?.get) {
120
+ const propertyKeyStr =
121
+ typeof normalizedPropertyKey === "string"
122
+ ? normalizedPropertyKey
123
+ : normalizedPropertyKey.toString();
124
+ throw new Error(
125
+ `@state decorator cannot be used with getter properties. Property: "${propertyKeyStr}"`
126
+ );
127
+ }
128
+
129
+ // Note: We don't store metadata or remove the property here.
130
+ // Babel plugin handles everything at compile time.
131
+ // If this function is called at runtime, it means Babel plugin didn't process the decorator.
132
+ }