custom-elements-ts 0.0.16 → 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.
- package/.eslintrc.json +46 -0
- package/.github/workflows/ci.yml +49 -0
- package/.prettierrc +7 -0
- package/LICENSE +20 -0
- package/README.md +426 -168
- package/demos/counter/counter.element.html +1 -0
- package/demos/counter/counter.element.scss +234 -0
- package/demos/counter/counter.element.ts +68 -0
- package/demos/counter/index.html +205 -0
- package/demos/counter/index.ts +1 -0
- package/demos/site/code-example/code-example.element.scss +168 -0
- package/demos/site/code-example/code-example.element.ts +88 -0
- package/demos/site/event-log/event-log.element.scss +179 -0
- package/demos/site/event-log/event-log.element.ts +134 -0
- package/demos/site/favicon.svg +14 -0
- package/demos/site/index.html +346 -0
- package/demos/site/index.ts +13 -0
- package/demos/site/message/message.element.scss +75 -0
- package/demos/site/message/message.element.ts +76 -0
- package/demos/site/og-image.png +0 -0
- package/demos/site/styles/site.css +1023 -0
- package/demos/site/styles/tokens.css +56 -0
- package/demos/site/toast/toast.element.scss +110 -0
- package/demos/site/toast/toast.element.ts +63 -0
- package/demos/todo-dashboard/index.html +141 -0
- package/demos/todo-dashboard/index.ts +4 -0
- package/demos/todo-dashboard/todo-dashboard.element.scss +1145 -0
- package/demos/todo-dashboard/todo-dashboard.element.ts +332 -0
- package/demos/todo-dashboard/todo-filters.element.ts +54 -0
- package/demos/todo-dashboard/todo-item.element.ts +126 -0
- package/demos/todo-dashboard/todo-stats.element.ts +189 -0
- package/package.json +73 -29
- package/src/custom-element.ts +206 -0
- package/{index.d.ts → src/index.ts} +2 -0
- package/src/listen.ts +70 -0
- package/src/prop.ts +92 -0
- package/src/state.ts +129 -0
- package/src/template-runtime.ts +435 -0
- package/src/toggle.ts +66 -0
- package/src/tsconfig.json +24 -0
- package/src/util.ts +33 -0
- package/src/watch.ts +14 -0
- package/tests/basic.spec.ts +70 -0
- package/tests/custom-element.spec.ts +77 -0
- package/tests/dispatch.spec.ts +52 -0
- package/tests/init.spec.ts +94 -0
- package/tests/listen.spec.ts +118 -0
- package/tests/prop.spec.ts +118 -0
- package/tests/templating-runtime.spec.ts +575 -0
- package/tests/toggle.spec.ts +92 -0
- package/tests/watch.spec.ts +183 -0
- package/tools/build.js +119 -0
- package/tools/bundle.js +167 -0
- package/tools/rollup-config.js +70 -0
- package/tools/start.js +188 -0
- package/tsconfig.json +38 -0
- package/vite.config.mts +30 -0
- package/bundles/custom-elements-ts.umd.js +0 -315
- package/bundles/custom-elements-ts.umd.js.map +0 -1
- package/custom-element.d.ts +0 -12
- package/esm2015/custom-elements-ts.js +0 -260
- package/esm2015/custom-elements-ts.js.map +0 -1
- package/esm5/custom-elements-ts.js +0 -298
- package/esm5/custom-elements-ts.js.map +0 -1
- package/listen.d.ts +0 -17
- package/prop.d.ts +0 -2
- package/toggle.d.ts +0 -1
- package/util.d.ts +0 -4
- package/watch.d.ts +0 -1
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
export type PrimitiveTemplateValue = string | number | boolean | null | undefined;
|
|
2
|
+
export type TemplateEventHandler = (event: any) => void;
|
|
3
|
+
|
|
4
|
+
export type TemplateValue =
|
|
5
|
+
| PrimitiveTemplateValue
|
|
6
|
+
| Node
|
|
7
|
+
| TemplateResult
|
|
8
|
+
| TemplateValue[]
|
|
9
|
+
| (() => TemplateValue)
|
|
10
|
+
| TemplateEventHandler
|
|
11
|
+
| EventListenerObject;
|
|
12
|
+
|
|
13
|
+
export interface TemplateResult {
|
|
14
|
+
readonly strings: TemplateStringsArray;
|
|
15
|
+
readonly values: TemplateValue[];
|
|
16
|
+
readonly __customElementsTsTemplateResult: true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TemplateInstance {
|
|
20
|
+
readonly strings: TemplateStringsArray;
|
|
21
|
+
readonly nodes: ChildNode[];
|
|
22
|
+
update(values: TemplateValue[]): void;
|
|
23
|
+
dispose(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Part {
|
|
27
|
+
update(value: TemplateValue): void;
|
|
28
|
+
dispose(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RenderState {
|
|
32
|
+
instance?: TemplateInstance;
|
|
33
|
+
part?: ChildPart;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ParsedTemplate {
|
|
37
|
+
html: string;
|
|
38
|
+
markers: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const templateCache = new WeakMap<TemplateStringsArray, ParsedTemplate>();
|
|
42
|
+
|
|
43
|
+
export const html = (
|
|
44
|
+
strings: TemplateStringsArray,
|
|
45
|
+
...values: TemplateValue[]
|
|
46
|
+
): TemplateResult => ({
|
|
47
|
+
strings,
|
|
48
|
+
values,
|
|
49
|
+
__customElementsTsTemplateResult: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const isTemplateResult = (value: unknown): value is TemplateResult => {
|
|
53
|
+
return Boolean(
|
|
54
|
+
value &&
|
|
55
|
+
typeof value === 'object' &&
|
|
56
|
+
(value as TemplateResult).__customElementsTsTemplateResult === true
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const renderIntoAnchor = (
|
|
61
|
+
value: TemplateValue,
|
|
62
|
+
anchor: Comment,
|
|
63
|
+
state: RenderState = {},
|
|
64
|
+
host?: unknown
|
|
65
|
+
): RenderState => {
|
|
66
|
+
if (isTemplateResult(value)) {
|
|
67
|
+
if (state.part) {
|
|
68
|
+
state.part.dispose();
|
|
69
|
+
state.part = undefined;
|
|
70
|
+
}
|
|
71
|
+
if (state.instance && state.instance.strings === value.strings) {
|
|
72
|
+
state.instance.update(value.values);
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
if (state.instance) {
|
|
76
|
+
state.instance.dispose();
|
|
77
|
+
}
|
|
78
|
+
state.instance = createTemplateInstance(value, host);
|
|
79
|
+
insertAfter(anchor, state.instance.nodes);
|
|
80
|
+
return state;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (state.instance) {
|
|
84
|
+
state.instance.dispose();
|
|
85
|
+
state.instance = undefined;
|
|
86
|
+
}
|
|
87
|
+
if (!state.part) {
|
|
88
|
+
state.part = new ChildPart(anchor, host);
|
|
89
|
+
}
|
|
90
|
+
state.part.update(value);
|
|
91
|
+
return state;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const createTemplateInstance = (result: TemplateResult, host?: unknown): TemplateInstance => {
|
|
95
|
+
const parsed = getParsedTemplate(result.strings);
|
|
96
|
+
const template = document.createElement('template');
|
|
97
|
+
template.innerHTML = parsed.html;
|
|
98
|
+
|
|
99
|
+
const fragment = document.importNode(template.content, true);
|
|
100
|
+
const parts = discoverParts(fragment, parsed.markers, host);
|
|
101
|
+
const nodes = Array.from(fragment.childNodes);
|
|
102
|
+
parts.forEach((part, index) => part.update(result.values[index]));
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
strings: result.strings,
|
|
106
|
+
nodes,
|
|
107
|
+
update(values: TemplateValue[]) {
|
|
108
|
+
parts.forEach((part, index) => part.update(values[index]));
|
|
109
|
+
},
|
|
110
|
+
dispose() {
|
|
111
|
+
parts.forEach((part) => part.dispose());
|
|
112
|
+
nodes.forEach((node) => node.parentNode?.removeChild(node));
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const getParsedTemplate = (strings: TemplateStringsArray): ParsedTemplate => {
|
|
118
|
+
const cached = templateCache.get(strings);
|
|
119
|
+
if (cached) {
|
|
120
|
+
return cached;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const markers: string[] = [];
|
|
124
|
+
let parsedHtml = '';
|
|
125
|
+
for (let index = 0; index < strings.length - 1; index++) {
|
|
126
|
+
parsedHtml += strings[index];
|
|
127
|
+
const marker = `__custom_elements_ts_marker_${index}__`;
|
|
128
|
+
markers.push(marker);
|
|
129
|
+
parsedHtml += isAttributePosition(strings[index]) ? marker : `<!--${marker}-->`;
|
|
130
|
+
}
|
|
131
|
+
parsedHtml += strings[strings.length - 1];
|
|
132
|
+
|
|
133
|
+
const parsed = { html: parsedHtml, markers };
|
|
134
|
+
templateCache.set(strings, parsed);
|
|
135
|
+
return parsed;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const isAttributePosition = (text: string): boolean => {
|
|
139
|
+
const lastOpen = text.lastIndexOf('<');
|
|
140
|
+
const lastClose = text.lastIndexOf('>');
|
|
141
|
+
if (lastOpen < lastClose) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return /[^\s<>"'=/]+\s*=\s*["']?$/.test(text);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const discoverParts = (fragment: DocumentFragment, markers: string[], host?: unknown): Part[] => {
|
|
148
|
+
const parts: Part[] = new Array(markers.length);
|
|
149
|
+
const markerToIndex = new Map(markers.map((marker, index) => [marker, index]));
|
|
150
|
+
const walker = document.createTreeWalker(
|
|
151
|
+
fragment,
|
|
152
|
+
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
let current = walker.nextNode();
|
|
156
|
+
while (current) {
|
|
157
|
+
if (current.nodeType === Node.COMMENT_NODE) {
|
|
158
|
+
const marker = current.nodeValue || '';
|
|
159
|
+
const index = markerToIndex.get(marker);
|
|
160
|
+
if (index !== undefined) {
|
|
161
|
+
parts[index] = new ChildPart(current as Comment, host);
|
|
162
|
+
}
|
|
163
|
+
} else if (current.nodeType === Node.ELEMENT_NODE) {
|
|
164
|
+
discoverAttributeParts(current as Element, markerToIndex, parts, host);
|
|
165
|
+
}
|
|
166
|
+
current = walker.nextNode();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return parts;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const discoverAttributeParts = (
|
|
173
|
+
element: Element,
|
|
174
|
+
markerToIndex: Map<string, number>,
|
|
175
|
+
parts: Part[],
|
|
176
|
+
host?: unknown
|
|
177
|
+
) => {
|
|
178
|
+
Array.from(element.attributes).forEach((attribute) => {
|
|
179
|
+
const index = markerToIndex.get(attribute.value);
|
|
180
|
+
if (index === undefined) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const name = attribute.name;
|
|
185
|
+
element.removeAttribute(name);
|
|
186
|
+
if (name.startsWith('@')) {
|
|
187
|
+
parts[index] = new EventPart(element, name.slice(1), host);
|
|
188
|
+
} else if (name.startsWith('.')) {
|
|
189
|
+
parts[index] = new PropertyPart(element, name.slice(1));
|
|
190
|
+
} else {
|
|
191
|
+
parts[index] = new AttributePart(element, name);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
class ChildPart implements Part {
|
|
197
|
+
private kind: 'empty' | 'text' | 'node' | 'template' | 'array' = 'empty';
|
|
198
|
+
private nodes: ChildNode[] = [];
|
|
199
|
+
private templateInstance?: TemplateInstance;
|
|
200
|
+
private arrayItems: Array<{ anchor: Comment; part: ChildPart }> = [];
|
|
201
|
+
|
|
202
|
+
constructor(
|
|
203
|
+
private anchor: Comment,
|
|
204
|
+
private host?: unknown
|
|
205
|
+
) {}
|
|
206
|
+
|
|
207
|
+
update(value: TemplateValue): void {
|
|
208
|
+
const resolved = resolveValue(value);
|
|
209
|
+
if (resolved === null || resolved === undefined || resolved === false) {
|
|
210
|
+
this.clear();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (Array.isArray(resolved)) {
|
|
214
|
+
this.updateArray(resolved);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (isTemplateResult(resolved)) {
|
|
218
|
+
this.updateTemplate(resolved);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (resolved instanceof Node) {
|
|
222
|
+
this.updateNode(resolved);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
this.updateText(String(resolved));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
dispose(): void {
|
|
229
|
+
this.clear();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private clear(): void {
|
|
233
|
+
this.templateInstance?.dispose();
|
|
234
|
+
this.templateInstance = undefined;
|
|
235
|
+
this.arrayItems.forEach((item) => {
|
|
236
|
+
item.part.dispose();
|
|
237
|
+
item.anchor.parentNode?.removeChild(item.anchor);
|
|
238
|
+
});
|
|
239
|
+
this.arrayItems = [];
|
|
240
|
+
this.nodes.forEach((node) => node.parentNode?.removeChild(node));
|
|
241
|
+
this.nodes = [];
|
|
242
|
+
this.kind = 'empty';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private updateText(value: string): void {
|
|
246
|
+
if (this.kind === 'text' && this.nodes[0]?.nodeType === Node.TEXT_NODE) {
|
|
247
|
+
if (this.nodes[0].nodeValue !== value) {
|
|
248
|
+
this.nodes[0].nodeValue = value;
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.clear();
|
|
253
|
+
const text = document.createTextNode(value);
|
|
254
|
+
insertAfter(this.anchor, [text]);
|
|
255
|
+
this.nodes = [text];
|
|
256
|
+
this.kind = 'text';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private updateNode(value: Node): void {
|
|
260
|
+
if (this.kind === 'node' && this.nodes[0] === value) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
this.clear();
|
|
264
|
+
insertAfter(this.anchor, [value as ChildNode]);
|
|
265
|
+
this.nodes = [value as ChildNode];
|
|
266
|
+
this.kind = 'node';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private updateTemplate(value: TemplateResult): void {
|
|
270
|
+
if (this.kind === 'template' && this.templateInstance?.strings === value.strings) {
|
|
271
|
+
this.templateInstance.update(value.values);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.clear();
|
|
275
|
+
this.templateInstance = createTemplateInstance(value, this.host);
|
|
276
|
+
insertAfter(this.anchor, this.templateInstance.nodes);
|
|
277
|
+
this.nodes = this.templateInstance.nodes;
|
|
278
|
+
this.kind = 'template';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private updateArray(values: TemplateValue[]): void {
|
|
282
|
+
if (this.kind !== 'array') {
|
|
283
|
+
this.clear();
|
|
284
|
+
this.kind = 'array';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
while (this.arrayItems.length > values.length) {
|
|
288
|
+
const item = this.arrayItems.pop()!;
|
|
289
|
+
item.part.dispose();
|
|
290
|
+
item.anchor.parentNode?.removeChild(item.anchor);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (let index = 0; index < values.length; index++) {
|
|
294
|
+
let item = this.arrayItems[index];
|
|
295
|
+
if (!item) {
|
|
296
|
+
const itemAnchor = document.createComment('custom-elements-ts-array-item');
|
|
297
|
+
insertAfter(this.getEndNode(), [itemAnchor]);
|
|
298
|
+
item = {
|
|
299
|
+
anchor: itemAnchor,
|
|
300
|
+
part: new ChildPart(itemAnchor, this.host),
|
|
301
|
+
};
|
|
302
|
+
this.arrayItems[index] = item;
|
|
303
|
+
}
|
|
304
|
+
item.part.update(values[index]);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private getEndNode(): ChildNode {
|
|
309
|
+
const lastItem = this.arrayItems[this.arrayItems.length - 1];
|
|
310
|
+
if (lastItem) {
|
|
311
|
+
return lastItem.part.getEndNode();
|
|
312
|
+
}
|
|
313
|
+
return this.nodes[this.nodes.length - 1] || this.anchor;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
class AttributePart implements Part {
|
|
318
|
+
private currentValue: TemplateValue | typeof noValue = noValue;
|
|
319
|
+
|
|
320
|
+
constructor(
|
|
321
|
+
private element: Element,
|
|
322
|
+
private name: string
|
|
323
|
+
) {}
|
|
324
|
+
|
|
325
|
+
update(value: TemplateValue): void {
|
|
326
|
+
const resolved = resolveValue(value);
|
|
327
|
+
if (Object.is(this.currentValue, resolved)) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.currentValue = resolved;
|
|
331
|
+
if (resolved === false || resolved === null || resolved === undefined) {
|
|
332
|
+
this.element.removeAttribute(this.name);
|
|
333
|
+
} else {
|
|
334
|
+
this.element.setAttribute(this.name, String(resolved));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
dispose(): void {
|
|
339
|
+
this.currentValue = noValue;
|
|
340
|
+
this.element.removeAttribute(this.name);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
class PropertyPart implements Part {
|
|
345
|
+
private currentValue: TemplateValue | typeof noValue = noValue;
|
|
346
|
+
|
|
347
|
+
constructor(
|
|
348
|
+
private element: Element,
|
|
349
|
+
private name: string
|
|
350
|
+
) {}
|
|
351
|
+
|
|
352
|
+
update(value: TemplateValue): void {
|
|
353
|
+
const resolved = resolveValue(value);
|
|
354
|
+
if (Object.is(this.currentValue, resolved)) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.currentValue = resolved;
|
|
358
|
+
(this.element as any)[this.name] = resolved === null || resolved === undefined ? '' : resolved;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
dispose(): void {
|
|
362
|
+
this.currentValue = noValue;
|
|
363
|
+
(this.element as any)[this.name] = '';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
class EventPart implements Part {
|
|
368
|
+
private currentValue: TemplateValue | typeof noValue = noValue;
|
|
369
|
+
private listener?: EventListenerOrEventListenerObject;
|
|
370
|
+
|
|
371
|
+
constructor(
|
|
372
|
+
private element: Element,
|
|
373
|
+
private eventName: string,
|
|
374
|
+
private host?: unknown
|
|
375
|
+
) {}
|
|
376
|
+
|
|
377
|
+
update(value: TemplateValue): void {
|
|
378
|
+
if (Object.is(this.currentValue, value)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
this.currentValue = value;
|
|
382
|
+
if (this.listener) {
|
|
383
|
+
this.element.removeEventListener(this.eventName, this.listener);
|
|
384
|
+
this.listener = undefined;
|
|
385
|
+
}
|
|
386
|
+
if (typeof value === 'function') {
|
|
387
|
+
const handler = value as unknown as (event: Event) => void;
|
|
388
|
+
this.listener = ((event: Event) =>
|
|
389
|
+
handler.call(this.host || this.element, event)) as EventListener;
|
|
390
|
+
this.element.addEventListener(this.eventName, this.listener);
|
|
391
|
+
} else if (isEventListenerObject(value)) {
|
|
392
|
+
this.listener = value;
|
|
393
|
+
this.element.addEventListener(this.eventName, this.listener);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
dispose(): void {
|
|
398
|
+
this.currentValue = noValue;
|
|
399
|
+
if (this.listener) {
|
|
400
|
+
this.element.removeEventListener(this.eventName, this.listener);
|
|
401
|
+
this.listener = undefined;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const resolveValue = (value: TemplateValue): TemplateValue => {
|
|
407
|
+
if (typeof value === 'function' && value.length === 0) {
|
|
408
|
+
return (value as () => TemplateValue)();
|
|
409
|
+
}
|
|
410
|
+
return value;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const isEventListenerObject = (value: unknown): value is EventListenerObject => {
|
|
414
|
+
return Boolean(
|
|
415
|
+
value &&
|
|
416
|
+
typeof value === 'object' &&
|
|
417
|
+
typeof (value as EventListenerObject).handleEvent === 'function'
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const noValue = Symbol('custom-elements-ts-no-value');
|
|
422
|
+
|
|
423
|
+
const insertAfter = (anchor: ChildNode, nodes: ChildNode[]) => {
|
|
424
|
+
let reference = anchor.nextSibling;
|
|
425
|
+
const parent = anchor.parentNode;
|
|
426
|
+
if (!parent) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
nodes.forEach((node) => {
|
|
430
|
+
parent.insertBefore(node, reference);
|
|
431
|
+
reference = node.nextSibling;
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
export { ChildPart };
|
package/src/toggle.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { toKebabCase } from './util';
|
|
2
|
+
|
|
3
|
+
export const Toggle = (): any => {
|
|
4
|
+
return (target: any, propName: any) => {
|
|
5
|
+
function get(this: any) {
|
|
6
|
+
const getAttribute = (attrName: string) => {
|
|
7
|
+
if (this.hasAttribute(attrName)) {
|
|
8
|
+
const attrValue = this.getAttribute(attrName);
|
|
9
|
+
if (/^(true|false|^$)$/.test(attrValue)) {
|
|
10
|
+
return attrValue === 'true' || attrValue === '';
|
|
11
|
+
} else {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
};
|
|
17
|
+
return getAttribute(propName);
|
|
18
|
+
}
|
|
19
|
+
function set(this: any, value: any) {
|
|
20
|
+
const oldValue = value;
|
|
21
|
+
if (value !== null && value !== undefined) {
|
|
22
|
+
switch (typeof value) {
|
|
23
|
+
case 'boolean':
|
|
24
|
+
break;
|
|
25
|
+
case 'string':
|
|
26
|
+
if (/^(true|false|^$)$/.test(value)) {
|
|
27
|
+
value = oldValue === 'true' || oldValue === '';
|
|
28
|
+
} else {
|
|
29
|
+
console.warn(
|
|
30
|
+
`TypeError: Cannot set boolean toggle property '${propName}' to '${value}'`
|
|
31
|
+
);
|
|
32
|
+
value = false;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
throw new TypeError(`Cannot set boolean toggle property '${propName}' to '${value}'`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (this.__connected) {
|
|
40
|
+
const previous = this.props[propName];
|
|
41
|
+
this.props[propName] = value || false;
|
|
42
|
+
if (oldValue !== '' && oldValue !== null) {
|
|
43
|
+
this.setAttribute(propName, value);
|
|
44
|
+
} else {
|
|
45
|
+
if (value) {
|
|
46
|
+
this.setAttribute(propName, '');
|
|
47
|
+
} else {
|
|
48
|
+
this.removeAttribute(propName);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!Object.is(previous, this.props[propName])) {
|
|
52
|
+
this.__scheduleRender?.();
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
if (!this.hasAttribute(toKebabCase(propName))) {
|
|
56
|
+
this.constructor.propsInit[propName] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!target.constructor.propsInit) {
|
|
61
|
+
target.constructor.propsInit = {};
|
|
62
|
+
}
|
|
63
|
+
target.constructor.propsInit[propName] = null;
|
|
64
|
+
Object.defineProperty(target, propName, { get, set });
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowSyntheticDefaultImports": true,
|
|
4
|
+
"allowUnreachableCode": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"experimentalDecorators": true,
|
|
7
|
+
"lib": ["es5", "es6", "dom"],
|
|
8
|
+
"module": "es2015",
|
|
9
|
+
"moduleResolution": "node",
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true,
|
|
12
|
+
"outDir": "../dist",
|
|
13
|
+
"removeComments": true,
|
|
14
|
+
"target": "es5",
|
|
15
|
+
"strict": true,
|
|
16
|
+
"noImplicitAny": true,
|
|
17
|
+
"strictNullChecks": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"./*.ts"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const toKebabCase = (str: string) => {
|
|
2
|
+
return str
|
|
3
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
4
|
+
.replace(/[\s_]+/g, '-')
|
|
5
|
+
.toLowerCase();
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const toCamelCase = (str: string) => {
|
|
9
|
+
return str.toLowerCase().replace(/(-\w)/g, (m: string) => m[1].toUpperCase());
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const toDotCase = (str: string) => {
|
|
13
|
+
return str
|
|
14
|
+
.replace(/(?!^)([A-Z])/g, ' $1')
|
|
15
|
+
.replace(/[_\s]+(?=[a-zA-Z])/g, '.')
|
|
16
|
+
.toLowerCase();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const tryParseInt = (value: unknown) => {
|
|
20
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === 'string') {
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
if (trimmed !== '') {
|
|
26
|
+
const parsed = Number(trimmed);
|
|
27
|
+
if (Number.isInteger(parsed) && String(parsed) === trimmed) {
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
};
|
package/src/watch.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { toKebabCase } from './util';
|
|
2
|
+
|
|
3
|
+
export const Watch = (attrName: string) => {
|
|
4
|
+
return (target: any, propertyName: string) => {
|
|
5
|
+
if (!target.constructor.watchAttributes) {
|
|
6
|
+
target.constructor.watchAttributes = {};
|
|
7
|
+
}
|
|
8
|
+
target.constructor.watchAttributes[toKebabCase(attrName)] = propertyName;
|
|
9
|
+
if (!target.constructor.propsInit) {
|
|
10
|
+
target.constructor.propsInit = {};
|
|
11
|
+
}
|
|
12
|
+
target.constructor.propsInit[attrName] = null;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CustomElement } from 'custom-elements-ts';
|
|
3
|
+
|
|
4
|
+
@CustomElement({
|
|
5
|
+
tag: 'basic-element',
|
|
6
|
+
template: '<span>my element</span>',
|
|
7
|
+
style: ':host{border:0}',
|
|
8
|
+
})
|
|
9
|
+
class BasicElement extends HTMLElement {}
|
|
10
|
+
|
|
11
|
+
describe('basic test', () => {
|
|
12
|
+
let myElementInstance: any;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
const myElement = document.createElement('basic-element');
|
|
16
|
+
myElementInstance = document.body.appendChild(myElement);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
document.body.innerHTML = '';
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should load html template', () => {
|
|
24
|
+
expect(myElementInstance.shadowRoot.innerHTML).toContain('<span>my element</span>');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should load css', () => {
|
|
28
|
+
expect(myElementInstance.shadowRoot.querySelector('style').innerText).toContain(
|
|
29
|
+
':host{border:0}'
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should have shadowroot', () => {
|
|
34
|
+
expect(myElementInstance.shadowRoot).toBeTruthy();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
39
|
+
@CustomElement({
|
|
40
|
+
tag: 'shadow-false-element',
|
|
41
|
+
template: '<span>my element</span>',
|
|
42
|
+
style: ':host{border:0}',
|
|
43
|
+
shadow: false,
|
|
44
|
+
})
|
|
45
|
+
class ShadowFalseElement extends HTMLElement {}
|
|
46
|
+
|
|
47
|
+
describe('basic test no shadowroot', () => {
|
|
48
|
+
let myElementInstance: any;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
const myElement = document.createElement('shadow-false-element');
|
|
52
|
+
myElementInstance = document.body.appendChild(myElement);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
document.body.innerHTML = '';
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should load html template', () => {
|
|
60
|
+
expect(myElementInstance.innerHTML).toContain('<span>my element</span>');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should have css', () => {
|
|
64
|
+
expect(myElementInstance.querySelector('style')).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('shadow not have a shadowroot', () => {
|
|
68
|
+
expect(myElementInstance.shadowRoot).toBeFalsy();
|
|
69
|
+
});
|
|
70
|
+
});
|