@dooboostore/simple-web-component 1.0.7 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dooboostore/simple-web-component",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -79,8 +79,8 @@
79
79
  "esbuild-plugin-tsc": "^0.5.0",
80
80
  "esbuild": "^0.23.0",
81
81
  "reflect-metadata": "^0.2.2",
82
- "@dooboostore/core": "1.0.28",
83
82
  "@dooboostore/core-web": "1.0.9",
83
+ "@dooboostore/core": "1.0.28",
84
84
  "@dooboostore/core-node": "1.0.10"
85
85
  },
86
86
  "scripts": {
@@ -125,15 +125,19 @@ export const elementDefine =
125
125
 
126
126
  constructor(...args: any[]) {
127
127
  super(...args);
128
+
128
129
  if (stateList) {
129
130
  stateList.forEach(meta => {
130
131
  const key = meta.propertyKey;
131
132
  const stateName = meta.options.name!;
133
+
134
+ // 필드 초기화로 덮어씌워지기 전/후의 값을 확실히 낚아챔
132
135
  const initialVal = (this as any)[key];
133
136
  this._internalStates.set(
134
137
  key,
135
138
  SwcUtils.createReactiveProxy(initialVal, () => this._updateState(stateName))
136
139
  );
140
+
137
141
  Object.defineProperty(this, key, {
138
142
  get: () => this._internalStates.get(key),
139
143
  set: newVal => {
@@ -149,6 +153,7 @@ export const elementDefine =
149
153
  });
150
154
  });
151
155
  }
156
+
152
157
  const innerHtmlList = getInnerHtmlMetadataList(this);
153
158
  if (innerHtmlList?.some(it => it.options.useShadow === true) && !this.shadowRoot) {
154
159
  this.attachShadow({ mode: 'open' });
@@ -157,7 +162,6 @@ export const elementDefine =
157
162
 
158
163
  private _syncDecorators() {
159
164
  this._buildStateMap();
160
-
161
165
  const getSearchRoots = (rootOption?: string): Node[] => {
162
166
  const roots: Node[] = [];
163
167
  if (rootOption === 'shadow') {
@@ -168,7 +172,6 @@ export const elementDefine =
168
172
  if (this.shadowRoot) roots.push(this.shadowRoot);
169
173
  roots.push(this as any as Node);
170
174
  } else {
171
- // Default: 'auto'
172
175
  roots.push(this.shadowRoot || (this as any as Node));
173
176
  }
174
177
  return roots;
@@ -252,8 +255,7 @@ export const elementDefine =
252
255
  }
253
256
 
254
257
  private _buildStateMap() {
255
- this._stateBindings.clear();
256
- this._externalSources.clear();
258
+ // 기존 바인딩 맵을 완전히 비우지 않고 유지하면서 새로운 노드만 추가함 (바인딩 소실 방지 핵심)
257
259
  const scan = (root: Node) => {
258
260
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
259
261
  let node: Node | null = null;
@@ -262,7 +264,7 @@ export const elementDefine =
262
264
  else if (node.nodeType === Node.ELEMENT_NODE) {
263
265
  const el = node as HTMLElement;
264
266
  const alias = el.getAttribute('as');
265
- if (alias) {
267
+ if (alias && !this._externalSources.has(alias)) {
266
268
  this._externalSources.set(alias, el);
267
269
  el.addEventListener(STATE_CHANGE_EVENT, () => this._updateState(alias));
268
270
  }
@@ -275,20 +277,36 @@ export const elementDefine =
275
277
  }
276
278
 
277
279
  private _parseAndBind(node: Node | Attr, type: 'text' | 'attribute', owner?: HTMLElement) {
278
- const content = node.textContent || '';
280
+ const tplKey = `__swc_original_${this._swcId}`;
281
+ // 이미 이 인스턴스에 의해 바인딩된 노드라면 스킵 (중복 방지)
282
+ const isAlreadyBound = (node as any).__swc_bound_ids?.has(this._swcId);
283
+
284
+ // 텍스트는 원본 템플릿(tplKey)에서, 없으면 현재 내용에서 추출
285
+ const content = (node as any)[tplKey] || node.textContent || '';
279
286
  const matches = Array.from(content.matchAll(/{{(.*?)}}/g));
280
287
  if (matches.length === 0) return;
288
+
289
+ if (isAlreadyBound) return;
290
+
281
291
  matches.forEach(match => {
282
292
  const fullPath = match[1].trim();
283
293
  const rootName = fullPath.split('.')[0];
294
+
284
295
  const isState = stateList?.some(s => s.options.name === rootName);
285
296
  const isLogicKey = (this as any)._asKey === rootName || (this as any)._asIndexKey === rootName;
286
297
  const isExternal = this._externalSources.has(rootName);
287
298
  const isSelfAlias = this.getAttribute('as') === rootName;
299
+
288
300
  if (!isState && !isLogicKey && !isExternal && !isSelfAlias) return;
301
+
289
302
  if (!this._stateBindings.has(rootName)) this._stateBindings.set(rootName, []);
290
- const tplKey = `__swc_original_${this._swcId}`;
303
+
291
304
  if (!(node as any)[tplKey]) (node as any)[tplKey] = content;
305
+
306
+ // 바인딩 ID 기록
307
+ if (!(node as any).__swc_bound_ids) (node as any).__swc_bound_ids = new Set();
308
+ (node as any).__swc_bound_ids.add(this._swcId);
309
+
292
310
  this._stateBindings.get(rootName)!.push({ node, type, owner, path: fullPath });
293
311
  this._updateState(rootName);
294
312
  });
@@ -306,14 +324,20 @@ export const elementDefine =
306
324
  const bindings = this._stateBindings.get(stateName);
307
325
  if (!bindings) return;
308
326
  const tplKey = `__swc_original_${this._swcId}`;
327
+
309
328
  bindings.forEach(bin => {
310
329
  let text = (bin.node as any)[tplKey];
330
+ if (!text) return;
331
+
311
332
  const matches = Array.from(text.matchAll(/{{(.*?)}}/g));
333
+ let updatedText = text;
334
+
312
335
  for (const match of matches) {
313
336
  const path = match[1].trim();
314
337
  const root = path.split('.')[0];
315
338
  let val: any = undefined;
316
339
  let current: HTMLElement | null = this as any as HTMLElement;
340
+
317
341
  while (current) {
318
342
  const currentNewClass = current as any;
319
343
  if (current.getAttribute('as') === root) {
@@ -338,16 +362,24 @@ export const elementDefine =
338
362
  }
339
363
  current = current.parentElement || (current.getRootNode() as any).host;
340
364
  }
365
+
341
366
  if (val !== undefined) {
342
367
  const strVal = val === null || val === undefined ? '' : typeof val === 'object' ? '[Object]' : String(val);
343
- text = text.replace(match[0], strVal);
368
+ updatedText = updatedText.replace(match[0], strVal);
369
+
344
370
  if (bin.type === 'attribute' && bin.owner) {
345
- if (val === null || val === undefined) bin.owner.removeAttribute(bin.attrName!);
346
- else bin.owner.setAttribute(bin.attrName!, text);
371
+ const attrName = (bin.node as Attr).name;
372
+ if (val === null || val === undefined) bin.owner.removeAttribute(attrName);
373
+ else {
374
+ bin.owner.setAttribute(attrName, updatedText);
375
+ if ((attrName === 'value' || attrName === 'checked') && bin.owner.tagName.match(/INPUT|TEXTAREA|SELECT/)) {
376
+ (bin.owner as any)[attrName] = updatedText;
377
+ }
378
+ }
347
379
  }
348
380
  }
349
381
  }
350
- if (bin.type === 'text') bin.node.textContent = text;
382
+ if (bin.type === 'text') bin.node.textContent = updatedText;
351
383
  });
352
384
  }
353
385
 
@@ -1,45 +1,33 @@
1
1
  export class SwcUtils {
2
- static getValueByPath(obj: any, path: string, asKey: string) {
2
+ static getValueByPath(obj: any, path: string, rootName: string) {
3
3
  if (!obj || !path) return undefined;
4
4
 
5
5
  const parts = path.split('.');
6
6
 
7
- // 1. Try resolving directly on the provided object (Full path)
8
- let result = obj;
9
- let success = true;
10
- for (const part of parts) {
11
- if (result !== null && typeof result === 'object' && part in result) {
12
- result = result[part];
13
- } else {
14
- success = false;
15
- break;
16
- }
17
- }
18
-
19
- if (success && result !== obj && !(result instanceof HTMLElement)) {
20
- return result;
21
- }
22
-
23
- // 2. Handle asKey fallback (for swc-for-of where obj IS the data)
24
- if (path === asKey) return obj;
25
- if (path.startsWith(asKey + '.')) {
7
+ // 1. If the path starts with the rootName (alias), strip it and resolve within obj
8
+ // e.g., getValueByPath({name: 'Kim'}, 'u.name', 'u') -> returns 'Kim'
9
+ if (parts[0] === rootName && parts.length > 1) {
26
10
  const subParts = parts.slice(1);
27
11
  let subResult = obj;
28
12
  for (const part of subParts) {
29
- if (subResult !== null && typeof subResult === 'object' && part in subResult) {
30
- subResult = subResult[part];
31
- } else {
32
- return undefined;
33
- }
13
+ if (subResult === null || subResult === undefined) return undefined;
14
+ subResult = subResult[part];
34
15
  }
35
16
  return subResult;
36
17
  }
37
18
 
38
- // 3. 상위 스코프 탐색 (Bubbling Up)
39
- // 만약 현재 엘리먼트에서 못 찾았다면, 부모 엘리먼트에게 물어봅니다.
40
- if (obj instanceof HTMLElement && obj.parentElement) {
41
- // 부모의 바인딩 시스템에 접근하기 위해 이벤트를 쏘거나 직접 참조할 있음
42
- // 여기서는 엔진 레벨에서 처리하기 위해 elementDefine에서 루프를 돌릴 예정입니다.
19
+ // 2. Fallback: resolve directly on obj (standard behavior)
20
+ let result = obj;
21
+ for (const part of parts) {
22
+ if (result === null || result === undefined || !(part in result)) {
23
+ return undefined;
24
+ }
25
+ result = result[part];
26
+ }
27
+
28
+ // Safety: don't return the instance itself or a DOM element as a template value
29
+ if (result !== obj && !(result instanceof HTMLElement)) {
30
+ return result;
43
31
  }
44
32
 
45
33
  return undefined;
@@ -100,9 +88,7 @@ export class SwcUtils {
100
88
  set: (t, prop, val) => {
101
89
  const oldVal = t[prop];
102
90
  if (oldVal === val) return true;
103
-
104
91
  t[prop] = makeRecursiveProxy(val);
105
-
106
92
  const isIndex = !isNaN(Number(prop)) && Array.isArray(t);
107
93
  if (isIndex && onIndexChange) {
108
94
  onIndexChange(Number(prop), val);