~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.
3f6f6421
—
Peter John 4 years ago
improve some stuff
- element.js +74 -144
- element.test.js +29 -9
- package.json +8 -1
- page.js +13 -2
element.js
CHANGED
|
@@ -292,22 +292,19 @@ export const array = checkComplex('array', (innerType, context, data) => {
|
|
|
292
292
|
});
|
|
293
293
|
export const func = checkComplex('function', (innerType, context, data) => {});
|
|
294
294
|
|
|
295
|
-
const depsChanged = (prev, next) => prev == null || next.some((f, i) => !Object.is(f, prev[i]));
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
};
|
|
295
|
+
// const depsChanged = (prev, next) => prev == null || next.some((f, i) => !Object.is(f, prev[i]));
|
|
296
|
+
// useEffect(handler, deps) {
|
|
297
|
+
// const index = this.hooks.currentCursor++;
|
|
298
|
+
// if (!deps || depsChanged(this.hooks.deps[index], deps)) {
|
|
299
|
+
// this.hooks.deps[index] = deps || [];
|
|
300
|
+
// this.hooks.effects[index] = handler;
|
|
301
|
+
// }
|
|
302
|
+
// }
|
|
303
|
+
|
|
304
|
+
const normalizeCss = `html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}`;
|
|
306
305
|
const fifo = (q) => q.shift();
|
|
307
306
|
const filo = (q) => q.pop();
|
|
308
|
-
const microtask = (flush) => {
|
|
309
|
-
|
|
307
|
+
const microtask = (flush) => () => queueMicrotask(flush);
|
|
310
|
-
};
|
|
311
308
|
const task = (flush) => {
|
|
312
309
|
if (isBrowser) {
|
|
313
310
|
const ch = new window.MessageChannel();
|
|
@@ -317,9 +314,6 @@ const task = (flush) => {
|
|
|
317
314
|
return () => setImmediate(flush);
|
|
318
315
|
}
|
|
319
316
|
};
|
|
320
|
-
const enqueueLayoutEffects = batch(microtask, filo, (c) => c._flushEffects('layoutEffects'));
|
|
321
|
-
const enqueueEffects = batch(task, filo, (c) => c._flushEffects('effects'));
|
|
322
|
-
const enqueueUpdate = batch(microtask, fifo, (c) => c._performUpdate());
|
|
323
317
|
|
|
324
318
|
const registry = {};
|
|
325
319
|
const BaseElement = isBrowser ? window.HTMLElement : class {};
|
|
@@ -351,17 +345,12 @@ export default class AtomsElement extends BaseElement {
|
|
|
351
345
|
super();
|
|
352
346
|
this._dirty = false;
|
|
353
347
|
this._connected = false;
|
|
354
|
-
this.
|
|
348
|
+
this._state = {};
|
|
355
|
-
currentCursor: 0,
|
|
356
|
-
values: [],
|
|
357
|
-
deps: [],
|
|
358
|
-
effects: [],
|
|
359
|
-
|
|
349
|
+
this._effects = {};
|
|
360
|
-
cleanup: [],
|
|
361
|
-
};
|
|
362
350
|
this.ssrAttributes = ssrAttributes;
|
|
363
351
|
this.config = isBrowser ? window.config : global.config;
|
|
364
352
|
this.location = isBrowser ? window.location : global.location;
|
|
353
|
+
this.stylesMounted = false;
|
|
365
354
|
}
|
|
366
355
|
|
|
367
356
|
connectedCallback() {
|
|
@@ -372,10 +361,10 @@ export default class AtomsElement extends BaseElement {
|
|
|
372
361
|
}
|
|
373
362
|
disconnectedCallback() {
|
|
374
363
|
this._connected = false;
|
|
375
|
-
let cleanup;
|
|
364
|
+
// let cleanup;
|
|
376
|
-
while ((cleanup = this.hooks.cleanup.shift())) {
|
|
365
|
+
// while ((cleanup = this.hooks.cleanup.shift())) {
|
|
377
|
-
|
|
366
|
+
// cleanup();
|
|
378
|
-
}
|
|
367
|
+
// }
|
|
379
368
|
}
|
|
380
369
|
|
|
381
370
|
attributeChangedCallback(key, oldValue, newValue) {
|
|
@@ -389,33 +378,43 @@ export default class AtomsElement extends BaseElement {
|
|
|
389
378
|
return;
|
|
390
379
|
}
|
|
391
380
|
this._dirty = true;
|
|
392
|
-
enqueueUpdate(
|
|
381
|
+
this.enqueueUpdate();
|
|
393
382
|
}
|
|
394
383
|
|
|
395
384
|
_performUpdate() {
|
|
396
385
|
if (!this._connected) {
|
|
397
386
|
return;
|
|
398
387
|
}
|
|
399
|
-
this.hooks.currentCursor = 0;
|
|
400
|
-
|
|
388
|
+
this.renderTemplate();
|
|
401
|
-
enqueueLayoutEffects(this);
|
|
402
|
-
enqueueEffects(
|
|
389
|
+
this.enqueueEffects();
|
|
403
390
|
this._dirty = false;
|
|
404
391
|
}
|
|
405
392
|
|
|
406
|
-
|
|
393
|
+
batch(runner, pick, callback) {
|
|
394
|
+
const q = [];
|
|
407
|
-
const
|
|
395
|
+
const flush = () => {
|
|
396
|
+
let p;
|
|
408
|
-
|
|
397
|
+
while ((p = pick(q))) callback(p);
|
|
409
|
-
for (let i = 0, len = effects.length; i < len; i++) {
|
|
410
|
-
|
|
398
|
+
};
|
|
411
|
-
cleanups[i] && cleanups[i]();
|
|
412
|
-
|
|
399
|
+
const run = runner(flush);
|
|
413
|
-
if (cleanup) {
|
|
414
|
-
|
|
400
|
+
q.push(this) === 1 && run();
|
|
415
|
-
|
|
401
|
+
}
|
|
402
|
+
|
|
416
|
-
|
|
403
|
+
enqueueEffects() {
|
|
404
|
+
this.batch(task, filo, () => this._flushEffects());
|
|
417
|
-
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
enqueueUpdate() {
|
|
408
|
+
this.batch(microtask, fifo, () => this._performUpdate());
|
|
418
|
-
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
_flushEffects() {
|
|
412
|
+
Object.keys(this.constructor.effects).forEach((key) => {
|
|
413
|
+
const effect = this.constructor.effects[key];
|
|
414
|
+
// if (!effect.deps || depsChanged(, effect.deps)) {
|
|
415
|
+
// }
|
|
416
|
+
effect.callback(this.attrs, this.state);
|
|
417
|
+
});
|
|
419
418
|
}
|
|
420
419
|
|
|
421
420
|
get attrs() {
|
|
@@ -429,105 +428,36 @@ export default class AtomsElement extends BaseElement {
|
|
|
429
428
|
}, {});
|
|
430
429
|
}
|
|
431
430
|
|
|
432
|
-
useState(initialState) {
|
|
433
|
-
const index = this.hooks.currentCursor++;
|
|
434
|
-
if (this.hooks.values.length <= index) {
|
|
435
|
-
this.hooks.values[index] = [
|
|
436
|
-
typeof initialState === 'function' ? initialState() : initialState,
|
|
437
|
-
|
|
431
|
+
get state() {
|
|
432
|
+
return Object.keys(this.constructor.stateTypes).reduceRight((acc, key) => {
|
|
433
|
+
if (!this._state[key]) {
|
|
438
|
-
|
|
434
|
+
this._state[key] = 0; // TODO: default
|
|
439
|
-
if (typeof nextState === 'function') {
|
|
440
|
-
nextState = nextState(state);
|
|
441
|
-
|
|
435
|
+
}
|
|
442
|
-
|
|
436
|
+
acc[key] = this._state[key];
|
|
437
|
+
acc[`set${key[0].toUpperCase()}${key.slice(1)}`] = (v) => () => {
|
|
443
|
-
|
|
438
|
+
this._state[key] = v;
|
|
444
|
-
|
|
439
|
+
this.update();
|
|
445
|
-
}
|
|
446
|
-
|
|
440
|
+
};
|
|
447
|
-
];
|
|
448
|
-
}
|
|
449
|
-
return this.hooks.values[index];
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
useReducer(reducer, initialState) {
|
|
453
|
-
const index = this.hooks.currentCursor++;
|
|
454
|
-
if (this.hooks.values.length <= index) {
|
|
455
|
-
this.hooks.values[index] = [
|
|
456
|
-
initialState,
|
|
457
|
-
|
|
441
|
+
return acc;
|
|
458
|
-
const state = this.hooks.values[index][0];
|
|
459
|
-
const nextState = reducer(state, action);
|
|
460
|
-
if (!Object.is(state, nextState)) {
|
|
461
|
-
this.hooks.values[index][0] = nextState;
|
|
462
|
-
this.update();
|
|
463
|
-
}
|
|
464
|
-
|
|
442
|
+
}, {});
|
|
465
|
-
];
|
|
466
|
-
}
|
|
467
|
-
return this.hooks.values[index];
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
useEffect(handler, deps) {
|
|
471
|
-
const index = this.hooks.currentCursor++;
|
|
472
|
-
if (!deps || depsChanged(this.hooks.deps[index], deps)) {
|
|
473
|
-
this.hooks.deps[index] = deps || [];
|
|
474
|
-
this.hooks.effects[index] = handler;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
useLayoutEffect(handler, deps) {
|
|
479
|
-
const index = this.hooks.currentCursor++;
|
|
480
|
-
if (!deps || depsChanged(this.hooks.deps[index], deps)) {
|
|
481
|
-
this.hooks.deps[index] = deps || [];
|
|
482
|
-
this.hooks.layoutEffects[index] = handler;
|
|
483
|
-
}
|
|
484
443
|
}
|
|
485
444
|
|
|
486
|
-
|
|
445
|
+
renderTemplate() {
|
|
487
|
-
const
|
|
446
|
+
const template = this.render();
|
|
488
|
-
if (!deps || depsChanged(this.hooks.deps[index], deps)) {
|
|
489
|
-
|
|
447
|
+
const result = render(template, this);
|
|
448
|
+
if (isBrowser) {
|
|
449
|
+
if (!this.stylesMounted) {
|
|
450
|
+
this.appendChild(document.createElement('style')).textContent = normalizeCss + '\n' + this.constructor.styles.toString();
|
|
490
|
-
|
|
451
|
+
this.stylesMounted = true;
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
// ${normalizeCss}
|
|
455
|
+
return `
|
|
456
|
+
${result}
|
|
457
|
+
<style>
|
|
458
|
+
${this.constructor.styles.toString()}
|
|
459
|
+
</style>
|
|
460
|
+
`;
|
|
491
461
|
}
|
|
492
|
-
return this.hooks.values[i];
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
useCallback(callback, deps) {
|
|
496
|
-
return this.useMemo(() => callback, deps);
|
|
497
462
|
}
|
|
498
|
-
|
|
499
|
-
// export const useDispatchEvent = <T>(name: string, eventInit: EventInit = {}) =>
|
|
500
|
-
// hooks<(detail: T) => void>({
|
|
501
|
-
// _onmount: (_, c) => (detail: T) =>
|
|
502
|
-
// c._dispatch(name, { ...eventInit, detail }),
|
|
503
|
-
// });
|
|
504
|
-
|
|
505
|
-
// export const useListenEvent = <T extends Event>(
|
|
506
|
-
// name: string,
|
|
507
|
-
// listener: Listener<T>
|
|
508
|
-
// ) =>
|
|
509
|
-
// hooks<void>({
|
|
510
|
-
// _onmount(h, c, i) {
|
|
511
|
-
// h._cleanup[i] = c._listen(name, listener);
|
|
512
|
-
// },
|
|
513
|
-
// });
|
|
514
|
-
|
|
515
|
-
// useErrorBoundary() {
|
|
516
|
-
// const [error, setError] = this.useState(null);
|
|
517
|
-
// useListenEvent(errorType, (e) => {
|
|
518
|
-
// setError(e.detail);
|
|
519
|
-
// });
|
|
520
|
-
// return [error, () => setError(null)];
|
|
521
|
-
// }
|
|
522
|
-
|
|
523
|
-
// public _dispatch<T>(name: string, init: CustomEventInit<T>) {
|
|
524
|
-
// this.dispatchEvent(new CustomEvent<T>(name, init));
|
|
525
|
-
// }
|
|
526
|
-
|
|
527
|
-
// public _listen<T extends Event>(name: string, listener: Listener<T>) {
|
|
528
|
-
// this.addEventListener(name, listener as EventListener);
|
|
529
|
-
// return () => {
|
|
530
|
-
// this.removeEventListener(name, listener as EventListener);
|
|
531
|
-
// };
|
|
532
|
-
// }
|
|
533
463
|
}
|
element.test.js
CHANGED
|
@@ -241,20 +241,31 @@ test('AtomsElement', async () => {
|
|
|
241
241
|
}).isRequired,
|
|
242
242
|
};
|
|
243
243
|
|
|
244
|
+
static stateTypes = {
|
|
245
|
+
count: number.isRequired,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
static effects = {
|
|
244
|
-
|
|
249
|
+
logEffect: {
|
|
250
|
+
callback: (attrs, state) => {
|
|
251
|
+
console.log(attrs, state);
|
|
252
|
+
},
|
|
253
|
+
deps: ['attrs.perPage', 'state.count'],
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
245
|
-
|
|
257
|
+
static styles = css`
|
|
246
|
-
|
|
258
|
+
div {
|
|
247
|
-
|
|
259
|
+
color: red;
|
|
248
|
-
|
|
260
|
+
}
|
|
249
|
-
|
|
261
|
+
`;
|
|
250
|
-
}
|
|
251
262
|
|
|
252
263
|
render() {
|
|
253
264
|
const {
|
|
254
265
|
perPage,
|
|
255
266
|
address: { street },
|
|
256
267
|
} = this.attrs;
|
|
257
|
-
const
|
|
268
|
+
const { count, setCount } = this.state;
|
|
258
269
|
return html`
|
|
259
270
|
<div perPage=${perPage}>
|
|
260
271
|
<p>street: ${street}</p>
|
|
@@ -273,12 +284,21 @@ test('AtomsElement', async () => {
|
|
|
273
284
|
]);
|
|
274
285
|
instance.renderItem = () => html`<div><p>render item 1</p></div>`;
|
|
275
286
|
expect(AppItem.observedAttributes).toEqual(['perpage', 'address']);
|
|
276
|
-
const res =
|
|
287
|
+
const res = instance.renderTemplate();
|
|
277
288
|
expect(res).toEqual(`
|
|
289
|
+
|
|
278
290
|
<div perPage="1">
|
|
279
291
|
<p>street: 123</p>
|
|
280
292
|
<p>count: 0</p>
|
|
281
293
|
<div><p>render item 1</p></div>
|
|
282
294
|
</div>
|
|
295
|
+
|
|
296
|
+
<style>
|
|
297
|
+
|
|
298
|
+
div {
|
|
299
|
+
color: red;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
</style>
|
|
283
303
|
`);
|
|
284
304
|
});
|
package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atoms-element",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
"atomico",
|
|
20
20
|
"fuco"
|
|
21
21
|
],
|
|
22
|
+
"files": [
|
|
23
|
+
"lit-html.js",
|
|
24
|
+
"element.js",
|
|
25
|
+
"element.d.ts",
|
|
26
|
+
"page.js",
|
|
27
|
+
"page.d.ts"
|
|
28
|
+
],
|
|
22
29
|
"license": "MIT",
|
|
23
30
|
"author": "pyros.sh",
|
|
24
31
|
"type": "module",
|
page.js
CHANGED
|
@@ -15,9 +15,20 @@ export default class Page {
|
|
|
15
15
|
if (AtomsElement.getElement(child.tagName)) {
|
|
16
16
|
const Clazz = AtomsElement.getElement(child.tagName);
|
|
17
17
|
const instance = new Clazz(child.attrs);
|
|
18
|
-
const res =
|
|
18
|
+
const res = instance.renderTemplate();
|
|
19
19
|
const frag = parse5.parseFragment(res);
|
|
20
20
|
child.childNodes.push(...frag.childNodes);
|
|
21
|
+
child.childNodes.push({
|
|
22
|
+
nodeName: 'style',
|
|
23
|
+
tagName: 'style',
|
|
24
|
+
attrs: [],
|
|
25
|
+
childNodes: [
|
|
26
|
+
{
|
|
27
|
+
nodeName: '#text',
|
|
28
|
+
value: Clazz.styles.toString(),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
});
|
|
21
32
|
}
|
|
22
33
|
if (child.childNodes) {
|
|
23
34
|
this.find(child);
|
|
@@ -36,7 +47,7 @@ export default class Page {
|
|
|
36
47
|
const isProd = process.env.NODE_ENV === 'production';
|
|
37
48
|
const props = { config: this.config, data: this.data, item: this.item };
|
|
38
49
|
const headHtml = this.ssr(this.head(props));
|
|
39
|
-
const stylesCss = this.styles(props)
|
|
50
|
+
const stylesCss = this.styles(props);
|
|
40
51
|
const bodyHtml = this.ssr(this.body(props));
|
|
41
52
|
return `
|
|
42
53
|
<!DOCTYPE html>
|