@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/README.md +50 -20
- package/dist/cjs/decorators/elementDefine.js +19 -9
- package/dist/cjs/decorators/elementDefine.js.map +2 -2
- package/dist/cjs/utils/Utils.js +13 -22
- package/dist/cjs/utils/Utils.js.map +2 -2
- package/dist/esm/decorators/elementDefine.js +19 -9
- package/dist/esm/decorators/elementDefine.js.map +2 -2
- package/dist/esm/utils/Utils.js +13 -22
- package/dist/esm/utils/Utils.js.map +2 -2
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +32 -31
- package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +2 -2
- package/dist/types/decorators/elementDefine.d.ts.map +1 -1
- package/dist/types/utils/Utils.d.ts +1 -1
- package/dist/types/utils/Utils.d.ts.map +1 -1
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +32 -31
- package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +2 -2
- package/package.json +2 -2
- package/src/decorators/elementDefine.ts +43 -11
- package/src/utils/Utils.ts +18 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dooboostore/simple-web-component",
|
|
3
|
-
"version": "1.0.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
368
|
+
updatedText = updatedText.replace(match[0], strVal);
|
|
369
|
+
|
|
344
370
|
if (bin.type === 'attribute' && bin.owner) {
|
|
345
|
-
|
|
346
|
-
|
|
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 =
|
|
382
|
+
if (bin.type === 'text') bin.node.textContent = updatedText;
|
|
351
383
|
});
|
|
352
384
|
}
|
|
353
385
|
|
package/src/utils/Utils.ts
CHANGED
|
@@ -1,45 +1,33 @@
|
|
|
1
1
|
export class SwcUtils {
|
|
2
|
-
static getValueByPath(obj: any, path: 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.
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
30
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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);
|