~repos /atoms-element

#js

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
Files changed (4) hide show
  1. element.js +74 -144
  2. element.test.js +29 -9
  3. package.json +8 -1
  4. 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 batch = (runner, pick, callback) => {
298
- const q = [];
299
- const flush = () => {
300
- let p;
301
- while ((p = pick(q))) callback(p);
302
- };
303
- const run = runner(flush);
304
- return (c) => q.push(c) === 1 && run();
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
- return () => queueMicrotask(flush);
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.hooks = {
348
+ this._state = {};
355
- currentCursor: 0,
356
- values: [],
357
- deps: [],
358
- effects: [],
359
- layoutEffects: [],
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
- cleanup();
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(this);
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
- render(this.render(), this);
388
+ this.renderTemplate();
401
- enqueueLayoutEffects(this);
402
- enqueueEffects(this);
389
+ this.enqueueEffects();
403
390
  this._dirty = false;
404
391
  }
405
392
 
406
- _flushEffects(effectKey) {
393
+ batch(runner, pick, callback) {
394
+ const q = [];
407
- const effects = this.hooks[effectKey];
395
+ const flush = () => {
396
+ let p;
408
- const cleanups = this.hooks.cleanup;
397
+ while ((p = pick(q))) callback(p);
409
- for (let i = 0, len = effects.length; i < len; i++) {
410
- if (effects[i]) {
398
+ };
411
- cleanups[i] && cleanups[i]();
412
- const cleanup = effects[i]();
399
+ const run = runner(flush);
413
- if (cleanup) {
414
- cleanups[i] = cleanup;
400
+ q.push(this) === 1 && run();
415
- }
401
+ }
402
+
416
- delete effects[i];
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
- (nextState) => {
431
+ get state() {
432
+ return Object.keys(this.constructor.stateTypes).reduceRight((acc, key) => {
433
+ if (!this._state[key]) {
438
- const state = this.hooks.values[index][0];
434
+ this._state[key] = 0; // TODO: default
439
- if (typeof nextState === 'function') {
440
- nextState = nextState(state);
441
- }
435
+ }
442
- if (!Object.is(state, nextState)) {
436
+ acc[key] = this._state[key];
437
+ acc[`set${key[0].toUpperCase()}${key.slice(1)}`] = (v) => () => {
443
- this.hooks.values[index][0] = nextState;
438
+ this._state[key] = v;
444
- this.update();
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
- (action) => {
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
- useMemo(fn, deps) {
445
+ renderTemplate() {
487
- const index = this.hooks.currentCursor++;
446
+ const template = this.render();
488
- if (!deps || depsChanged(this.hooks.deps[index], deps)) {
489
- this.hooks.deps[index] = deps || [];
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
- this.hooks.values[i] = fn();
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
- styles() {
249
+ logEffect: {
250
+ callback: (attrs, state) => {
251
+ console.log(attrs, state);
252
+ },
253
+ deps: ['attrs.perPage', 'state.count'],
254
+ },
255
+ };
256
+
245
- return css`
257
+ static styles = css`
246
- div {
258
+ div {
247
- color: red;
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 [count] = this.useState(0);
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 = await render(instance.render());
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": "1.2.1",
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 = render(instance.render());
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).cssText;
50
+ const stylesCss = this.styles(props);
40
51
  const bodyHtml = this.ssr(this.body(props));
41
52
  return `
42
53
  <!DOCTYPE html>