~repos /atoms-element
git clone https://pyrossh.dev/repos/atoms-element.git
A simple web component library for defining your custom elements. It works on both client and server.
1bdc5ec4
—
pyros2097 4 years ago
Merge pull request #1 from pyros2097/class_based_atom
- element.d.ts +47 -0
- element.js +451 -0
- element.test.js +366 -0
- example/app-counter.js +91 -0
- example/index.js +38 -0
- example/main.js +0 -30
- example/server.js +45 -0
- example/styles.css +567 -0
- src/lit-html.js → lit-html.js +0 -0
- package-lock.json +2 -2
- package.json +9 -13
- page.d.ts +26 -0
- page.js +61 -0
- page.test.js +85 -0
- readme.md +84 -22
- src/index.d.ts +0 -60
- src/index.js +0 -497
- src/ssr.js +0 -24
- test/index.test.js +0 -266
element.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Config } from './page';
|
|
2
|
+
|
|
3
|
+
export declare type Location = {
|
|
4
|
+
readonly ancestorOrigins: DOMStringList;
|
|
5
|
+
hash: string;
|
|
6
|
+
host: string;
|
|
7
|
+
hostname: string;
|
|
8
|
+
href: string;
|
|
9
|
+
readonly origin: string;
|
|
10
|
+
pathname: string;
|
|
11
|
+
port: string;
|
|
12
|
+
protocol: string;
|
|
13
|
+
search: string;
|
|
14
|
+
assign: (url: string | URL) => void;
|
|
15
|
+
reload: () => void;
|
|
16
|
+
replace: (url: string | URL) => void;
|
|
17
|
+
toString: () => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default class AtomsElement {
|
|
21
|
+
static register(): () => void;
|
|
22
|
+
static observedAttributes: Array<string>;
|
|
23
|
+
static getElement: (name: string) => AtomsElement | undefined;
|
|
24
|
+
config: Config;
|
|
25
|
+
location: Location;
|
|
26
|
+
attrs: {[key: string]: any};
|
|
27
|
+
state: {[key: string]: any};
|
|
28
|
+
computed: {[key: string]: any};
|
|
29
|
+
styles: () => string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// declare type Colors = 'red' | 'purple' | 'blue' | 'green';
|
|
33
|
+
// declare type Luminance = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
|
|
34
|
+
// declare type BgColor = `bg-${Colors}-${Luminance}`;
|
|
35
|
+
// declare type Distance = 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
|
|
36
|
+
// declare type Breakpoints = 'xs:' | 'sm:' | 'md:' | 'lg:' | 'xl:' | '';
|
|
37
|
+
// declare type Space = `${Breakpoints}space-${'x' | 'y'}-${Distance}`;
|
|
38
|
+
// declare type ValidClass = Space | BgColor;
|
|
39
|
+
// declare type Tailwind<S> = S extends `${infer Class} ${infer Rest}` ? Class extends ValidClass ? `${Class} ${Tailwind<Rest>}` : never : S extends `${infer Class}` ? Class extends ValidClass ? S : never : never;
|
|
40
|
+
// declare function doSomethingWithTwClass<S>(cls: Tailwind<S>): Tailwind<S>;
|
|
41
|
+
// declare const bad: never;
|
|
42
|
+
// declare const bad2: never;
|
|
43
|
+
// declare const bad3: never;
|
|
44
|
+
// declare const bad4: never;
|
|
45
|
+
// declare const good: "bg-red-400 space-x-4 md:space-x-8";
|
|
46
|
+
// declare const good2: "bg-red-400 space-x-4";
|
|
47
|
+
// declare const good3: "space-x-1.5 bg-blue-200";
|
element.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { html, render as litRender, directive, NodePart, AttributePart, PropertyPart, isPrimitive } from './lit-html.js';
|
|
2
|
+
|
|
3
|
+
const isBrowser = typeof window !== 'undefined';
|
|
4
|
+
export { html, isBrowser };
|
|
5
|
+
|
|
6
|
+
// Taken from emotion
|
|
7
|
+
const murmur2 = (str) => {
|
|
8
|
+
var h = 0;
|
|
9
|
+
var k,
|
|
10
|
+
i = 0,
|
|
11
|
+
len = str.length;
|
|
12
|
+
for (; len >= 4; ++i, len -= 4) {
|
|
13
|
+
k = (str.charCodeAt(i) & 0xff) | ((str.charCodeAt(++i) & 0xff) << 8) | ((str.charCodeAt(++i) & 0xff) << 16) | ((str.charCodeAt(++i) & 0xff) << 24);
|
|
14
|
+
k = (k & 0xffff) * 0x5bd1e995 + (((k >>> 16) * 0xe995) << 16);
|
|
15
|
+
k ^= k >>> 24;
|
|
16
|
+
h = ((k & 0xffff) * 0x5bd1e995 + (((k >>> 16) * 0xe995) << 16)) ^ ((h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0xe995) << 16));
|
|
17
|
+
}
|
|
18
|
+
switch (len) {
|
|
19
|
+
case 3:
|
|
20
|
+
h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
|
|
21
|
+
case 2:
|
|
22
|
+
h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
|
|
23
|
+
case 1:
|
|
24
|
+
h ^= str.charCodeAt(i) & 0xff;
|
|
25
|
+
h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0xe995) << 16);
|
|
26
|
+
}
|
|
27
|
+
h ^= h >>> 13;
|
|
28
|
+
h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0xe995) << 16);
|
|
29
|
+
return ((h ^ (h >>> 15)) >>> 0).toString(36);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const hyphenate = (s) => s.replace(/[A-Z]|^ms/g, '-$&').toLowerCase();
|
|
33
|
+
|
|
34
|
+
export const convertStyles = (prefix, obj, parentClassName, indent = '') => {
|
|
35
|
+
const className = parentClassName || prefix + '-' + murmur2(JSON.stringify(obj)).toString(36);
|
|
36
|
+
const cssText = Object.keys(obj).reduce((acc, key) => {
|
|
37
|
+
const value = obj[key];
|
|
38
|
+
if (typeof value === 'object') {
|
|
39
|
+
acc += '\n ' + indent + convertStyles(prefix, value, key, indent + ' ').cssText;
|
|
40
|
+
} else {
|
|
41
|
+
acc += ' ' + indent + hyphenate(key) + ': ' + value + ';\n';
|
|
42
|
+
}
|
|
43
|
+
return acc;
|
|
44
|
+
}, `${parentClassName ? '' : '.'}${className} {\n`);
|
|
45
|
+
return { className, cssText: cssText + `\n${indent}}` };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const css = (obj) => {
|
|
49
|
+
Object.keys(obj).forEach((key) => {
|
|
50
|
+
const value = obj[key];
|
|
51
|
+
const { className, cssText } = convertStyles(key, value);
|
|
52
|
+
obj[key] = className;
|
|
53
|
+
obj[className] = cssText;
|
|
54
|
+
});
|
|
55
|
+
obj.toString = () => {
|
|
56
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
57
|
+
acc += key.includes('-') ? obj[key] + '\n\n' : '';
|
|
58
|
+
return acc;
|
|
59
|
+
}, '');
|
|
60
|
+
};
|
|
61
|
+
return obj;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const lastAttributeNameRegex =
|
|
65
|
+
/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
|
|
66
|
+
|
|
67
|
+
const wrapAttribute = (attrName, suffix, text, v) => {
|
|
68
|
+
let buffer = text;
|
|
69
|
+
const hasQuote = suffix && suffix.includes(`="`);
|
|
70
|
+
if (attrName && !hasQuote) {
|
|
71
|
+
buffer += `"`;
|
|
72
|
+
}
|
|
73
|
+
buffer += v;
|
|
74
|
+
if (attrName && !hasQuote) {
|
|
75
|
+
buffer += `"`;
|
|
76
|
+
}
|
|
77
|
+
return buffer;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const render = isBrowser
|
|
81
|
+
? litRender
|
|
82
|
+
: (template) => {
|
|
83
|
+
let js = '';
|
|
84
|
+
template.strings.forEach((text, i) => {
|
|
85
|
+
const value = template.values[i];
|
|
86
|
+
const type = typeof value;
|
|
87
|
+
let attrName, suffix;
|
|
88
|
+
const matchName = lastAttributeNameRegex.exec(text);
|
|
89
|
+
if (matchName) {
|
|
90
|
+
attrName = matchName[2];
|
|
91
|
+
suffix = matchName[3];
|
|
92
|
+
}
|
|
93
|
+
if (value === null || !(type === 'object' || type === 'function' || type === 'undefined')) {
|
|
94
|
+
js += wrapAttribute(attrName, suffix, text, type !== 'string' ? String(value) : value);
|
|
95
|
+
} else if (Array.isArray(value) && value.find((item) => item && item.strings && item.type === 'html')) {
|
|
96
|
+
js += text;
|
|
97
|
+
value.forEach((v) => {
|
|
98
|
+
js += render(v);
|
|
99
|
+
});
|
|
100
|
+
} else if (type === 'object') {
|
|
101
|
+
// TemplateResult
|
|
102
|
+
if (value.strings && value.type === 'html') {
|
|
103
|
+
js += text;
|
|
104
|
+
js += render(value);
|
|
105
|
+
} else {
|
|
106
|
+
js += wrapAttribute(attrName, suffix, text, JSON.stringify(value).replace(/"/g, `'`));
|
|
107
|
+
}
|
|
108
|
+
} else if (type == 'function') {
|
|
109
|
+
if (attrName) {
|
|
110
|
+
js += text.replace(' ' + attrName + '=', '');
|
|
111
|
+
} else {
|
|
112
|
+
// js += text;
|
|
113
|
+
// js += value();
|
|
114
|
+
}
|
|
115
|
+
} else if (type !== 'undefined') {
|
|
116
|
+
js += text;
|
|
117
|
+
js += value.toString();
|
|
118
|
+
} else {
|
|
119
|
+
js += text;
|
|
120
|
+
// console.log('value', value);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return js;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const previousValues = new WeakMap();
|
|
127
|
+
export const unsafeHTML = isBrowser
|
|
128
|
+
? directive((value) => (part) => {
|
|
129
|
+
if (!(part instanceof NodePart)) {
|
|
130
|
+
throw new Error('unsafeHTML can only be used in text bindings');
|
|
131
|
+
}
|
|
132
|
+
const previousValue = previousValues.get(part);
|
|
133
|
+
if (previousValue !== undefined && isPrimitive(value) && value === previousValue.value && part.value === previousValue.fragment) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const template = document.createElement('template');
|
|
137
|
+
template.innerHTML = value; // innerHTML casts to string internally
|
|
138
|
+
const fragment = document.importNode(template.content, true);
|
|
139
|
+
part.setValue(fragment);
|
|
140
|
+
previousValues.set(part, { value, fragment });
|
|
141
|
+
})
|
|
142
|
+
: (value) => value;
|
|
143
|
+
|
|
144
|
+
const previousClassesCache = new WeakMap();
|
|
145
|
+
export const classMap = isBrowser
|
|
146
|
+
? directive((classInfo) => (part) => {
|
|
147
|
+
if (!(part instanceof AttributePart) || part instanceof PropertyPart || part.committer.name !== 'class' || part.committer.parts.length > 1) {
|
|
148
|
+
throw new Error('The `classMap` directive must be used in the `class` attribute ' + 'and must be the only part in the attribute.');
|
|
149
|
+
}
|
|
150
|
+
const { committer } = part;
|
|
151
|
+
const { element } = committer;
|
|
152
|
+
let previousClasses = previousClassesCache.get(part);
|
|
153
|
+
if (previousClasses === undefined) {
|
|
154
|
+
// Write static classes once
|
|
155
|
+
// Use setAttribute() because className isn't a string on SVG elements
|
|
156
|
+
element.setAttribute('class', committer.strings.join(' '));
|
|
157
|
+
previousClassesCache.set(part, (previousClasses = new Set()));
|
|
158
|
+
}
|
|
159
|
+
const classList = element.classList;
|
|
160
|
+
// Remove old classes that no longer apply
|
|
161
|
+
// We use forEach() instead of for-of so that re don't require down-level
|
|
162
|
+
// iteration.
|
|
163
|
+
previousClasses.forEach((name) => {
|
|
164
|
+
if (!(name in classInfo)) {
|
|
165
|
+
classList.remove(name);
|
|
166
|
+
previousClasses.delete(name);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Add or remove classes based on their classMap value
|
|
170
|
+
for (const name in classInfo) {
|
|
171
|
+
const value = classInfo[name];
|
|
172
|
+
if (value != previousClasses.has(name)) {
|
|
173
|
+
// We explicitly want a loose truthy check of `value` because it seems
|
|
174
|
+
// more convenient that '' and 0 are skipped.
|
|
175
|
+
if (value) {
|
|
176
|
+
classList.add(name);
|
|
177
|
+
previousClasses.add(name);
|
|
178
|
+
} else {
|
|
179
|
+
classList.remove(name);
|
|
180
|
+
previousClasses.delete(name);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (typeof classList.commit === 'function') {
|
|
185
|
+
classList.commit();
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
: (classes) => {
|
|
189
|
+
let value = '';
|
|
190
|
+
for (const key in classes) {
|
|
191
|
+
if (classes[key]) {
|
|
192
|
+
value += `${value.length ? ' ' : ''}${key}`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const logError = (msg) => {
|
|
199
|
+
if (isBrowser ? window.__DEV__ : global.__DEV) {
|
|
200
|
+
console.warn(msg);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const validator = (type, validate) => (innerType) => {
|
|
205
|
+
const isPrimitiveType = ['number', 'string', 'boolean'].includes(type);
|
|
206
|
+
const common = {
|
|
207
|
+
type: type,
|
|
208
|
+
parse: isPrimitiveType ? (attr) => attr : (attr) => (attr ? JSON.parse(attr.replace(/'/g, `"`)) : null),
|
|
209
|
+
validate: (context, data) => {
|
|
210
|
+
if (data === null || typeof data === 'undefined') {
|
|
211
|
+
if (common.__required) {
|
|
212
|
+
logError(`'${context}' Field is required`);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!isPrimitiveType) {
|
|
217
|
+
validate(innerType, context, data);
|
|
218
|
+
} else {
|
|
219
|
+
const dataType = typeof data;
|
|
220
|
+
if (dataType !== type) {
|
|
221
|
+
logError(`'${context}' Expected type '${type}' got type '${dataType}'`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
common.required = () => {
|
|
227
|
+
common.__required = true;
|
|
228
|
+
return common;
|
|
229
|
+
};
|
|
230
|
+
common.default = (fnOrValue) => {
|
|
231
|
+
common.__default = fnOrValue;
|
|
232
|
+
return common;
|
|
233
|
+
};
|
|
234
|
+
common.compute = (...args) => {
|
|
235
|
+
const fn = args[args.length - 1];
|
|
236
|
+
const deps = args.slice(0, args.length - 1);
|
|
237
|
+
common.__compute = {
|
|
238
|
+
fn,
|
|
239
|
+
deps,
|
|
240
|
+
};
|
|
241
|
+
return common;
|
|
242
|
+
};
|
|
243
|
+
return common;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export const number = validator('number');
|
|
247
|
+
export const string = validator('string');
|
|
248
|
+
export const boolean = validator('boolean');
|
|
249
|
+
export const object = validator('object', (innerType, context, data) => {
|
|
250
|
+
if (data.constructor !== Object) {
|
|
251
|
+
logError(`'${context}' Expected object literal '{}' got '${typeof data}'`);
|
|
252
|
+
}
|
|
253
|
+
for (const key of Object.keys(innerType)) {
|
|
254
|
+
const fieldValidator = innerType[key];
|
|
255
|
+
const item = data[key];
|
|
256
|
+
fieldValidator.validate(`${context}.${key}`, item);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
export const array = validator('array', (innerType, context, data) => {
|
|
260
|
+
if (!Array.isArray(data)) {
|
|
261
|
+
logError(`Expected Array got ${data}`);
|
|
262
|
+
}
|
|
263
|
+
for (let i = 0; i < data.length; i++) {
|
|
264
|
+
const item = data[i];
|
|
265
|
+
innerType.validate(`${context}[${i}]`, item);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const fifo = (q) => q.shift();
|
|
270
|
+
const filo = (q) => q.pop();
|
|
271
|
+
const microtask = (flush) => () => queueMicrotask(flush);
|
|
272
|
+
const task = (flush) => {
|
|
273
|
+
if (isBrowser) {
|
|
274
|
+
const ch = new window.MessageChannel();
|
|
275
|
+
ch.port1.onmessage = flush;
|
|
276
|
+
return () => ch.port2.postMessage(null);
|
|
277
|
+
} else {
|
|
278
|
+
return () => setImmediate(flush);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const registry = {};
|
|
283
|
+
const BaseElement = isBrowser ? window.HTMLElement : class {};
|
|
284
|
+
|
|
285
|
+
export class AtomsElement extends BaseElement {
|
|
286
|
+
static register() {
|
|
287
|
+
registry[this.name] = this;
|
|
288
|
+
if (isBrowser) {
|
|
289
|
+
if (window.customElements.get(this.name)) {
|
|
290
|
+
return;
|
|
291
|
+
} else {
|
|
292
|
+
window.customElements.define(this.name, registry[this.name]);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
static getElement(name) {
|
|
298
|
+
return registry[name];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
static get observedAttributes() {
|
|
302
|
+
if (!this.attrTypes) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
return Object.keys(this.attrTypes).map((k) => k.toLowerCase());
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
constructor(ssrAttributes) {
|
|
309
|
+
super();
|
|
310
|
+
this._dirty = false;
|
|
311
|
+
this._connected = false;
|
|
312
|
+
this._state = {};
|
|
313
|
+
this.ssrAttributes = ssrAttributes;
|
|
314
|
+
this.config = isBrowser ? window.config : global.config;
|
|
315
|
+
this.location = isBrowser ? window.location : global.location;
|
|
316
|
+
this.stylesMounted = false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
connectedCallback() {
|
|
320
|
+
this._connected = true;
|
|
321
|
+
if (isBrowser) {
|
|
322
|
+
this.update();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
disconnectedCallback() {
|
|
326
|
+
this._connected = false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
attributeChangedCallback(key, oldValue, newValue) {
|
|
330
|
+
if (this._connected) {
|
|
331
|
+
this.update();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
update() {
|
|
336
|
+
if (this._dirty) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this._dirty = true;
|
|
340
|
+
this.enqueueUpdate();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
_performUpdate() {
|
|
344
|
+
if (!this._connected) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.renderTemplate();
|
|
348
|
+
this._dirty = false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
batch(runner, pick, callback) {
|
|
352
|
+
const q = [];
|
|
353
|
+
const flush = () => {
|
|
354
|
+
let p;
|
|
355
|
+
while ((p = pick(q))) callback(p);
|
|
356
|
+
};
|
|
357
|
+
const run = runner(flush);
|
|
358
|
+
q.push(this) === 1 && run();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
enqueueUpdate() {
|
|
362
|
+
this.batch(microtask, fifo, () => this._performUpdate());
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
get attrs() {
|
|
366
|
+
return Object.keys(this.constructor.attrTypes).reduceRight((acc, key) => {
|
|
367
|
+
const attrType = this.constructor.attrTypes[key];
|
|
368
|
+
const newValue = isBrowser ? this.getAttribute(key.toLowerCase()) : this.ssrAttributes.find((item) => item.name === key.toLowerCase())?.value;
|
|
369
|
+
const data = attrType.parse(newValue);
|
|
370
|
+
attrType.validate(`<${this.constructor.name}> ${key}`, data);
|
|
371
|
+
acc[key] = data;
|
|
372
|
+
return acc;
|
|
373
|
+
}, {});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
get state() {
|
|
377
|
+
return Object.keys(this.constructor.stateTypes).reduceRight((acc, key) => {
|
|
378
|
+
const stateType = this.constructor.stateTypes[key];
|
|
379
|
+
if (!this._state[key] && typeof stateType.__default !== 'undefined') {
|
|
380
|
+
this._state[key] = typeof stateType.__default === 'function' ? stateType.__default(this.attrs, this._state) : stateType.__default;
|
|
381
|
+
}
|
|
382
|
+
acc[key] = this._state[key];
|
|
383
|
+
acc[`set${key[0].toUpperCase()}${key.slice(1)}`] = (v) => {
|
|
384
|
+
// TODO: check type on set
|
|
385
|
+
this._state[key] = typeof v === 'function' ? v(this._state[key]) : v;
|
|
386
|
+
this.update();
|
|
387
|
+
};
|
|
388
|
+
return acc;
|
|
389
|
+
}, {});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
get computed() {
|
|
393
|
+
return Object.keys(this.constructor.computedTypes).reduceRight((acc, key) => {
|
|
394
|
+
const type = this.constructor.computedTypes[key];
|
|
395
|
+
const state = this.state;
|
|
396
|
+
const values = type.__compute.deps.reduce((acc, key) => {
|
|
397
|
+
if (typeof state[key] !== undefined) {
|
|
398
|
+
acc.push(state[key]);
|
|
399
|
+
}
|
|
400
|
+
return acc;
|
|
401
|
+
}, []);
|
|
402
|
+
acc[key] = type.__compute.fn(...values);
|
|
403
|
+
return acc;
|
|
404
|
+
}, {});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
renderTemplate() {
|
|
408
|
+
const template = this.render();
|
|
409
|
+
const result = render(template, this);
|
|
410
|
+
if (isBrowser) {
|
|
411
|
+
if (!this.stylesMounted) {
|
|
412
|
+
this.appendChild(document.createElement('style')).textContent = this.constructor.styles.toString();
|
|
413
|
+
this.stylesMounted = true;
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
return `
|
|
417
|
+
${result}
|
|
418
|
+
<style>
|
|
419
|
+
${this.constructor.styles.toString()}
|
|
420
|
+
</style>
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export const createElement = ({ name, attrTypes, stateTypes, computedTypes, styles, render }) => {
|
|
427
|
+
const Element = class extends AtomsElement {
|
|
428
|
+
static name = name();
|
|
429
|
+
|
|
430
|
+
static attrTypes = attrTypes();
|
|
431
|
+
|
|
432
|
+
static stateTypes = stateTypes();
|
|
433
|
+
|
|
434
|
+
static computedTypes = computedTypes();
|
|
435
|
+
|
|
436
|
+
static styles = styles;
|
|
437
|
+
|
|
438
|
+
constructor(ssrAttributes) {
|
|
439
|
+
super(ssrAttributes);
|
|
440
|
+
}
|
|
441
|
+
render() {
|
|
442
|
+
return render({
|
|
443
|
+
attrs: this.attrs,
|
|
444
|
+
state: this.state,
|
|
445
|
+
computed: this.computed,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
Element.register();
|
|
450
|
+
return { name, attrTypes, stateTypes, computedTypes, styles, render };
|
|
451
|
+
};
|
element.test.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { expect, test, jest } from '@jest/globals';
|
|
2
|
+
import { AtomsElement, html, render, number, boolean, string, array, object, unsafeHTML, css, classMap } from './element.js';
|
|
3
|
+
|
|
4
|
+
global.__DEV = true;
|
|
5
|
+
|
|
6
|
+
const primitives = [
|
|
7
|
+
{
|
|
8
|
+
type: 'number',
|
|
9
|
+
valid: [123, 40.5, 6410],
|
|
10
|
+
invalid: ['123', false, {}, [], new Date()],
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
type: 'boolean',
|
|
14
|
+
valid: [false, true],
|
|
15
|
+
invalid: ['123', 123, {}, [], new Date()],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: 'string',
|
|
19
|
+
valid: ['', '123'],
|
|
20
|
+
invalid: [123, false, {}, [], new Date()],
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
const primitiveTypes = {
|
|
24
|
+
number: number,
|
|
25
|
+
boolean: boolean,
|
|
26
|
+
string: string,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
primitives.forEach((value) =>
|
|
30
|
+
it(`${value.type}`, () => {
|
|
31
|
+
const spy = jest.spyOn(global.console, 'warn').mockImplementation();
|
|
32
|
+
const context = 'key';
|
|
33
|
+
const validator = primitiveTypes[value.type]();
|
|
34
|
+
const validatorReq = primitiveTypes[value.type]().required();
|
|
35
|
+
expect(validator.type).toEqual(value.type);
|
|
36
|
+
expect(validator.__required).toEqual(undefined);
|
|
37
|
+
expect(validatorReq.__required).toEqual(true);
|
|
38
|
+
validator.validate(context);
|
|
39
|
+
for (const v of value.valid) {
|
|
40
|
+
validator.validate(context, v);
|
|
41
|
+
validatorReq.validate(context, v);
|
|
42
|
+
}
|
|
43
|
+
validatorReq.validate(context);
|
|
44
|
+
expect(console.warn).toHaveBeenCalledWith(`'key' Field is required`);
|
|
45
|
+
for (const v of value.invalid) {
|
|
46
|
+
validator.validate(context, v);
|
|
47
|
+
expect(console.warn).toHaveBeenCalledWith(`'key' Expected type '${value.type}' got type '${typeof v}'`);
|
|
48
|
+
}
|
|
49
|
+
spy.mockRestore();
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
test('object', () => {
|
|
54
|
+
const spy = jest.spyOn(global.console, 'warn').mockImplementation();
|
|
55
|
+
const context = 'data';
|
|
56
|
+
object({}).validate(context, { name: '123' });
|
|
57
|
+
object({ name: string() }).validate(context, { name: '123' });
|
|
58
|
+
object({ name: string().required() }).validate(context, { name: '' });
|
|
59
|
+
object({ name: string().required() }).validate(context, {});
|
|
60
|
+
expect(console.warn).toHaveBeenCalledWith(`'data.name' Field is required`);
|
|
61
|
+
|
|
62
|
+
const schema = object({
|
|
63
|
+
address: object({
|
|
64
|
+
street: string(),
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
schema.validate(context, {});
|
|
68
|
+
schema.validate(context, '123');
|
|
69
|
+
expect(console.warn).toHaveBeenCalledWith(`'data' Expected object literal '{}' got 'string'`);
|
|
70
|
+
schema.validate(context, {
|
|
71
|
+
address: {},
|
|
72
|
+
});
|
|
73
|
+
schema.validate(context, {
|
|
74
|
+
address: '123',
|
|
75
|
+
});
|
|
76
|
+
expect(console.warn).toHaveBeenCalledWith(`'data.address' Expected object literal '{}' got 'string'`);
|
|
77
|
+
schema.validate(context, {
|
|
78
|
+
address: {
|
|
79
|
+
street: 'avenue 1',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
schema.validate(context, {
|
|
83
|
+
address: {
|
|
84
|
+
street: false,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
expect(console.warn).toHaveBeenCalledWith(`'data.address.street' Expected type 'string' got type 'boolean'`);
|
|
88
|
+
|
|
89
|
+
const schema2 = object({
|
|
90
|
+
address: object({
|
|
91
|
+
street: string().required(),
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
schema2.validate(context, {});
|
|
95
|
+
schema2.validate(context, {
|
|
96
|
+
address: {
|
|
97
|
+
street: '11th avenue',
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
schema2.validate(context, {
|
|
101
|
+
address: {},
|
|
102
|
+
});
|
|
103
|
+
expect(console.warn).toHaveBeenCalledWith(`'data.address.street' Field is required`);
|
|
104
|
+
spy.mockRestore();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('array', () => {
|
|
108
|
+
const spy = jest.spyOn(global.console, 'warn').mockImplementation();
|
|
109
|
+
const context = 'items';
|
|
110
|
+
array(string()).validate(context, ['123']);
|
|
111
|
+
array(string()).validate(context, [123]);
|
|
112
|
+
expect(console.warn).toHaveBeenCalledWith(`'items[0]' Expected type 'string' got type 'number'`);
|
|
113
|
+
array(array(string())).validate(context, [['123']]);
|
|
114
|
+
array(array(string())).validate(context, [[123]]);
|
|
115
|
+
expect(console.warn).toHaveBeenCalledWith(`'items[0][0]' Expected type 'string' got type 'number'`);
|
|
116
|
+
|
|
117
|
+
const schema = object({
|
|
118
|
+
street: string().required(),
|
|
119
|
+
});
|
|
120
|
+
array(schema).validate(context, []);
|
|
121
|
+
array(schema).validate(context, [{ street: '123' }, { street: '456' }, { street: '789' }]);
|
|
122
|
+
array(schema).validate(context, [{}]);
|
|
123
|
+
expect(console.warn).toHaveBeenCalledWith(`'items[0].street' Field is required`);
|
|
124
|
+
array(schema).validate(context, [{ street: false }]);
|
|
125
|
+
expect(console.warn).toHaveBeenCalledWith(`'items[0].street' Expected type 'string' got type 'boolean'`);
|
|
126
|
+
array(schema).validate(context, [{ street: '123' }, {}]);
|
|
127
|
+
expect(console.warn).toHaveBeenCalledWith(`'items[1].street' Field is required`);
|
|
128
|
+
array(schema).validate(context, [{ street: '123' }, { street: false }]);
|
|
129
|
+
expect(console.warn).toHaveBeenCalledWith(`'items[1].street' Expected type 'string' got type 'boolean'`);
|
|
130
|
+
spy.mockRestore();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('css', () => {
|
|
134
|
+
const styles = css({
|
|
135
|
+
button: {
|
|
136
|
+
color: 'magenta',
|
|
137
|
+
fontSize: '10px',
|
|
138
|
+
'@media screen and (min-width:40em)': {
|
|
139
|
+
fontSize: '64px',
|
|
140
|
+
},
|
|
141
|
+
':hover': {
|
|
142
|
+
color: 'black',
|
|
143
|
+
},
|
|
144
|
+
'@media screen and (min-width:56em)': {
|
|
145
|
+
':hover': {
|
|
146
|
+
color: 'navy',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
container: {
|
|
151
|
+
flex: 1,
|
|
152
|
+
alignItems: 'center',
|
|
153
|
+
justifyContent: 'center',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
expect(styles.toString()).toEqual(`.button-1t9ijgh {
|
|
157
|
+
color: magenta;
|
|
158
|
+
font-size: 10px;
|
|
159
|
+
|
|
160
|
+
@media screen and (min-width:40em) {
|
|
161
|
+
font-size: 64px;
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
:hover {
|
|
165
|
+
color: black;
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
@media screen and (min-width:56em) {
|
|
169
|
+
|
|
170
|
+
:hover {
|
|
171
|
+
color: navy;
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.container-1dvem0h {
|
|
178
|
+
flex: 1;
|
|
179
|
+
align-items: center;
|
|
180
|
+
justify-content: center;
|
|
181
|
+
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('render', async () => {
|
|
188
|
+
const age = 1;
|
|
189
|
+
const data = { name: '123', address: { street: '1' } };
|
|
190
|
+
const items = [1, 2, 3];
|
|
191
|
+
const highlight = 'high';
|
|
192
|
+
const template = html`
|
|
193
|
+
<div>
|
|
194
|
+
<app-counter name="123" class="abc ${highlight}" age=${age} details1=${data} items=${items}></app-counter>
|
|
195
|
+
</div>
|
|
196
|
+
`;
|
|
197
|
+
const res = await render(template);
|
|
198
|
+
expect(res).toEqual(`
|
|
199
|
+
<div>
|
|
200
|
+
<app-counter name="123" class="abc high" age="1" details1="{'name':'123','address':{'street':'1'}}" items="[1,2,3]"></app-counter>
|
|
201
|
+
</div>
|
|
202
|
+
`);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('render attribute keys', async () => {
|
|
206
|
+
const template = html`
|
|
207
|
+
<div>
|
|
208
|
+
<app-counter name="123" perPage="1"></app-counter>
|
|
209
|
+
</div>
|
|
210
|
+
`;
|
|
211
|
+
const res = await render(template);
|
|
212
|
+
expect(res).toEqual(`
|
|
213
|
+
<div>
|
|
214
|
+
<app-counter name="123" perPage="1"></app-counter>
|
|
215
|
+
</div>
|
|
216
|
+
`);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('render attributes within quotes', async () => {
|
|
220
|
+
const age = 1;
|
|
221
|
+
const data = { name: '123', address: { street: '1' } };
|
|
222
|
+
const items = [1, 2, 3];
|
|
223
|
+
const classes = 'high';
|
|
224
|
+
const template = html`
|
|
225
|
+
<div>
|
|
226
|
+
<app-counter name="123" class=${classes} age="${age}" details1="${data}" items="${items}"></app-counter>
|
|
227
|
+
</div>
|
|
228
|
+
`;
|
|
229
|
+
const res = await render(template);
|
|
230
|
+
expect(res).toEqual(`
|
|
231
|
+
<div>
|
|
232
|
+
<app-counter name="123" class="high" age="1" details1="{'name':'123','address':{'street':'1'}}" items="[1,2,3]"></app-counter>
|
|
233
|
+
</div>
|
|
234
|
+
`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('render unsafeHTML', async () => {
|
|
238
|
+
const textContent = `<div><p class="123">this is unsafe</p></div>`;
|
|
239
|
+
const template = html` <div>${unsafeHTML(textContent)}</div> `;
|
|
240
|
+
const res = await render(template);
|
|
241
|
+
expect(res).toEqual(` <div><div><p class="123">this is unsafe</p></div></div> `);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('render classMap show', async () => {
|
|
245
|
+
const hide = false;
|
|
246
|
+
const template = html` <div class="abc ${classMap({ show: !hide })}"></div> `;
|
|
247
|
+
const res = await render(template);
|
|
248
|
+
expect(res).toEqual(` <div class="abc show"></div> `);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('render classMap hide', async () => {
|
|
252
|
+
const hide = true;
|
|
253
|
+
const template = html` <div class="abc ${classMap({ show: !hide })}"></div> `;
|
|
254
|
+
const res = await render(template);
|
|
255
|
+
expect(res).toEqual(` <div class="abc "></div> `);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('render single template', async () => {
|
|
259
|
+
const template = html` <div>${html`NoCountry ${false}`}</div> `;
|
|
260
|
+
const res = await render(template);
|
|
261
|
+
expect(res).toEqual(` <div>NoCountry false</div> `);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('render multi template', async () => {
|
|
265
|
+
const template = html`
|
|
266
|
+
<div>
|
|
267
|
+
${[1, 2].map(
|
|
268
|
+
(v) => html`
|
|
269
|
+
<app-item meta="${{ index: v }}" @click=${() => {}} .handleClick=${() => {}}>
|
|
270
|
+
<button @click=${() => {}}>+</button>
|
|
271
|
+
</app-item>
|
|
272
|
+
`,
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
`;
|
|
276
|
+
const res = await render(template);
|
|
277
|
+
expect(res).toEqual(`
|
|
278
|
+
<div>
|
|
279
|
+
|
|
280
|
+
<app-item meta="{'index':1}">
|
|
281
|
+
<button>+</button>
|
|
282
|
+
</app-item>
|
|
283
|
+
|
|
284
|
+
<app-item meta="{'index':2}">
|
|
285
|
+
<button>+</button>
|
|
286
|
+
</app-item>
|
|
287
|
+
|
|
288
|
+
</div>
|
|
289
|
+
`);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('AtomsElement', async () => {
|
|
293
|
+
class AppItem extends AtomsElement {
|
|
294
|
+
static name = 'app-item';
|
|
295
|
+
|
|
296
|
+
static attrTypes = {
|
|
297
|
+
perPage: string().required(),
|
|
298
|
+
address: object({
|
|
299
|
+
street: string().required(),
|
|
300
|
+
}).required(),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
static stateTypes = {
|
|
304
|
+
count: number().required().default(0),
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
static computedTypes = {
|
|
308
|
+
sum: number()
|
|
309
|
+
.required()
|
|
310
|
+
.compute('count', (count) => {
|
|
311
|
+
return count + 10;
|
|
312
|
+
}),
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
static styles = css({
|
|
316
|
+
div: {
|
|
317
|
+
color: 'red',
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
render() {
|
|
322
|
+
const {
|
|
323
|
+
perPage,
|
|
324
|
+
address: { street },
|
|
325
|
+
} = this.attrs;
|
|
326
|
+
const { count, setCount } = this.state;
|
|
327
|
+
const { sum } = this.computed;
|
|
328
|
+
return html`
|
|
329
|
+
<div perPage=${perPage}>
|
|
330
|
+
<p>street: ${street}</p>
|
|
331
|
+
<p>count: ${count}</p>
|
|
332
|
+
<p>sum: ${sum}</p>
|
|
333
|
+
${this.renderItem()}
|
|
334
|
+
</div>
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
AppItem.register();
|
|
339
|
+
const Clazz = AtomsElement.getElement('app-item');
|
|
340
|
+
expect(Clazz.name).toEqual(AppItem.name);
|
|
341
|
+
const instance = new AppItem([
|
|
342
|
+
{ name: 'address', value: JSON.stringify({ street: '123' }).replace(/"/g, `'`) },
|
|
343
|
+
{ name: 'perpage', value: '1' },
|
|
344
|
+
]);
|
|
345
|
+
instance.renderItem = () => html`<div><p>render item 1</p></div>`;
|
|
346
|
+
expect(AppItem.observedAttributes).toEqual(['perpage', 'address']);
|
|
347
|
+
const res = instance.renderTemplate();
|
|
348
|
+
expect(res).toEqual(`
|
|
349
|
+
|
|
350
|
+
<div perPage="1">
|
|
351
|
+
<p>street: 123</p>
|
|
352
|
+
<p>count: 0</p>
|
|
353
|
+
<p>sum: 10</p>
|
|
354
|
+
<div><p>render item 1</p></div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<style>
|
|
358
|
+
.div-1gao8uk {
|
|
359
|
+
color: red;
|
|
360
|
+
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
</style>
|
|
365
|
+
`);
|
|
366
|
+
});
|
example/app-counter.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createElement, html, css, object, number, string } from '../element.js';
|
|
2
|
+
|
|
3
|
+
const name = () => 'app-counter';
|
|
4
|
+
|
|
5
|
+
const attrTypes = () => ({
|
|
6
|
+
name: string().required(),
|
|
7
|
+
meta: object({
|
|
8
|
+
start: number(),
|
|
9
|
+
}),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const stateTypes = () => ({
|
|
13
|
+
count: number()
|
|
14
|
+
.required()
|
|
15
|
+
.default((attrs) => attrs.meta?.start || 0),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const computedTypes = () => ({
|
|
19
|
+
sum: number()
|
|
20
|
+
.required()
|
|
21
|
+
.compute('count', (count) => {
|
|
22
|
+
return count + 10;
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const styles = css({
|
|
27
|
+
title: {
|
|
28
|
+
fontSize: '20px',
|
|
29
|
+
marginBottom: '0.5rem',
|
|
30
|
+
textAlign: 'center',
|
|
31
|
+
},
|
|
32
|
+
span: {
|
|
33
|
+
fontSize: '16px',
|
|
34
|
+
},
|
|
35
|
+
container: {
|
|
36
|
+
display: 'flex',
|
|
37
|
+
flex: 1,
|
|
38
|
+
flexDirection: 'row',
|
|
39
|
+
fontSize: '32px',
|
|
40
|
+
color: 'rgba(55, 65, 81, 1)',
|
|
41
|
+
},
|
|
42
|
+
mx: {
|
|
43
|
+
marginLeft: '5rem',
|
|
44
|
+
marginRight: '5rem',
|
|
45
|
+
fontSize: '30px',
|
|
46
|
+
fontFamily: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace`,
|
|
47
|
+
},
|
|
48
|
+
button: {
|
|
49
|
+
paddingTop: '0.5rem',
|
|
50
|
+
paddingBottom: '0.5rem',
|
|
51
|
+
paddingLeft: '1rem',
|
|
52
|
+
paddingRight: '1rem',
|
|
53
|
+
color: 'rgba(55, 65, 81, 1)',
|
|
54
|
+
borderRadius: '0.25rem',
|
|
55
|
+
backgroundColor: 'rgba(209, 213, 219, 1)',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const render = ({ attrs, state, computed }) => {
|
|
60
|
+
const { name, meta } = attrs;
|
|
61
|
+
const { count, setCount } = state;
|
|
62
|
+
const { sum } = computed;
|
|
63
|
+
|
|
64
|
+
return html`
|
|
65
|
+
<div>
|
|
66
|
+
<div class=${styles.title}>
|
|
67
|
+
Counter: ${name}
|
|
68
|
+
<span class=${styles.span}>starts at ${meta?.start}</span>
|
|
69
|
+
</div>
|
|
70
|
+
<div class=${styles.container}>
|
|
71
|
+
<button class=${styles.button} @click=${() => setCount((v) => v - 1)}>-</button>
|
|
72
|
+
<div class=${styles.mx}>
|
|
73
|
+
<h1>${count}</h1>
|
|
74
|
+
</div>
|
|
75
|
+
<button class=${styles.button} @click=${() => setCount((v) => v + 1)}>+</button>
|
|
76
|
+
</div>
|
|
77
|
+
<div class=${styles.mx}>
|
|
78
|
+
<h1>Sum: ${sum}</h1>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
`;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default createElement({
|
|
85
|
+
name,
|
|
86
|
+
attrTypes,
|
|
87
|
+
stateTypes,
|
|
88
|
+
computedTypes,
|
|
89
|
+
styles,
|
|
90
|
+
render,
|
|
91
|
+
});
|
example/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { html, css } from '../element.js';
|
|
2
|
+
import { createPage } from '../page.js';
|
|
3
|
+
import './app-counter.js';
|
|
4
|
+
|
|
5
|
+
const route = () => {
|
|
6
|
+
return '/counter';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const styles = css({
|
|
10
|
+
center: {
|
|
11
|
+
display: 'flex',
|
|
12
|
+
flex: 1,
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
justifyContent: 'center',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const head = ({ config }) => {
|
|
19
|
+
return html`
|
|
20
|
+
<title>${config.title}</title>
|
|
21
|
+
<link href="/styles.css" rel="stylesheet" as="style" />
|
|
22
|
+
`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const body = () => {
|
|
26
|
+
return html`
|
|
27
|
+
<div class=${styles.center}>
|
|
28
|
+
<app-counter name="1" meta="{'start': 5}"></app-counter>
|
|
29
|
+
</div>
|
|
30
|
+
`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default createPage({
|
|
34
|
+
route,
|
|
35
|
+
styles,
|
|
36
|
+
head,
|
|
37
|
+
body,
|
|
38
|
+
});
|
example/main.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { defineElement, html, object, number, string, useState } from '../src/index.js';
|
|
2
|
-
import { ssr } from '../src/ssr.js';
|
|
3
|
-
|
|
4
|
-
const attrTypes = {
|
|
5
|
-
name: string.isRequired,
|
|
6
|
-
meta: object({
|
|
7
|
-
start: number,
|
|
8
|
-
}),
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const Counter = ({ name, meta }) => {
|
|
12
|
-
const [count, setCount] = useState(meta?.start || 0);
|
|
13
|
-
|
|
14
|
-
return html`
|
|
15
|
-
<div>
|
|
16
|
-
<div class="font-bold mb-2">Counter: ${name}</div>
|
|
17
|
-
<div class="flex flex-1 flex-row text-3xl text-gray-700">
|
|
18
|
-
<button @click=${() => setCount((v) => v - 1)}>-</button>
|
|
19
|
-
<div class="mx-20">
|
|
20
|
-
<h1 class="text-1xl">${count}</h1>
|
|
21
|
-
</div>
|
|
22
|
-
<button @click=${() => setCount((v) => v + 1)}>+</button>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
`;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
defineElement('app-counter', Counter, attrTypes);
|
|
29
|
-
|
|
30
|
-
console.log(ssr(html`<app-counter name="1"></app-counter>`));
|
example/server.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import path, { dirname } from 'path';
|
|
5
|
+
import renderIndex from './index.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const port = process.argv[2] || 3000;
|
|
11
|
+
const map = {
|
|
12
|
+
'/element.js': `${__dirname}/../element.js`,
|
|
13
|
+
'/lit-html.js': `${__dirname}/../lit-html.js`,
|
|
14
|
+
'/styles.css': `${__dirname}/styles.css`,
|
|
15
|
+
'/app-counter.js': `${__dirname}/app-counter.js`,
|
|
16
|
+
'.js': 'text/javascript',
|
|
17
|
+
'.css': 'text/css',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
http
|
|
21
|
+
.createServer(function (req, res) {
|
|
22
|
+
if (req.url === '/') {
|
|
23
|
+
res.statusCode = 200;
|
|
24
|
+
res.setHeader('Content-type', 'text/html');
|
|
25
|
+
const html = renderIndex({
|
|
26
|
+
config: { lang: 'en', title: 'Counter App' },
|
|
27
|
+
headScript: '',
|
|
28
|
+
bodyScript: `
|
|
29
|
+
<script type="module">
|
|
30
|
+
import './app-counter.js';
|
|
31
|
+
</script>
|
|
32
|
+
`,
|
|
33
|
+
});
|
|
34
|
+
res.end(html);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const filename = map[req.url];
|
|
38
|
+
const data = fs.readFileSync(filename);
|
|
39
|
+
const ext = path.parse(filename).ext;
|
|
40
|
+
res.setHeader('Content-type', map[ext] || 'text/plain');
|
|
41
|
+
res.end(data);
|
|
42
|
+
})
|
|
43
|
+
.listen(parseInt(port));
|
|
44
|
+
|
|
45
|
+
console.log(`Server listening on port ${port}`);
|
example/styles.css
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/*! tailwindcss v2.1.2 | MIT License | https://tailwindcss.com */
|
|
2
|
+
|
|
3
|
+
/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Document
|
|
7
|
+
========
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
Use a better box model (opinionated).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
*,
|
|
15
|
+
::before,
|
|
16
|
+
::after {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
Use a more readable tab size (opinionated).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
html {
|
|
25
|
+
-moz-tab-size: 4;
|
|
26
|
+
-o-tab-size: 4;
|
|
27
|
+
tab-size: 4;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
1. Correct the line height in all browsers.
|
|
32
|
+
2. Prevent adjustments of font size after orientation changes in iOS.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
html {
|
|
36
|
+
line-height: 1.15; /* 1 */
|
|
37
|
+
-webkit-text-size-adjust: 100%; /* 2 */
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
Sections
|
|
42
|
+
========
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
Remove the margin in all browsers.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
body {
|
|
50
|
+
margin: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
body {
|
|
58
|
+
font-family: system-ui, -apple-system, /* Firefox supports this but not yet `system-ui` */ 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
|
59
|
+
'Apple Color Emoji', 'Segoe UI Emoji';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
Grouping content
|
|
64
|
+
================
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
1. Add the correct height in Firefox.
|
|
69
|
+
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
hr {
|
|
73
|
+
height: 0; /* 1 */
|
|
74
|
+
color: inherit; /* 2 */
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
Text-level semantics
|
|
79
|
+
====================
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
Add the correct text decoration in Chrome, Edge, and Safari.
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
abbr[title] {
|
|
87
|
+
-webkit-text-decoration: underline dotted;
|
|
88
|
+
text-decoration: underline dotted;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
Add the correct font weight in Edge and Safari.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
b,
|
|
96
|
+
strong {
|
|
97
|
+
font-weight: bolder;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
|
|
102
|
+
2. Correct the odd 'em' font sizing in all browsers.
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
code,
|
|
106
|
+
kbd,
|
|
107
|
+
samp,
|
|
108
|
+
pre {
|
|
109
|
+
font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; /* 1 */
|
|
110
|
+
font-size: 1em; /* 2 */
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
Add the correct font size in all browsers.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
small {
|
|
118
|
+
font-size: 80%;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
sub,
|
|
126
|
+
sup {
|
|
127
|
+
font-size: 75%;
|
|
128
|
+
line-height: 0;
|
|
129
|
+
position: relative;
|
|
130
|
+
vertical-align: baseline;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
sub {
|
|
134
|
+
bottom: -0.25em;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
sup {
|
|
138
|
+
top: -0.5em;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/*
|
|
142
|
+
Tabular data
|
|
143
|
+
============
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
|
148
|
+
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
table {
|
|
152
|
+
text-indent: 0; /* 1 */
|
|
153
|
+
border-color: inherit; /* 2 */
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/*
|
|
157
|
+
Forms
|
|
158
|
+
=====
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
1. Change the font styles in all browsers.
|
|
163
|
+
2. Remove the margin in Firefox and Safari.
|
|
164
|
+
*/
|
|
165
|
+
|
|
166
|
+
button,
|
|
167
|
+
input,
|
|
168
|
+
optgroup,
|
|
169
|
+
select,
|
|
170
|
+
textarea {
|
|
171
|
+
/* font-family: inherit; */ /* 1 */
|
|
172
|
+
font-size: 100%; /* 1 */
|
|
173
|
+
/* line-height: 1.15; */ /* 1 */
|
|
174
|
+
margin: 0; /* 2 */
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
Remove the inheritance of text transform in Edge and Firefox.
|
|
179
|
+
1. Remove the inheritance of text transform in Firefox.
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
button,
|
|
183
|
+
select {
|
|
184
|
+
/* 1 */
|
|
185
|
+
/* text-transform: none; */
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
Correct the inability to style clickable types in iOS and Safari.
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
button,
|
|
193
|
+
[type='button'],
|
|
194
|
+
[type='reset'],
|
|
195
|
+
[type='submit'] {
|
|
196
|
+
/* -webkit-appearance: button; */
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
Remove the inner border and padding in Firefox.
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
::-moz-focus-inner {
|
|
204
|
+
border-style: none;
|
|
205
|
+
padding: 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
Restore the focus styles unset by the previous rule.
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
:-moz-focusring {
|
|
213
|
+
outline: 1px dotted ButtonText;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
Remove the additional ':invalid' styles in Firefox.
|
|
218
|
+
See: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737
|
|
219
|
+
*/
|
|
220
|
+
|
|
221
|
+
:-moz-ui-invalid {
|
|
222
|
+
box-shadow: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.
|
|
227
|
+
*/
|
|
228
|
+
|
|
229
|
+
legend {
|
|
230
|
+
padding: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
Add the correct vertical alignment in Chrome and Firefox.
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
progress {
|
|
238
|
+
vertical-align: baseline;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
Correct the cursor style of increment and decrement buttons in Safari.
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
::-webkit-inner-spin-button,
|
|
246
|
+
::-webkit-outer-spin-button {
|
|
247
|
+
height: auto;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
1. Correct the odd appearance in Chrome and Safari.
|
|
252
|
+
2. Correct the outline style in Safari.
|
|
253
|
+
*/
|
|
254
|
+
|
|
255
|
+
[type='search'] {
|
|
256
|
+
-webkit-appearance: textfield; /* 1 */
|
|
257
|
+
outline-offset: -2px; /* 2 */
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
Remove the inner padding in Chrome and Safari on macOS.
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
::-webkit-search-decoration {
|
|
265
|
+
-webkit-appearance: none;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
1. Correct the inability to style clickable types in iOS and Safari.
|
|
270
|
+
2. Change font properties to 'inherit' in Safari.
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
::-webkit-file-upload-button {
|
|
274
|
+
-webkit-appearance: button; /* 1 */
|
|
275
|
+
font: inherit; /* 2 */
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/*
|
|
279
|
+
Interactive
|
|
280
|
+
===========
|
|
281
|
+
*/
|
|
282
|
+
|
|
283
|
+
/*
|
|
284
|
+
Add the correct display in Chrome and Safari.
|
|
285
|
+
*/
|
|
286
|
+
|
|
287
|
+
summary {
|
|
288
|
+
display: list-item;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Manually forked from SUIT CSS Base: https://github.com/suitcss/base
|
|
293
|
+
* A thin layer on top of normalize.css that provides a starting point more
|
|
294
|
+
* suitable for web applications.
|
|
295
|
+
*/
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Removes the default spacing and border for appropriate elements.
|
|
299
|
+
*/
|
|
300
|
+
|
|
301
|
+
blockquote,
|
|
302
|
+
dl,
|
|
303
|
+
dd,
|
|
304
|
+
h1,
|
|
305
|
+
h2,
|
|
306
|
+
h3,
|
|
307
|
+
h4,
|
|
308
|
+
h5,
|
|
309
|
+
h6,
|
|
310
|
+
hr,
|
|
311
|
+
figure,
|
|
312
|
+
p,
|
|
313
|
+
pre {
|
|
314
|
+
margin: 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
button {
|
|
318
|
+
/* background-color: transparent; */
|
|
319
|
+
kground-image: none;
|
|
320
|
+
/* background-image: none; */
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Work around a Firefox/IE bug where the transparent `button` background
|
|
325
|
+
* results in a loss of the default `button` focus styles.
|
|
326
|
+
*/
|
|
327
|
+
|
|
328
|
+
button:focus {
|
|
329
|
+
outline: 1px dotted;
|
|
330
|
+
outline: 5px auto -webkit-focus-ring-color;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fieldset {
|
|
334
|
+
margin: 0;
|
|
335
|
+
padding: 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
ol,
|
|
339
|
+
ul {
|
|
340
|
+
list-style: none;
|
|
341
|
+
margin: 0;
|
|
342
|
+
padding: 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Tailwind custom reset styles
|
|
347
|
+
*/
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 1. Use the user's configured `sans` font-family (with Tailwind's default
|
|
351
|
+
* sans-serif font stack as a fallback) as a sane default.
|
|
352
|
+
* 2. Use Tailwind's default "normal" line-height so the user isn't forced
|
|
353
|
+
* to override it to ensure consistency even when using the default theme.
|
|
354
|
+
*/
|
|
355
|
+
|
|
356
|
+
html {
|
|
357
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
|
358
|
+
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; /* 1 */
|
|
359
|
+
line-height: 1.5; /* 2 */
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Inherit font-family and line-height from `html` so users can set them as
|
|
364
|
+
* a class directly on the `html` element.
|
|
365
|
+
*/
|
|
366
|
+
|
|
367
|
+
body {
|
|
368
|
+
font-family: inherit;
|
|
369
|
+
line-height: inherit;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 1. Prevent padding and border from affecting element width.
|
|
374
|
+
*
|
|
375
|
+
* We used to set this in the html element and inherit from
|
|
376
|
+
* the parent element for everything else. This caused issues
|
|
377
|
+
* in shadow-dom-enhanced elements like <details> where the content
|
|
378
|
+
* is wrapped by a div with box-sizing set to `content-box`.
|
|
379
|
+
*
|
|
380
|
+
* https://github.com/mozdevs/cssremedy/issues/4
|
|
381
|
+
*
|
|
382
|
+
*
|
|
383
|
+
* 2. Allow adding a border to an element by just adding a border-width.
|
|
384
|
+
*
|
|
385
|
+
* By default, the way the browser specifies that an element should have no
|
|
386
|
+
* border is by setting it's border-style to `none` in the user-agent
|
|
387
|
+
* stylesheet.
|
|
388
|
+
*
|
|
389
|
+
* In order to easily add borders to elements by just setting the `border-width`
|
|
390
|
+
* property, we change the default border-style for all elements to `solid`, and
|
|
391
|
+
* use border-width to hide them instead. This way our `border` utilities only
|
|
392
|
+
* need to set the `border-width` property instead of the entire `border`
|
|
393
|
+
* shorthand, making our border utilities much more straightforward to compose.
|
|
394
|
+
*
|
|
395
|
+
* https://github.com/tailwindcss/tailwindcss/pull/116
|
|
396
|
+
*/
|
|
397
|
+
|
|
398
|
+
*,
|
|
399
|
+
::before,
|
|
400
|
+
::after {
|
|
401
|
+
box-sizing: border-box; /* 1 */
|
|
402
|
+
border-width: 0; /* 2 */
|
|
403
|
+
border-style: solid; /* 2 */
|
|
404
|
+
border-color: #e5e7eb; /* 2 */
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/*
|
|
408
|
+
* Ensure horizontal rules are visible by default
|
|
409
|
+
*/
|
|
410
|
+
|
|
411
|
+
hr {
|
|
412
|
+
border-top-width: 1px;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Undo the `border-style: none` reset that Normalize applies to images so that
|
|
417
|
+
* our `border-{width}` utilities have the expected effect.
|
|
418
|
+
*
|
|
419
|
+
* The Normalize reset is unnecessary for us since we default the border-width
|
|
420
|
+
* to 0 on all elements.
|
|
421
|
+
*
|
|
422
|
+
* https://github.com/tailwindcss/tailwindcss/issues/362
|
|
423
|
+
*/
|
|
424
|
+
|
|
425
|
+
img {
|
|
426
|
+
border-style: solid;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
textarea {
|
|
430
|
+
resize: vertical;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
input::-moz-placeholder,
|
|
434
|
+
textarea::-moz-placeholder {
|
|
435
|
+
opacity: 1;
|
|
436
|
+
color: #9ca3af;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
input:-ms-input-placeholder,
|
|
440
|
+
textarea:-ms-input-placeholder {
|
|
441
|
+
opacity: 1;
|
|
442
|
+
color: #9ca3af;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
input::placeholder,
|
|
446
|
+
textarea::placeholder {
|
|
447
|
+
opacity: 1;
|
|
448
|
+
color: #9ca3af;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
button,
|
|
452
|
+
[role='button'] {
|
|
453
|
+
cursor: pointer;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
table {
|
|
457
|
+
border-collapse: collapse;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
h1,
|
|
461
|
+
h2,
|
|
462
|
+
h3,
|
|
463
|
+
h4,
|
|
464
|
+
h5,
|
|
465
|
+
h6 {
|
|
466
|
+
font-size: inherit;
|
|
467
|
+
font-weight: inherit;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Reset links to optimize for opt-in styling instead of
|
|
472
|
+
* opt-out.
|
|
473
|
+
*/
|
|
474
|
+
|
|
475
|
+
a {
|
|
476
|
+
color: inherit;
|
|
477
|
+
text-decoration: inherit;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Reset form element properties that are easy to forget to
|
|
482
|
+
* style explicitly so you don't inadvertently introduce
|
|
483
|
+
* styles that deviate from your design system. These styles
|
|
484
|
+
* supplement a partial reset that is already applied by
|
|
485
|
+
* normalize.css.
|
|
486
|
+
*/
|
|
487
|
+
|
|
488
|
+
button,
|
|
489
|
+
input,
|
|
490
|
+
optgroup,
|
|
491
|
+
select,
|
|
492
|
+
textarea {
|
|
493
|
+
padding: 0;
|
|
494
|
+
line-height: inherit;
|
|
495
|
+
color: inherit;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Use the configured 'mono' font family for elements that
|
|
500
|
+
* are expected to be rendered with a monospace font, falling
|
|
501
|
+
* back to the system monospace stack if there is no configured
|
|
502
|
+
* 'mono' font family.
|
|
503
|
+
*/
|
|
504
|
+
|
|
505
|
+
pre,
|
|
506
|
+
code,
|
|
507
|
+
kbd,
|
|
508
|
+
samp {
|
|
509
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Make replaced elements `display: block` by default as that's
|
|
514
|
+
* the behavior you want almost all of the time. Inspired by
|
|
515
|
+
* CSS Remedy, with `svg` added as well.
|
|
516
|
+
*
|
|
517
|
+
* https://github.com/mozdevs/cssremedy/issues/14
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
img,
|
|
521
|
+
svg,
|
|
522
|
+
video,
|
|
523
|
+
canvas,
|
|
524
|
+
audio,
|
|
525
|
+
iframe,
|
|
526
|
+
embed,
|
|
527
|
+
object {
|
|
528
|
+
display: block;
|
|
529
|
+
vertical-align: middle;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Constrain images and videos to the parent width and preserve
|
|
534
|
+
* their intrinsic aspect ratio.
|
|
535
|
+
*
|
|
536
|
+
* https://github.com/mozdevs/cssremedy/issues/14
|
|
537
|
+
*/
|
|
538
|
+
|
|
539
|
+
img,
|
|
540
|
+
video {
|
|
541
|
+
max-width: 100%;
|
|
542
|
+
height: auto;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
html {
|
|
546
|
+
width: 100%;
|
|
547
|
+
height: 100%;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
body {
|
|
551
|
+
background-color: white;
|
|
552
|
+
width: 100%;
|
|
553
|
+
height: 100%;
|
|
554
|
+
display: flex;
|
|
555
|
+
flex-direction: column;
|
|
556
|
+
flex: 1 1 0%;
|
|
557
|
+
min-width: 320px;
|
|
558
|
+
margin: 0px;
|
|
559
|
+
min-height: 100vh;
|
|
560
|
+
line-height: 1.4;
|
|
561
|
+
font-weight: 400;
|
|
562
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
563
|
+
color: rgba(44, 62, 80, 1);
|
|
564
|
+
direction: ltr;
|
|
565
|
+
font-synthesis: none;
|
|
566
|
+
text-rendering: optimizeLegibility;
|
|
567
|
+
}
|
src/lit-html.js → lit-html.js
RENAMED
|
File without changes
|
package-lock.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atoms-element",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"lockfileVersion": 2,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
|
-
"version": "
|
|
8
|
+
"version": "2.1.0",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"parse5": "^6.0.1"
|
package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atoms-element",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A simple web component library for defining your custom elements. It works on both client and server. It supports hooks and follows the same principles of react.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pyros.sh",
|
|
@@ -10,28 +10,24 @@
|
|
|
10
10
|
"server side rendering",
|
|
11
11
|
"client",
|
|
12
12
|
"server",
|
|
13
|
-
"hooks",
|
|
14
13
|
"react",
|
|
15
|
-
"lit-html"
|
|
14
|
+
"lit-html"
|
|
16
|
-
"lit-html-server",
|
|
17
|
-
"haunted",
|
|
18
|
-
"tonic",
|
|
19
|
-
"atomico",
|
|
20
|
-
"fuco"
|
|
21
15
|
],
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"main": "src/index.js",
|
|
24
16
|
"files": [
|
|
17
|
+
"lit-html.js",
|
|
18
|
+
"element.js",
|
|
19
|
+
"element.d.ts",
|
|
25
|
-
"
|
|
20
|
+
"page.js",
|
|
26
|
-
"
|
|
21
|
+
"page.d.ts"
|
|
27
22
|
],
|
|
28
|
-
"
|
|
23
|
+
"license": "MIT",
|
|
29
24
|
"author": "pyros.sh",
|
|
30
25
|
"type": "module",
|
|
31
26
|
"engines": {
|
|
32
27
|
"node": ">=14.0.0"
|
|
33
28
|
},
|
|
34
29
|
"scripts": {
|
|
30
|
+
"example": "node example/server.js",
|
|
35
31
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
|
|
36
32
|
},
|
|
37
33
|
"devDependencies": {
|
page.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare type Config = {
|
|
2
|
+
version: string
|
|
3
|
+
url: string
|
|
4
|
+
image: string
|
|
5
|
+
author: string
|
|
6
|
+
languages: Array<string>
|
|
7
|
+
title: string
|
|
8
|
+
description: string
|
|
9
|
+
keywords: string
|
|
10
|
+
categories: Array<any>
|
|
11
|
+
tags: Array<any>
|
|
12
|
+
strings: {[key: string]: any}
|
|
13
|
+
themes: {[key: string]: any}
|
|
14
|
+
}
|
|
15
|
+
export declare type Data = any;
|
|
16
|
+
export declare type Item = any;
|
|
17
|
+
|
|
18
|
+
export class Page {
|
|
19
|
+
config: Config;
|
|
20
|
+
data: Data;
|
|
21
|
+
item: Item;
|
|
22
|
+
route: () => string;
|
|
23
|
+
styles: () => string;
|
|
24
|
+
head: () => string;
|
|
25
|
+
body: () => string;
|
|
26
|
+
}
|
page.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import parse5 from 'parse5';
|
|
2
|
+
import { AtomsElement, render } from './element.js';
|
|
3
|
+
|
|
4
|
+
export const find = (node) => {
|
|
5
|
+
for (const child of node.childNodes) {
|
|
6
|
+
if (AtomsElement.getElement(child.tagName)) {
|
|
7
|
+
const Clazz = AtomsElement.getElement(child.tagName);
|
|
8
|
+
const instance = new Clazz(child.attrs);
|
|
9
|
+
const res = instance.renderTemplate();
|
|
10
|
+
const frag = parse5.parseFragment(res);
|
|
11
|
+
child.childNodes.push(...frag.childNodes);
|
|
12
|
+
}
|
|
13
|
+
if (child.childNodes) {
|
|
14
|
+
find(child);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const ssr = (template) => {
|
|
20
|
+
const text = render(template);
|
|
21
|
+
const h = parse5.parseFragment(text);
|
|
22
|
+
find(h);
|
|
23
|
+
return parse5.serialize(h);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const createPage = ({ route, datapaths, head, body, styles }) => {
|
|
27
|
+
return ({ config, data, item, headScript, bodyScript }) => {
|
|
28
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
29
|
+
const props = { config, data, item };
|
|
30
|
+
const headHtml = ssr(head(props));
|
|
31
|
+
const bodyHtml = ssr(body(props));
|
|
32
|
+
return `
|
|
33
|
+
<!DOCTYPE html>
|
|
34
|
+
<html lang="${config.lang}">
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="utf-8" />
|
|
37
|
+
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
38
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
39
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no">
|
|
40
|
+
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
|
|
41
|
+
<link rel="icon" type="image/png" href="/assets/icon.png" />
|
|
42
|
+
${headHtml}
|
|
43
|
+
<style>
|
|
44
|
+
${styles.toString()}
|
|
45
|
+
</style>
|
|
46
|
+
${headScript}
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
${bodyHtml}
|
|
50
|
+
<script>
|
|
51
|
+
window.__DEV__ = ${!isProd};
|
|
52
|
+
window.config = ${JSON.stringify(config)};
|
|
53
|
+
window.data = ${JSON.stringify(data)};
|
|
54
|
+
window.item = ${JSON.stringify(item)};
|
|
55
|
+
</script>
|
|
56
|
+
${bodyScript}
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|
|
59
|
+
`;
|
|
60
|
+
};
|
|
61
|
+
};
|
page.test.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { expect, test } from '@jest/globals';
|
|
2
|
+
import { html, css } from './element.js';
|
|
3
|
+
import { createPage } from './page.js';
|
|
4
|
+
|
|
5
|
+
test('Page', () => {
|
|
6
|
+
const route = () => {
|
|
7
|
+
const langPart = this.config.lang === 'en' ? '' : `/${this.config.lang}`;
|
|
8
|
+
return `${langPart}`;
|
|
9
|
+
};
|
|
10
|
+
const styles = css({
|
|
11
|
+
div: {
|
|
12
|
+
color: 'red',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
const head = ({ config }) => {
|
|
16
|
+
return html`
|
|
17
|
+
<title>${config.title}</title>
|
|
18
|
+
<meta name="title" content=${config.title} />
|
|
19
|
+
<meta name="description" content=${config.title} />
|
|
20
|
+
`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const body = ({ config }) => {
|
|
24
|
+
return html`
|
|
25
|
+
<div>
|
|
26
|
+
<app-header></app-header>
|
|
27
|
+
<main class="flex flex-1 flex-col mt-20 items-center">
|
|
28
|
+
<h1 class="text-5xl">${config.title}</h1>
|
|
29
|
+
</main>
|
|
30
|
+
</div>
|
|
31
|
+
`;
|
|
32
|
+
};
|
|
33
|
+
const renderPage = createPage({
|
|
34
|
+
route,
|
|
35
|
+
head,
|
|
36
|
+
body,
|
|
37
|
+
styles,
|
|
38
|
+
});
|
|
39
|
+
const scripts = '<script type="module"><script>';
|
|
40
|
+
const res = renderPage({ config: { lang: 'en', title: '123' }, headScript: scripts, bodyScript: scripts });
|
|
41
|
+
expect(res).toEqual(`
|
|
42
|
+
<!DOCTYPE html>
|
|
43
|
+
<html lang="en">
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="utf-8" />
|
|
46
|
+
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
|
47
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
48
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no">
|
|
49
|
+
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
|
|
50
|
+
<link rel="icon" type="image/png" href="/assets/icon.png" />
|
|
51
|
+
|
|
52
|
+
<title>123</title>
|
|
53
|
+
<meta name="title" content="123">
|
|
54
|
+
<meta name="description" content="123">
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
.div-1gao8uk {
|
|
58
|
+
color: red;
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
</style>
|
|
64
|
+
<script type="module"><script>
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
|
|
68
|
+
<div>
|
|
69
|
+
<app-header></app-header>
|
|
70
|
+
<main class="flex flex-1 flex-col mt-20 items-center">
|
|
71
|
+
<h1 class="text-5xl">123</h1>
|
|
72
|
+
</main>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<script>
|
|
76
|
+
window.__DEV__ = true;
|
|
77
|
+
window.config = {"lang":"en","title":"123"};
|
|
78
|
+
window.data = undefined;
|
|
79
|
+
window.item = undefined;
|
|
80
|
+
</script>
|
|
81
|
+
<script type="module"><script>
|
|
82
|
+
</body>
|
|
83
|
+
</html>
|
|
84
|
+
`);
|
|
85
|
+
});
|
readme.md
CHANGED
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/atoms-element)
|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
A simple web component library for defining your custom elements. It works on both client and server. It
|
|
7
|
-
principles of react. Data props are attributes on the custom element by default so its easier to debug and functions/handlers are
|
|
8
|
-
methods attached to the element.
|
|
6
|
+
A simple web component library for defining your custom elements. It works on both client and server. It follows the same principles of react. Data props are attributes on the custom element by default so its easier to debug and functions/handlers are attached to the element.
|
|
9
7
|
|
|
10
8
|
I initially started researching if it was possible to server render web components but found out not one framework supported it. I liked using
|
|
11
|
-
[haunted](https://github.com/matthewp/haunted) as it was react
|
|
9
|
+
[haunted](https://github.com/matthewp/haunted) as it was react-like with hooks but was lost on how to implement server rendering. Libraries like
|
|
12
10
|
JSDOM couldn't be of use since it didn't support web components and I didn't want to use puppeteer for something like this.
|
|
13
11
|
|
|
14
12
|
After a year of thinking about it and researching it I found out this awesome framework [Tonic](https://github.com/optoolco/tonic).
|
|
@@ -23,7 +21,7 @@ After going through all these libraries,
|
|
|
23
21
|
5. [Atomico](https://github.com/atomicojs/atomico)
|
|
24
22
|
6. [fuco](https://github.com/wtnbass/fuco)
|
|
25
23
|
|
|
26
|
-
And figuring out how each one implemented their on custom elements I came up with atoms-element. It still doesn't have proper rehydration lit-html just replaces the DOM under the web component for now. Atomico has implemented proper SSR with hydration so I might need to look into that in the future or
|
|
24
|
+
And figuring out how each one implemented their on custom elements I came up with atoms-element. It still doesn't have proper rehydration lit-html just replaces the DOM under the server rendered web component for now. Atomico has implemented proper SSR with hydration so I might need to look into that in the future or
|
|
27
25
|
use it instead of lit-html. Since I made a few modifications like json attributes and attrTypes I don't know if it will easy.
|
|
28
26
|
|
|
29
27
|
## Installation
|
|
@@ -35,38 +33,102 @@ npm i atoms-element
|
|
|
35
33
|
## Example
|
|
36
34
|
|
|
37
35
|
```js
|
|
38
|
-
import {
|
|
36
|
+
import { createElement, html, css, object, number, string } from 'atoms-element/element.js';
|
|
39
|
-
import { ssr } from 'atoms-element/
|
|
37
|
+
import { ssr } from 'atoms-element/page.js';
|
|
40
38
|
|
|
39
|
+
const name = () => 'app-counter';
|
|
40
|
+
|
|
41
|
-
const attrTypes = {
|
|
41
|
+
const attrTypes = () => ({
|
|
42
|
-
name: string.
|
|
42
|
+
name: string().required(),
|
|
43
43
|
meta: object({
|
|
44
|
-
start: number,
|
|
44
|
+
start: number(),
|
|
45
45
|
}),
|
|
46
|
-
};
|
|
46
|
+
});
|
|
47
|
+
|
|
47
|
-
|
|
48
|
+
const stateTypes = () => ({
|
|
49
|
+
count: number()
|
|
50
|
+
.required()
|
|
51
|
+
.default((attrs) => attrs.meta?.start || 0),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const computedTypes = () => ({
|
|
55
|
+
sum: number()
|
|
56
|
+
.required()
|
|
57
|
+
.compute('count', (count) => {
|
|
58
|
+
return count + 10;
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const styles = css({
|
|
63
|
+
title: {
|
|
64
|
+
fontSize: '20px',
|
|
65
|
+
marginBottom: '0.5rem',
|
|
66
|
+
textAlign: 'center',
|
|
67
|
+
},
|
|
68
|
+
span: {
|
|
69
|
+
fontSize: '16px',
|
|
70
|
+
},
|
|
71
|
+
container: {
|
|
72
|
+
display: 'flex',
|
|
73
|
+
flex: 1,
|
|
74
|
+
flexDirection: 'row',
|
|
75
|
+
fontSize: '32px',
|
|
76
|
+
color: 'rgba(55, 65, 81, 1)',
|
|
77
|
+
},
|
|
78
|
+
mx: {
|
|
79
|
+
marginLeft: '5rem',
|
|
80
|
+
marginRight: '5rem',
|
|
81
|
+
fontSize: '30px',
|
|
82
|
+
fontFamily: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace`,
|
|
83
|
+
},
|
|
84
|
+
button: {
|
|
85
|
+
paddingTop: '0.5rem',
|
|
86
|
+
paddingBottom: '0.5rem',
|
|
87
|
+
paddingLeft: '1rem',
|
|
88
|
+
paddingRight: '1rem',
|
|
89
|
+
color: 'rgba(55, 65, 81, 1)',
|
|
90
|
+
borderRadius: '0.25rem',
|
|
91
|
+
backgroundColor: 'rgba(209, 213, 219, 1)',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
48
|
-
const
|
|
95
|
+
const render = ({ attrs, state, computed }) => {
|
|
96
|
+
const { name, meta } = attrs;
|
|
49
|
-
const
|
|
97
|
+
const { count, setCount } = state;
|
|
98
|
+
const { sum } = computed;
|
|
50
99
|
|
|
51
100
|
return html`
|
|
52
101
|
<div>
|
|
102
|
+
<div class=${styles.title}>
|
|
103
|
+
Counter: ${name}
|
|
53
|
-
|
|
104
|
+
<span class=${styles.span}>starts at ${meta?.start}</span>
|
|
105
|
+
</div>
|
|
54
|
-
<div class=
|
|
106
|
+
<div class=${styles.container}>
|
|
55
|
-
<button @click=${() => setCount((v) => v - 1)}>-</button>
|
|
107
|
+
<button class=${styles.button} @click=${() => setCount((v) => v - 1)}>-</button>
|
|
56
|
-
<div class=
|
|
108
|
+
<div class=${styles.mx}>
|
|
57
|
-
<h1
|
|
109
|
+
<h1>${count}</h1>
|
|
58
110
|
</div>
|
|
59
|
-
<button @click=${() => setCount((v) => v + 1)}>+</button>
|
|
111
|
+
<button class=${styles.button} @click=${() => setCount((v) => v + 1)}>+</button>
|
|
112
|
+
</div>
|
|
113
|
+
<div class=${styles.mx}>
|
|
114
|
+
<h1>Sum: ${sum}</h1>
|
|
60
115
|
</div>
|
|
61
116
|
</div>
|
|
62
117
|
`;
|
|
63
118
|
};
|
|
64
119
|
|
|
65
|
-
|
|
120
|
+
export default createElement({
|
|
121
|
+
name,
|
|
122
|
+
attrTypes,
|
|
123
|
+
stateTypes,
|
|
124
|
+
computedTypes,
|
|
125
|
+
styles,
|
|
126
|
+
render,
|
|
127
|
+
});
|
|
66
128
|
|
|
67
129
|
console.log(ssr(html`<app-counter name="1"></app-counter>`));
|
|
68
130
|
```
|
|
69
131
|
|
|
70
132
|
This works in node, to make it work in the client side you need to either compile atoms-element using esintall or esbuild to an esm module and then
|
|
71
133
|
rewrite the path from 'atoms-element' to 'web_modules/atoms-element/index.js' or if you can host the node_modules in a web server and change the path to
|
|
72
|
-
the import '
|
|
134
|
+
the import 'atoms-element/element.js'
|
src/index.d.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
export declare type Destructor = () => void | undefined;
|
|
2
|
-
export declare type EffectCallback = () => (void | Destructor);
|
|
3
|
-
export declare type SetStateAction<S> = S | ((prevState: S) => S);
|
|
4
|
-
export declare type Dispatch<A> = (value: A) => void;
|
|
5
|
-
export declare type DispatchWithoutAction = () => void;
|
|
6
|
-
export declare type DependencyList = ReadonlyArray<any>;
|
|
7
|
-
export declare type Reducer<S, A> = (prevState: S, action: A) => S;
|
|
8
|
-
export declare type ReducerWithoutAction<S> = (prevState: S) => S;
|
|
9
|
-
export declare type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any> ? S : never;
|
|
10
|
-
export declare type ReducerAction<R extends Reducer<any, any>> = R extends Reducer<any, infer A> ? A : never;
|
|
11
|
-
export declare type ReducerStateWithoutAction<R extends ReducerWithoutAction<any>> = R extends ReducerWithoutAction<infer S> ? S : never;
|
|
12
|
-
export declare interface MutableRefObject<T> {
|
|
13
|
-
current: T | null | undefined;
|
|
14
|
-
}
|
|
15
|
-
export declare type Config = {
|
|
16
|
-
version: string
|
|
17
|
-
url: string
|
|
18
|
-
image: string
|
|
19
|
-
author: string
|
|
20
|
-
languages: Array<string>
|
|
21
|
-
title: string
|
|
22
|
-
description: string
|
|
23
|
-
keywords: string
|
|
24
|
-
categories: Array<string>
|
|
25
|
-
tags: Array<string>
|
|
26
|
-
strings: {[key: string]: string}
|
|
27
|
-
}
|
|
28
|
-
export declare type Location = {
|
|
29
|
-
readonly ancestorOrigins: DOMStringList;
|
|
30
|
-
hash: string;
|
|
31
|
-
host: string;
|
|
32
|
-
hostname: string;
|
|
33
|
-
href: string;
|
|
34
|
-
readonly origin: string;
|
|
35
|
-
pathname: string;
|
|
36
|
-
port: string;
|
|
37
|
-
protocol: string;
|
|
38
|
-
search: string;
|
|
39
|
-
assign: (url: string | URL) => void;
|
|
40
|
-
reload: () => void;
|
|
41
|
-
replace: (url: string | URL) => void;
|
|
42
|
-
toString: () => string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export declare const useState: <S>(initialState: S | (() => S)) => [S, Dispatch<SetStateAction<S>>];
|
|
46
|
-
export declare const useEffect: (effect: EffectCallback, deps?: DependencyList) => void;
|
|
47
|
-
export declare const useLayoutEffect: (effect: EffectCallback, deps?: DependencyList) => void;
|
|
48
|
-
export declare const useReducer: <R extends ReducerWithoutAction<any>, I>(
|
|
49
|
-
reducer: R,
|
|
50
|
-
initializerArg: I,
|
|
51
|
-
initializer: (arg: I) => ReducerStateWithoutAction<R>
|
|
52
|
-
) => [ReducerStateWithoutAction<R>, DispatchWithoutAction];
|
|
53
|
-
|
|
54
|
-
export declare const useCallback: <T extends (...args: any[]) => any>(callback: T, deps: DependencyList) => T;
|
|
55
|
-
export declare const useMemo: <T>(factory: () => T, deps: DependencyList | undefined) => T;
|
|
56
|
-
// function useImperativeHandle<T, R extends T>(ref: Ref<T>|undefined, init: () => R, deps?: DependencyList): void;
|
|
57
|
-
export declare const useRef: <T>(initialValue: T | null | undefined) => MutableRefObject<T>;
|
|
58
|
-
|
|
59
|
-
export declare const useConfig: () => Config;
|
|
60
|
-
export declare const useLocation: () => Location;
|
src/index.js
DELETED
|
@@ -1,497 +0,0 @@
|
|
|
1
|
-
import { html, render as litRender, directive, NodePart, AttributePart, PropertyPart, isPrimitive } from './lit-html.js';
|
|
2
|
-
|
|
3
|
-
const registry = {};
|
|
4
|
-
const isBrowser = typeof window !== 'undefined';
|
|
5
|
-
export { html, isBrowser };
|
|
6
|
-
|
|
7
|
-
const lastAttributeNameRegex =
|
|
8
|
-
/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
|
|
9
|
-
|
|
10
|
-
const wrapAttribute = (attrName, suffix, text, v) => {
|
|
11
|
-
let buffer = text;
|
|
12
|
-
const hasQuote = suffix && suffix.includes(`="`);
|
|
13
|
-
if (attrName && !hasQuote) {
|
|
14
|
-
buffer += `"`;
|
|
15
|
-
}
|
|
16
|
-
buffer += v;
|
|
17
|
-
if (attrName && !hasQuote) {
|
|
18
|
-
buffer += `"`;
|
|
19
|
-
}
|
|
20
|
-
return buffer;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const render = isBrowser
|
|
24
|
-
? litRender
|
|
25
|
-
: (template) => {
|
|
26
|
-
let js = '';
|
|
27
|
-
template.strings.forEach((text, i) => {
|
|
28
|
-
const value = template.values[i];
|
|
29
|
-
const type = typeof value;
|
|
30
|
-
let attrName, suffix;
|
|
31
|
-
const matchName = lastAttributeNameRegex.exec(text);
|
|
32
|
-
if (matchName) {
|
|
33
|
-
attrName = matchName[2];
|
|
34
|
-
suffix = matchName[3];
|
|
35
|
-
}
|
|
36
|
-
if (value === null || !(type === 'object' || type === 'function' || type === 'undefined')) {
|
|
37
|
-
js += wrapAttribute(attrName, suffix, text, type !== 'string' ? String(value) : value);
|
|
38
|
-
} else if (Array.isArray(value) && value.find((item) => item && item.strings && item.type === 'html')) {
|
|
39
|
-
js += text;
|
|
40
|
-
value.forEach((v) => {
|
|
41
|
-
js += render(v);
|
|
42
|
-
});
|
|
43
|
-
} else if (type === 'object') {
|
|
44
|
-
// TemplateResult
|
|
45
|
-
if (value.strings && value.type === 'html') {
|
|
46
|
-
js += text;
|
|
47
|
-
js += render(value);
|
|
48
|
-
} else {
|
|
49
|
-
js += wrapAttribute(attrName, suffix, text, JSON.stringify(value).replace(/"/g, `'`));
|
|
50
|
-
}
|
|
51
|
-
} else if (type == 'function') {
|
|
52
|
-
if (attrName) {
|
|
53
|
-
js += text.replace(' ' + attrName + '=', '');
|
|
54
|
-
} else {
|
|
55
|
-
// js += text;
|
|
56
|
-
// js += value();
|
|
57
|
-
}
|
|
58
|
-
} else if (type !== 'undefined') {
|
|
59
|
-
js += text;
|
|
60
|
-
js += value.toString();
|
|
61
|
-
} else {
|
|
62
|
-
js += text;
|
|
63
|
-
// console.log('value', value);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
return js;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const previousValues = new WeakMap();
|
|
70
|
-
export const unsafeHTML = isBrowser
|
|
71
|
-
? directive((value) => (part) => {
|
|
72
|
-
if (!(part instanceof NodePart)) {
|
|
73
|
-
throw new Error('unsafeHTML can only be used in text bindings');
|
|
74
|
-
}
|
|
75
|
-
const previousValue = previousValues.get(part);
|
|
76
|
-
if (previousValue !== undefined && isPrimitive(value) && value === previousValue.value && part.value === previousValue.fragment) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const template = document.createElement('template');
|
|
80
|
-
template.innerHTML = value; // innerHTML casts to string internally
|
|
81
|
-
const fragment = document.importNode(template.content, true);
|
|
82
|
-
part.setValue(fragment);
|
|
83
|
-
previousValues.set(part, { value, fragment });
|
|
84
|
-
})
|
|
85
|
-
: (value) => value;
|
|
86
|
-
|
|
87
|
-
const previousClassesCache = new WeakMap();
|
|
88
|
-
export const classMap = isBrowser
|
|
89
|
-
? directive((classInfo) => (part) => {
|
|
90
|
-
if (!(part instanceof AttributePart) || part instanceof PropertyPart || part.committer.name !== 'class' || part.committer.parts.length > 1) {
|
|
91
|
-
throw new Error('The `classMap` directive must be used in the `class` attribute ' + 'and must be the only part in the attribute.');
|
|
92
|
-
}
|
|
93
|
-
const { committer } = part;
|
|
94
|
-
const { element } = committer;
|
|
95
|
-
let previousClasses = previousClassesCache.get(part);
|
|
96
|
-
if (previousClasses === undefined) {
|
|
97
|
-
// Write static classes once
|
|
98
|
-
// Use setAttribute() because className isn't a string on SVG elements
|
|
99
|
-
element.setAttribute('class', committer.strings.join(' '));
|
|
100
|
-
previousClassesCache.set(part, (previousClasses = new Set()));
|
|
101
|
-
}
|
|
102
|
-
const classList = element.classList;
|
|
103
|
-
// Remove old classes that no longer apply
|
|
104
|
-
// We use forEach() instead of for-of so that re don't require down-level
|
|
105
|
-
// iteration.
|
|
106
|
-
previousClasses.forEach((name) => {
|
|
107
|
-
if (!(name in classInfo)) {
|
|
108
|
-
classList.remove(name);
|
|
109
|
-
previousClasses.delete(name);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
// Add or remove classes based on their classMap value
|
|
113
|
-
for (const name in classInfo) {
|
|
114
|
-
const value = classInfo[name];
|
|
115
|
-
if (value != previousClasses.has(name)) {
|
|
116
|
-
// We explicitly want a loose truthy check of `value` because it seems
|
|
117
|
-
// more convenient that '' and 0 are skipped.
|
|
118
|
-
if (value) {
|
|
119
|
-
classList.add(name);
|
|
120
|
-
previousClasses.add(name);
|
|
121
|
-
} else {
|
|
122
|
-
classList.remove(name);
|
|
123
|
-
previousClasses.delete(name);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (typeof classList.commit === 'function') {
|
|
128
|
-
classList.commit();
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
: (classes) => {
|
|
132
|
-
let value = '';
|
|
133
|
-
for (const key in classes) {
|
|
134
|
-
if (classes[key]) {
|
|
135
|
-
value += `${value.length ? ' ' : ''}${key}`;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return value;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
let currentCursor;
|
|
142
|
-
let currentComponent;
|
|
143
|
-
let logError = (msg) => {
|
|
144
|
-
console.warn(msg);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
export const setLogError = (fn) => {
|
|
148
|
-
logError = fn;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const checkRequired = (context, data) => {
|
|
152
|
-
if (data === null || typeof data === 'undefined') {
|
|
153
|
-
logError(`'${context}' Field is required`);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const checkPrimitive = (primitiveType) => {
|
|
158
|
-
const common = {
|
|
159
|
-
type: primitiveType,
|
|
160
|
-
parse: (attr) => attr,
|
|
161
|
-
};
|
|
162
|
-
const validate = (context, data) => {
|
|
163
|
-
if (data === null || typeof data === 'undefined') {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const dataType = typeof data;
|
|
167
|
-
if (dataType !== primitiveType) {
|
|
168
|
-
logError(`'${context}' Expected type '${primitiveType}' got type '${dataType}'`);
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
return {
|
|
172
|
-
validate,
|
|
173
|
-
...common,
|
|
174
|
-
isRequired: {
|
|
175
|
-
...common,
|
|
176
|
-
validate: (context, data) => {
|
|
177
|
-
checkRequired(context, data);
|
|
178
|
-
validate(context, data);
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const checkComplex = (complexType, validate) => {
|
|
185
|
-
const common = {
|
|
186
|
-
type: complexType,
|
|
187
|
-
parse: (attr) => (attr ? JSON.parse(attr.replace(/'/g, `"`)) : null),
|
|
188
|
-
};
|
|
189
|
-
return (innerType) => {
|
|
190
|
-
return {
|
|
191
|
-
...common,
|
|
192
|
-
validate: (context, data) => {
|
|
193
|
-
if (!data) {
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
validate(innerType, context, data);
|
|
197
|
-
},
|
|
198
|
-
isRequired: {
|
|
199
|
-
...common,
|
|
200
|
-
validate: (context, data) => {
|
|
201
|
-
checkRequired(context, data);
|
|
202
|
-
validate(innerType, context, data);
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
};
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
export const number = checkPrimitive('number');
|
|
210
|
-
export const string = checkPrimitive('string');
|
|
211
|
-
export const boolean = checkPrimitive('boolean');
|
|
212
|
-
export const object = checkComplex('object', (innerType, context, data) => {
|
|
213
|
-
if (data.constructor !== Object) {
|
|
214
|
-
logError(`'${context}' Expected object literal '{}' got '${typeof data}'`);
|
|
215
|
-
}
|
|
216
|
-
for (const key of Object.keys(innerType)) {
|
|
217
|
-
const fieldValidator = innerType[key];
|
|
218
|
-
const item = data[key];
|
|
219
|
-
fieldValidator.validate(`${context}.${key}`, item);
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
export const array = checkComplex('array', (innerType, context, data) => {
|
|
223
|
-
if (!Array.isArray(data)) {
|
|
224
|
-
logError(`Expected Array got ${data}`);
|
|
225
|
-
}
|
|
226
|
-
for (let i = 0; i < data.length; i++) {
|
|
227
|
-
const item = data[i];
|
|
228
|
-
innerType.validate(`${context}[${i}]`, item);
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
export const func = checkComplex('function', (innerType, context, data) => {});
|
|
232
|
-
|
|
233
|
-
export const hooks = (config) => {
|
|
234
|
-
const h = currentComponent.hooks;
|
|
235
|
-
const c = currentComponent;
|
|
236
|
-
const index = currentCursor++;
|
|
237
|
-
if (h.values.length <= index && config.oncreate) {
|
|
238
|
-
h.values[index] = config.oncreate(h, c, index);
|
|
239
|
-
}
|
|
240
|
-
if (config.onupdate) {
|
|
241
|
-
h.values[index] = config.onupdate(h, c, index);
|
|
242
|
-
}
|
|
243
|
-
return h.values[index];
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
export const __setCurrent__ = (c) => {
|
|
247
|
-
currentComponent = c;
|
|
248
|
-
currentCursor = 0;
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
export const useDispatchEvent = (name) =>
|
|
252
|
-
hooks({
|
|
253
|
-
oncreate: (_, c) => (data) => c.dispatchEvent(new CustomEvent(name, data)),
|
|
254
|
-
});
|
|
255
|
-
export const useRef = (initialValue) =>
|
|
256
|
-
hooks({
|
|
257
|
-
oncreate: (_h, _c) => ({ current: initialValue }),
|
|
258
|
-
});
|
|
259
|
-
export const useState = (initialState) =>
|
|
260
|
-
hooks({
|
|
261
|
-
oncreate: (h, c, i) => [
|
|
262
|
-
typeof initialState === 'function' ? initialState() : initialState,
|
|
263
|
-
function setState(nextState) {
|
|
264
|
-
const state = h.values[i][0];
|
|
265
|
-
if (typeof nextState === 'function') {
|
|
266
|
-
nextState = nextState(state);
|
|
267
|
-
}
|
|
268
|
-
if (!Object.is(state, nextState)) {
|
|
269
|
-
h.values[i][0] = nextState;
|
|
270
|
-
c.update();
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
});
|
|
275
|
-
export const useReducer = (reducer, initialState) =>
|
|
276
|
-
hooks({
|
|
277
|
-
oncreate: (h, c, i) => [
|
|
278
|
-
initialState,
|
|
279
|
-
function dispatch(action) {
|
|
280
|
-
const state = h.values[i][0];
|
|
281
|
-
const nextState = reducer(state, action);
|
|
282
|
-
if (!Object.is(state, nextState)) {
|
|
283
|
-
h.values[i][0] = nextState;
|
|
284
|
-
c.update();
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
],
|
|
288
|
-
});
|
|
289
|
-
const depsChanged = (prev, next) => prev == null || next.some((f, i) => !Object.is(f, prev[i]));
|
|
290
|
-
export const useEffect = (handler, deps) =>
|
|
291
|
-
hooks({
|
|
292
|
-
onupdate(h, _, i) {
|
|
293
|
-
if (!deps || depsChanged(h.deps[i], deps)) {
|
|
294
|
-
h.deps[i] = deps || [];
|
|
295
|
-
h.effects[i] = handler;
|
|
296
|
-
}
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
export const useLayoutEffect = (handler, deps) =>
|
|
300
|
-
hooks({
|
|
301
|
-
onupdate(h, _, i) {
|
|
302
|
-
if (!deps || depsChanged(h.deps[i], deps)) {
|
|
303
|
-
h.deps[i] = deps || [];
|
|
304
|
-
h.layoutEffects[i] = handler;
|
|
305
|
-
}
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
export const useMemo = (fn, deps) =>
|
|
309
|
-
hooks({
|
|
310
|
-
onupdate(h, _, i) {
|
|
311
|
-
let value = h.values[i];
|
|
312
|
-
if (!deps || depsChanged(h.deps[i], deps)) {
|
|
313
|
-
h.deps[i] = deps || [];
|
|
314
|
-
value = fn();
|
|
315
|
-
}
|
|
316
|
-
return value;
|
|
317
|
-
},
|
|
318
|
-
});
|
|
319
|
-
export const useCallback = (callback, deps) => useMemo(() => callback, deps);
|
|
320
|
-
export const useConfig = () => {
|
|
321
|
-
if (isBrowser) {
|
|
322
|
-
return window.config;
|
|
323
|
-
}
|
|
324
|
-
return global.config;
|
|
325
|
-
};
|
|
326
|
-
export const useLocation = () => {
|
|
327
|
-
if (isBrowser) {
|
|
328
|
-
return window.location;
|
|
329
|
-
}
|
|
330
|
-
return global.location;
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
const batch = (runner, pick, callback) => {
|
|
334
|
-
const q = [];
|
|
335
|
-
const flush = () => {
|
|
336
|
-
let p;
|
|
337
|
-
while ((p = pick(q))) callback(p);
|
|
338
|
-
};
|
|
339
|
-
const run = runner(flush);
|
|
340
|
-
return (c) => q.push(c) === 1 && run();
|
|
341
|
-
};
|
|
342
|
-
const fifo = (q) => q.shift();
|
|
343
|
-
const filo = (q) => q.pop();
|
|
344
|
-
const microtask = (flush) => {
|
|
345
|
-
return () => queueMicrotask(flush);
|
|
346
|
-
};
|
|
347
|
-
const task = (flush) => {
|
|
348
|
-
if (isBrowser) {
|
|
349
|
-
const ch = new window.MessageChannel();
|
|
350
|
-
ch.port1.onmessage = flush;
|
|
351
|
-
return () => ch.port2.postMessage(null);
|
|
352
|
-
} else {
|
|
353
|
-
return () => setImmediate(flush);
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
const enqueueLayoutEffects = batch(microtask, filo, (c) => c._flushEffects('layoutEffects'));
|
|
357
|
-
const enqueueEffects = batch(task, filo, (c) => c._flushEffects('effects'));
|
|
358
|
-
const enqueueUpdate = batch(microtask, fifo, (c) => c._performUpdate());
|
|
359
|
-
|
|
360
|
-
const BaseElement = isBrowser ? window.HTMLElement : class {};
|
|
361
|
-
|
|
362
|
-
export class AtomsElement extends BaseElement {
|
|
363
|
-
constructor() {
|
|
364
|
-
super();
|
|
365
|
-
this._dirty = false;
|
|
366
|
-
this._connected = false;
|
|
367
|
-
this.hooks = {
|
|
368
|
-
values: [],
|
|
369
|
-
deps: [],
|
|
370
|
-
effects: [],
|
|
371
|
-
layoutEffects: [],
|
|
372
|
-
cleanup: [],
|
|
373
|
-
};
|
|
374
|
-
this.props = {};
|
|
375
|
-
this.attrTypes = {};
|
|
376
|
-
this.name = '';
|
|
377
|
-
this.renderer = () => {};
|
|
378
|
-
this.attrTypes = {};
|
|
379
|
-
this.funcKeys = [];
|
|
380
|
-
this.attrTypesMap = {};
|
|
381
|
-
}
|
|
382
|
-
connectedCallback() {
|
|
383
|
-
this._connected = true;
|
|
384
|
-
if (isBrowser) {
|
|
385
|
-
this.update();
|
|
386
|
-
} else {
|
|
387
|
-
__setCurrent__(this);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
disconnectedCallback() {
|
|
391
|
-
this._connected = false;
|
|
392
|
-
let cleanup;
|
|
393
|
-
while ((cleanup = this.hooks.cleanup.shift())) {
|
|
394
|
-
cleanup();
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
attributeChangedCallback(key, oldValue, newValue) {
|
|
399
|
-
const attr = this.attrTypesMap[key];
|
|
400
|
-
if (!attr) {
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
const data = attr.propType.parse(newValue);
|
|
404
|
-
attr.propType.validate(`<${this.name}> ${key}`, data);
|
|
405
|
-
this.props[attr.propName] = data;
|
|
406
|
-
if (this._connected) {
|
|
407
|
-
this.update();
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
update() {
|
|
412
|
-
if (this._dirty) {
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
this._dirty = true;
|
|
416
|
-
enqueueUpdate(this);
|
|
417
|
-
}
|
|
418
|
-
_performUpdate() {
|
|
419
|
-
if (!this._connected) {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
__setCurrent__(this);
|
|
423
|
-
this.render();
|
|
424
|
-
enqueueLayoutEffects(this);
|
|
425
|
-
enqueueEffects(this);
|
|
426
|
-
this._dirty = false;
|
|
427
|
-
}
|
|
428
|
-
_flushEffects(effectKey) {
|
|
429
|
-
const effects = this.hooks[effectKey];
|
|
430
|
-
const cleanups = this.hooks.cleanup;
|
|
431
|
-
for (let i = 0, len = effects.length; i < len; i++) {
|
|
432
|
-
if (effects[i]) {
|
|
433
|
-
cleanups[i] && cleanups[i]();
|
|
434
|
-
const cleanup = effects[i]();
|
|
435
|
-
if (cleanup) {
|
|
436
|
-
cleanups[i] = cleanup;
|
|
437
|
-
}
|
|
438
|
-
delete effects[i];
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
render() {
|
|
444
|
-
if (isBrowser) {
|
|
445
|
-
this.funcKeys.forEach((key) => {
|
|
446
|
-
this.props[key] = this[key];
|
|
447
|
-
});
|
|
448
|
-
render(this.renderer(this.props), this);
|
|
449
|
-
} else {
|
|
450
|
-
__setCurrent__(this);
|
|
451
|
-
return render(this.renderer(this.props), this);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
export const getElement = (name) => registry[name];
|
|
456
|
-
|
|
457
|
-
export function defineElement(name, fn, attrTypes = {}) {
|
|
458
|
-
const keys = Object.keys(attrTypes);
|
|
459
|
-
registry[name] = {
|
|
460
|
-
fn,
|
|
461
|
-
attrTypes,
|
|
462
|
-
Clazz: class extends AtomsElement {
|
|
463
|
-
constructor(attrs) {
|
|
464
|
-
super();
|
|
465
|
-
this.name = name;
|
|
466
|
-
this.renderer = fn;
|
|
467
|
-
this.attrTypes = attrTypes;
|
|
468
|
-
this.funcKeys = keys.filter((key) => attrTypes[key].type === 'function');
|
|
469
|
-
this.attrTypesMap = keys
|
|
470
|
-
.filter((key) => attrTypes[key].type !== 'function')
|
|
471
|
-
.reduce((acc, key) => {
|
|
472
|
-
acc[key.toLowerCase()] = {
|
|
473
|
-
propName: key,
|
|
474
|
-
propType: attrTypes[key],
|
|
475
|
-
};
|
|
476
|
-
return acc;
|
|
477
|
-
}, {});
|
|
478
|
-
if (attrs) {
|
|
479
|
-
attrs.forEach((item) => {
|
|
480
|
-
this.attributeChangedCallback(item.name, null, item.value);
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
static get observedAttributes() {
|
|
486
|
-
return keys.map((k) => k.toLowerCase());
|
|
487
|
-
}
|
|
488
|
-
},
|
|
489
|
-
};
|
|
490
|
-
if (isBrowser) {
|
|
491
|
-
if (window.customElements.get(name)) {
|
|
492
|
-
return;
|
|
493
|
-
} else {
|
|
494
|
-
window.customElements.define(name, registry[name].Clazz);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
src/ssr.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import parse5 from 'parse5';
|
|
2
|
-
import { getElement, render } from '../src/index.js';
|
|
3
|
-
|
|
4
|
-
export const find = async (node) => {
|
|
5
|
-
for (const child of node.childNodes) {
|
|
6
|
-
if (getElement(child.tagName)) {
|
|
7
|
-
const element = getElement(child.tagName);
|
|
8
|
-
const elementInstance = new element.Clazz(child.attrs);
|
|
9
|
-
const res = elementInstance.render();
|
|
10
|
-
const frag = parse5.parseFragment(res);
|
|
11
|
-
child.childNodes.push(...frag.childNodes);
|
|
12
|
-
}
|
|
13
|
-
if (child.childNodes) {
|
|
14
|
-
find(child);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const ssr = (template) => {
|
|
20
|
-
const text = render(template);
|
|
21
|
-
const h = parse5.parseFragment(text);
|
|
22
|
-
find(h);
|
|
23
|
-
return parse5.serialize(h);
|
|
24
|
-
};
|
test/index.test.js
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { expect, test, jest } from '@jest/globals';
|
|
2
|
-
import {
|
|
3
|
-
html,
|
|
4
|
-
render,
|
|
5
|
-
number,
|
|
6
|
-
boolean,
|
|
7
|
-
string,
|
|
8
|
-
array,
|
|
9
|
-
object,
|
|
10
|
-
setLogError,
|
|
11
|
-
defineElement,
|
|
12
|
-
getElement,
|
|
13
|
-
useConfig,
|
|
14
|
-
useLocation,
|
|
15
|
-
useState,
|
|
16
|
-
unsafeHTML,
|
|
17
|
-
classMap,
|
|
18
|
-
} from '../src/index.js';
|
|
19
|
-
|
|
20
|
-
const logMock = jest.fn();
|
|
21
|
-
setLogError(logMock);
|
|
22
|
-
|
|
23
|
-
const expectError = (msg) => expect(logMock).toHaveBeenCalledWith(msg);
|
|
24
|
-
|
|
25
|
-
const primitives = [
|
|
26
|
-
{
|
|
27
|
-
type: 'number',
|
|
28
|
-
validator: number,
|
|
29
|
-
valid: [123, 40.5, 6410],
|
|
30
|
-
invalid: ['123', false, {}, [], new Date()],
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
type: 'boolean',
|
|
34
|
-
validator: boolean,
|
|
35
|
-
valid: [false, true],
|
|
36
|
-
invalid: ['123', 123, {}, [], new Date()],
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
type: 'string',
|
|
40
|
-
validator: string,
|
|
41
|
-
valid: ['', '123'],
|
|
42
|
-
invalid: [123, false, {}, [], new Date()],
|
|
43
|
-
},
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
primitives.forEach((value) =>
|
|
47
|
-
it(`${value.type}`, () => {
|
|
48
|
-
const context = 'key';
|
|
49
|
-
expect(value.validator.type).toEqual(value.type);
|
|
50
|
-
expect(value.validator.isRequired.type).toEqual(value.type);
|
|
51
|
-
value.validator.validate(context);
|
|
52
|
-
for (const v of value.valid) {
|
|
53
|
-
value.validator.validate(context, v);
|
|
54
|
-
value.validator.isRequired.validate(context, v);
|
|
55
|
-
}
|
|
56
|
-
value.validator.isRequired.validate(context);
|
|
57
|
-
expectError(`'key' Field is required`);
|
|
58
|
-
for (const v of value.invalid) {
|
|
59
|
-
value.validator.validate(context, v);
|
|
60
|
-
expectError(`'key' Expected type '${value.type}' got type '${typeof v}'`);
|
|
61
|
-
}
|
|
62
|
-
}),
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
test('object', () => {
|
|
66
|
-
const context = 'data';
|
|
67
|
-
object({}).validate(context, { name: '123' });
|
|
68
|
-
object({ name: string }).validate(context, { name: '123' });
|
|
69
|
-
object({ name: string.isRequired }).validate(context, { name: '' });
|
|
70
|
-
object({ name: string.isRequired }).validate(context, {});
|
|
71
|
-
expectError(`'data.name' Field is required`);
|
|
72
|
-
|
|
73
|
-
const schema = object({
|
|
74
|
-
address: object({
|
|
75
|
-
street: string,
|
|
76
|
-
}),
|
|
77
|
-
});
|
|
78
|
-
schema.validate(context, {});
|
|
79
|
-
schema.validate(context, '123');
|
|
80
|
-
expectError(`'data' Expected object literal '{}' got 'string'`);
|
|
81
|
-
schema.validate(context, {
|
|
82
|
-
address: {},
|
|
83
|
-
});
|
|
84
|
-
schema.validate(context, {
|
|
85
|
-
address: '123',
|
|
86
|
-
});
|
|
87
|
-
expectError(`'data.address' Expected object literal '{}' got 'string'`);
|
|
88
|
-
schema.validate(context, {
|
|
89
|
-
address: {
|
|
90
|
-
street: 'avenue 1',
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
schema.validate(context, {
|
|
94
|
-
address: {
|
|
95
|
-
street: false,
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
expectError(`'data.address.street' Expected type 'string' got type 'boolean'`);
|
|
99
|
-
|
|
100
|
-
const schema2 = object({
|
|
101
|
-
address: object({
|
|
102
|
-
street: string.isRequired,
|
|
103
|
-
}),
|
|
104
|
-
});
|
|
105
|
-
schema2.validate(context, {});
|
|
106
|
-
schema2.validate(context, {
|
|
107
|
-
address: {
|
|
108
|
-
street: '11th avenue',
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
schema2.validate(context, {
|
|
112
|
-
address: {},
|
|
113
|
-
});
|
|
114
|
-
expectError(`'data.address.street' Field is required`);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('array', () => {
|
|
118
|
-
const context = 'items';
|
|
119
|
-
array(string).validate(context, ['123']);
|
|
120
|
-
array(string).validate(context, [123]);
|
|
121
|
-
expectError(`'items[0]' Expected type 'string' got type 'number'`);
|
|
122
|
-
array(array(string)).validate(context, [['123']]);
|
|
123
|
-
array(array(string)).validate(context, [[123]]);
|
|
124
|
-
expectError(`'items[0][0]' Expected type 'string' got type 'number'`);
|
|
125
|
-
|
|
126
|
-
const schema = object({
|
|
127
|
-
street: string.isRequired,
|
|
128
|
-
});
|
|
129
|
-
array(schema).validate(context, []);
|
|
130
|
-
array(schema).validate(context, [{ street: '123' }, { street: '456' }, { street: '789' }]);
|
|
131
|
-
array(schema).validate(context, [{}]);
|
|
132
|
-
expectError(`'items[0].street' Field is required`);
|
|
133
|
-
array(schema).validate(context, [{ street: false }]);
|
|
134
|
-
expectError(`'items[0].street' Expected type 'string' got type 'boolean'`);
|
|
135
|
-
array(schema).validate(context, [{ street: '123' }, {}]);
|
|
136
|
-
expectError(`'items[1].street' Field is required`);
|
|
137
|
-
array(schema).validate(context, [{ street: '123' }, { street: false }]);
|
|
138
|
-
expectError(`'items[1].street' Expected type 'string' got type 'boolean'`);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('useConfig', async () => {
|
|
142
|
-
expect(useConfig()).toBe(undefined);
|
|
143
|
-
global.config = {};
|
|
144
|
-
expect(useConfig()).toMatchObject({});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('useLocation', async () => {
|
|
148
|
-
expect(useLocation()).toBe(undefined);
|
|
149
|
-
global.location = {};
|
|
150
|
-
expect(useLocation()).toMatchObject({});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test('render', async () => {
|
|
154
|
-
const age = 1;
|
|
155
|
-
const data = { name: '123', address: { street: '1' } };
|
|
156
|
-
const items = [1, 2, 3];
|
|
157
|
-
const highlight = 'high';
|
|
158
|
-
const template = html`
|
|
159
|
-
<div>
|
|
160
|
-
<app-counter name="123" class="abc ${highlight}" age=${age} details1=${data} items=${items}></app-counter>
|
|
161
|
-
</div>
|
|
162
|
-
`;
|
|
163
|
-
const res = await render(template);
|
|
164
|
-
expect(res).toEqual(`
|
|
165
|
-
<div>
|
|
166
|
-
<app-counter name="123" class="abc high" age="1" details1="{'name':'123','address':{'street':'1'}}" items="[1,2,3]"></app-counter>
|
|
167
|
-
</div>
|
|
168
|
-
`);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('render attribute keys', async () => {
|
|
172
|
-
const template = html`
|
|
173
|
-
<div>
|
|
174
|
-
<app-counter name="123" perPage="1"></app-counter>
|
|
175
|
-
</div>
|
|
176
|
-
`;
|
|
177
|
-
const res = await render(template);
|
|
178
|
-
expect(res).toEqual(`
|
|
179
|
-
<div>
|
|
180
|
-
<app-counter name="123" perPage="1"></app-counter>
|
|
181
|
-
</div>
|
|
182
|
-
`);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test('render attributes within quotes', async () => {
|
|
186
|
-
const age = 1;
|
|
187
|
-
const data = { name: '123', address: { street: '1' } };
|
|
188
|
-
const items = [1, 2, 3];
|
|
189
|
-
const classes = 'high';
|
|
190
|
-
const template = html`
|
|
191
|
-
<div>
|
|
192
|
-
<app-counter name="123" class=${classes} age="${age}" details1="${data}" items="${items}"></app-counter>
|
|
193
|
-
</div>
|
|
194
|
-
`;
|
|
195
|
-
const res = await render(template);
|
|
196
|
-
expect(res).toEqual(`
|
|
197
|
-
<div>
|
|
198
|
-
<app-counter name="123" class="high" age="1" details1="{'name':'123','address':{'street':'1'}}" items="[1,2,3]"></app-counter>
|
|
199
|
-
</div>
|
|
200
|
-
`);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('render unsafeHTML', async () => {
|
|
204
|
-
const textContent = `<div><p class="123">this is unsafe</p></div>`;
|
|
205
|
-
const template = html` <div>${unsafeHTML(textContent)}</div> `;
|
|
206
|
-
const res = await render(template);
|
|
207
|
-
expect(res).toEqual(` <div><div><p class="123">this is unsafe</p></div></div> `);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test('render classMap show', async () => {
|
|
211
|
-
const hide = false;
|
|
212
|
-
const template = html` <div class="abc ${classMap({ show: !hide })}"></div> `;
|
|
213
|
-
const res = await render(template);
|
|
214
|
-
expect(res).toEqual(` <div class="abc show"></div> `);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test('render classMap hide', async () => {
|
|
218
|
-
const hide = true;
|
|
219
|
-
const template = html` <div class="abc ${classMap({ show: !hide })}"></div> `;
|
|
220
|
-
const res = await render(template);
|
|
221
|
-
expect(res).toEqual(` <div class="abc "></div> `);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test('render single template', async () => {
|
|
225
|
-
const template = html` <div>${html`NoCountry ${false}`}</div> `;
|
|
226
|
-
const res = await render(template);
|
|
227
|
-
expect(res).toEqual(` <div>NoCountry false</div> `);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('render multi template', async () => {
|
|
231
|
-
const template = html` <div>${[1, 2].map((v) => html` <app-item meta="${{ index: v }}" @click=${() => {}} .handleClick=${() => {}}></app-item>`)}</div> `;
|
|
232
|
-
const res = await render(template);
|
|
233
|
-
expect(res).toEqual(` <div> <app-item meta="{'index':1}"></app-item> <app-item meta="{'index':2}"></app-item></div> `);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test('defineElement', async () => {
|
|
237
|
-
const attrTypes = {
|
|
238
|
-
perPage: string.isRequired,
|
|
239
|
-
address: object({
|
|
240
|
-
street: string.isRequired,
|
|
241
|
-
}).isRequired,
|
|
242
|
-
};
|
|
243
|
-
const AppItem = ({ perPage, address: { street } }) => {
|
|
244
|
-
const [count] = useState(0);
|
|
245
|
-
return html`
|
|
246
|
-
<div perPage=${perPage}>
|
|
247
|
-
<p>street: ${street}</p>
|
|
248
|
-
<p>count: ${count}</p>
|
|
249
|
-
</div>
|
|
250
|
-
`;
|
|
251
|
-
};
|
|
252
|
-
defineElement('app-item', AppItem, attrTypes);
|
|
253
|
-
const { Clazz } = getElement('app-item');
|
|
254
|
-
const instance = new Clazz([
|
|
255
|
-
{ name: 'address', value: JSON.stringify({ street: '123' }).replace(/"/g, `'`) },
|
|
256
|
-
{ name: 'perpage', value: '1' },
|
|
257
|
-
]);
|
|
258
|
-
expect(Clazz.observedAttributes).toEqual(['perpage', 'address']);
|
|
259
|
-
const res = await instance.render();
|
|
260
|
-
expect(res).toEqual(`
|
|
261
|
-
<div perPage="1">
|
|
262
|
-
<p>street: 123</p>
|
|
263
|
-
<p>count: 0</p>
|
|
264
|
-
</div>
|
|
265
|
-
`);
|
|
266
|
-
});
|