~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.


8f1fc6d5 Peter John

4 years ago
update stuff
__snapshots__/index.test.js.snap ADDED
@@ -0,0 +1,134 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`AtomsElement 1`] = `
4
+ "
5
+
6
+ <div perPage=\\"1\\">
7
+ <p>street: 123</p> <p>count: 0</p> <p>sum: 10</p> <div><p>render item 1</p></div> </div>
8
+ <style>
9
+
10
+ </style>
11
+ "
12
+ `;
13
+
14
+ exports[`Page 1`] = `
15
+ "
16
+ <!DOCTYPE html>
17
+ <html lang=\\"en\\">
18
+ <head>
19
+ <meta charset=\\"utf-8\\" />
20
+ <meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\" />
21
+ <meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=utf-8\\">
22
+ <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no\\">
23
+ <link rel=\\"sitemap\\" type=\\"application/xml\\" href=\\"/sitemap.xml\\" />
24
+ <link rel=\\"icon\\" type=\\"image/png\\" href=\\"/assets/icon.png\\" />
25
+
26
+ <title>123</title> <meta name=\\"title\\" content=\\"123\\"/> <meta name=\\"description\\" content=\\"123\\"/>
27
+ <style>
28
+
29
+ .flex {
30
+ display: flex;
31
+ }
32
+
33
+ .flex-1 {
34
+ flex: 1;
35
+ }
36
+
37
+ .flex-col {
38
+ flex-direction: column;
39
+ }
40
+
41
+ .mt-20 {
42
+ margin-top: 5rem;
43
+ }
44
+
45
+ .items-center {
46
+ align-items: center;
47
+ }
48
+
49
+ .text-5xl {
50
+ font-size: 3rem;
51
+ line-height: 1;
52
+ }
53
+
54
+ </style>
55
+ <script type=\\"module\\"><script>
56
+ </head>
57
+ <body>
58
+
59
+ <div>
60
+ <app-header></app-header> <main class=\\"flex flex-1 flex-col mt-20 items-center\\">
61
+ <h1 class=\\"text-5xl\\">123</h1> </main> </div>
62
+ <script>
63
+ window.__DEV__ = true;
64
+ window.props = {\\"config\\":{\\"title\\":\\"123\\"}};
65
+ </script>
66
+ <script type=\\"module\\"><script>
67
+ </body>
68
+ </html>
69
+ "
70
+ `;
71
+
72
+ exports[`createElement 1`] = `
73
+ "
74
+ <div></div>
75
+ <style>
76
+
77
+ </style>
78
+ "
79
+ `;
80
+
81
+ exports[`css 1`] = `
82
+ "button {
83
+
84
+ color: magenta;
85
+ font-size: 10px;
86
+
87
+ font-size: 64px;
88
+
89
+ color: black;
90
+
91
+
92
+ color: navy;
93
+
94
+ }
95
+ container {
96
+
97
+ flex: 1;
98
+ align-items: center;
99
+ justify-content: center;
100
+
101
+ }
102
+ "
103
+ `;
104
+
105
+ exports[`render 1`] = `
106
+ "
107
+ <div>
108
+ <app-counter name=\\"123\\" class=\\"abc high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
109
+ `;
110
+
111
+ exports[`render attribute keys 1`] = `
112
+ "
113
+ <div>
114
+ <app-counter name=\\"123\\" perPage=\\"1\\"></app-counter> </div>"
115
+ `;
116
+
117
+ exports[`render attributes within quotes 1`] = `
118
+ "
119
+ <div>
120
+ <app-counter name=\\"123\\" class=\\"high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
121
+ `;
122
+
123
+ exports[`render multi template 1`] = `
124
+ "
125
+ <div>
126
+
127
+ <app-item meta=\\"{'index':1}\\">
128
+ <button>+</button> </app-item> <app-item meta=\\"{'index':2}\\">
129
+ <button>+</button> </app-item> </div>"
130
+ `;
131
+
132
+ exports[`render single template 1`] = `" <div>NoCountry false</div>"`;
133
+
134
+ exports[`render unsafeHTML 1`] = `" <div><div><p class=\\"123\\">this is unsafe</p></div></div>"`;
example/app-counter.js CHANGED
@@ -12,7 +12,9 @@ const attrTypes = () => ({
12
12
  const stateTypes = () => ({
13
13
  count: number()
14
14
  .required()
15
- .default((attrs) => attrs.meta?.start || 0),
15
+ .default((attrs) => attrs.meta?.start || 0)
16
+ .action('increment', ({ state }) => state.setCount(state.count + 1))
17
+ .action('decrement', ({ state }) => state.setCount(state.count - 1)),
16
18
  });
17
19
 
18
20
  const computedTypes = () => ({
@@ -21,12 +23,17 @@ const computedTypes = () => ({
21
23
  .compute('count', (count) => {
22
24
  return count + 10;
23
25
  }),
26
+ warningClass: string()
27
+ .required()
28
+ .compute('count', (count) => {
29
+ return count > 10 ? 'text-red-500' : '';
30
+ }),
24
31
  });
25
32
 
26
33
  const render = ({ attrs, state, computed }) => {
27
34
  const { name, meta } = attrs;
28
- const { count, setCount } = state;
35
+ const { count, increment, decrement } = state;
29
- const { sum } = computed;
36
+ const { sum, warningClass } = computed;
30
37
 
31
38
  return html`
32
39
  <div>
@@ -35,15 +42,11 @@ const render = ({ attrs, state, computed }) => {
35
42
  <span>starts at ${meta?.start}</span>
36
43
  </div>
37
44
  <div class="flex flex-1 flex-row items-center text-gray-700">
38
- <button class="bg-gray-300 text-gray-700 rounded hover:bg-gray-200 px-4 py-2 text-3xl focus:outline-none" @click=${() => setCount((v) => v - 1)}>
45
+ <button class="bg-gray-300 text-gray-700 rounded hover:bg-gray-200 px-4 py-2 text-3xl focus:outline-none" @click=${decrement}>-</button>
39
- -
40
- </button>
41
46
  <div class="mx-20">
42
- <h1 class="text-2xl font-mono">${count < 10 ? unsafeHTML('&nbsp;') : ''}${count}</h1>
47
+ <h1 class="text-3xl font-mono ${warningClass}">${count}</h1>
43
48
  </div>
44
- <button class="bg-gray-300 text-gray-700 rounded hover:bg-gray-200 px-4 py-2 text-3xl focus:outline-none" @click=${() => setCount((v) => v + 1)}>
49
+ <button class="bg-gray-300 text-gray-700 rounded hover:bg-gray-200 px-4 py-2 text-3xl focus:outline-none" @click=${increment}>+</button>
45
- +
46
- </button>
47
50
  </div>
48
51
  <div class="mx-20">
49
52
  <h1 class="text-xl font-mono">Sum: ${sum}</h1>
example/{index.js → page.js} RENAMED
File without changes
example/server.js CHANGED
@@ -2,7 +2,7 @@ import http from 'http';
2
2
  import fs from 'fs';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { dirname } from 'path';
5
- import renderIndex from './index.js';
5
+ import renderPage from './page.js';
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = dirname(__filename);
@@ -20,8 +20,11 @@ http
20
20
  if (req.url === '/') {
21
21
  res.statusCode = 200;
22
22
  res.setHeader('Content-type', 'text/html');
23
- const html = renderIndex({
23
+ const html = renderPage({
24
+ lang: 'en',
25
+ props: {
24
- config: { lang: 'en', title: 'Counter App' },
26
+ config: { lang: 'en', title: 'Counter App' },
27
+ },
25
28
  headScript: '',
26
29
  bodyScript: `
27
30
  <script type="module">
index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { html, render as litRender, directive, NodePart, AttributePart, PropertyPart, isPrimitive } from './lit-html.js';
1
+ import { html, render as litRender, directive, NodePart, isPrimitive } from './lit-html.js';
2
2
 
3
3
  const isBrowser = typeof window !== 'undefined';
4
4
  export { html, isBrowser };
@@ -20,20 +20,6 @@ export const css = (obj, isChild = false, indent = '') => {
20
20
  return cssText;
21
21
  };
22
22
 
23
- // const width = Dimensions.get('screen').width;
24
- // let bp = '';
25
- // if (width >= 640 && width <= 768) {
26
- // bp = 'sm';
27
- // } else if (width >= 768 && width < 1024) {
28
- // bp = 'md';
29
- // } else if (width >= 1024 && width < 1280) {
30
- // bp = 'lg';
31
- // } else if (width >= 1280 && width < 1536) {
32
- // bp = 'xl';
33
- // } else if (width >= 1536) {
34
- // bp = '2xl';
35
- // }
36
-
37
23
  const colors = {
38
24
  keys: {
39
25
  bg: 'backgroundColor',
@@ -189,23 +175,30 @@ const spacing = {
189
175
  },
190
176
  };
191
177
 
178
+ const radius = {
179
+ keys: {
192
- // rounded-none border-radius: 0px;
180
+ rounded: 'borderRadius',
193
- // rounded-sm border-radius: 0.125rem;
194
- // rounded border-radius: 0.25rem;
195
- // rounded-md border-radius: 0.375rem;
196
- // rounded-lg border-radius: 0.5rem;
197
- // rounded-xl border-radius: 0.75rem;
198
- // rounded-2xl border-radius: 1rem;
181
+ 'rounded-t': 'borderTopRadius',
182
+ 'rounded-r': 'borderRightRadius',
199
- // rounded-3xl border-radius: 1.5rem;
183
+ 'rounded-l': 'borderLeftRadius',
200
- // rounded-full border-radius: 9999px;
184
+ 'rounded-b': 'borderBottomRadius',
185
+ 'rounded-tl': ['borderTopRadius', 'borderLeftRadius'],
186
+ 'rounded-tr': ['borderTopRadius', 'borderRightRadius'],
187
+ 'rounded-bl': ['borderBottomRadius', 'borderLeftRadius'],
188
+ 'rounded-br': ['borderBottomRadius', 'borderRightRadius'],
189
+ },
190
+ values: {
191
+ none: '0px',
192
+ sm: '0.125rem',
201
- // rounded-t-
193
+ '': '0.25rem',
194
+ md: '0.375rem',
195
+ lg: '0.5rem',
202
- // rounded-r
196
+ xl: '0.75rem',
203
- // rounded-b
204
- // rounded-l
205
- // rounded-tl
197
+ '2xl': '1rem',
206
- // rounded-tr
198
+ '3xl': '1.5rem',
207
- // rounded-br
199
+ full: '9999px',
200
+ },
208
- // rounded-bl
201
+ };
209
202
 
210
203
  const borders = {
211
204
  keys: {
@@ -369,11 +362,11 @@ const classLookup = {
369
362
  ...mapApply(spacing),
370
363
  ...mapApply(colors),
371
364
  ...mapApply(borders),
365
+ ...mapApply(radius),
372
366
  };
373
- // console.log('classLookup', classLookup);
374
367
 
375
- export const getTWStyleSheet = (template) => {
368
+ export const getClassList = (template) => {
376
- const classList = template.strings
369
+ return template.strings
377
370
  .reduce((acc, item) => {
378
371
  const matches = item.match(/class=(?:["']\W+\s*(?:\w+)\()?["']([^'"]+)['"]/gim);
379
372
  if (matches) {
@@ -385,11 +378,14 @@ export const getTWStyleSheet = (template) => {
385
378
  }, '')
386
379
  .split(' ')
387
380
  .filter((it) => it !== '');
381
+ };
382
+
383
+ export const getStyleSheet = (classList) => {
388
- let style = ``;
384
+ let styleSheet = ``;
389
385
  classList.forEach((k) => {
390
386
  const item = classLookup[k];
391
387
  if (item) {
392
- style += `
388
+ styleSheet += `
393
389
  .${k} {
394
390
  ${Object.keys(item)
395
391
  .map((key) => `${key}: ${item[key]};`)
@@ -398,45 +394,15 @@ export const getTWStyleSheet = (template) => {
398
394
  `;
399
395
  }
400
396
  });
401
- return style;
397
+ return styleSheet;
402
398
  };
403
399
 
404
400
  const lastAttributeNameRegex =
405
401
  /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
406
-
407
- const wrapAttribute = (attrName, suffix, text, v) => {
408
- let buffer = text;
409
- const hasQuote = suffix && suffix.includes(`="`);
410
- if (attrName && !hasQuote) {
411
- buffer += `"`;
412
- }
413
- buffer += v;
414
- if (attrName && !hasQuote) {
415
- buffer += `"`;
416
- }
417
- return buffer;
418
- };
419
-
420
402
  const tagRE = /<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
421
403
  const whitespaceRE = /^\s*$/;
422
404
  const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;
423
- const voidElements = {
424
- area: true,
425
- base: true,
426
- br: true,
427
- col: true,
428
- embed: true,
429
- hr: true,
430
- img: true,
431
- input: true,
432
- link: true,
433
- meta: true,
434
- param: true,
435
- source: true,
436
- track: true,
437
- wbr: true,
438
- };
439
- const empty = Object.create(null);
405
+ const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
440
406
 
441
407
  const parseTag = (tag) => {
442
408
  const res = {
@@ -450,7 +416,7 @@ const parseTag = (tag) => {
450
416
  const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
451
417
  if (tagMatch) {
452
418
  res.name = tagMatch[1];
453
- if (voidElements[tagMatch[1]] || tag.charAt(tag.length - 2) === '/') {
419
+ if (voidElements.includes(tagMatch[1]) || tag.charAt(tag.length - 2) === '/') {
454
420
  res.voidElement = true;
455
421
  }
456
422
 
@@ -494,14 +460,11 @@ const parseTag = (tag) => {
494
460
 
495
461
  return res;
496
462
  };
497
- const parseHtml = (html, options) => {
463
+ const parseHtml = (html) => {
498
- options || (options = {});
499
- options.components || (options.components = empty);
500
464
  const result = [];
501
465
  const arr = [];
502
466
  let current;
503
467
  let level = -1;
504
- let inComponent = false;
505
468
 
506
469
  // handle text at top level
507
470
  if (html.indexOf('<') !== 0) {
@@ -513,13 +476,6 @@ const parseHtml = (html, options) => {
513
476
  }
514
477
 
515
478
  html.replace(tagRE, function (tag, index) {
516
- if (inComponent) {
517
- if (tag !== '</' + current.name + '>') {
518
- return;
519
- } else {
520
- inComponent = false;
521
- }
522
- }
523
479
  const isOpen = tag.charAt(1) !== '/';
524
480
  const isComment = tag.startsWith('<!--');
525
481
  const start = index + tag.length;
@@ -543,12 +499,8 @@ const parseHtml = (html, options) => {
543
499
  level++;
544
500
 
545
501
  current = parseTag(tag);
546
- if (current.type === 'tag' && options.components[current.name]) {
547
- current.type = 'component';
548
- inComponent = true;
549
- }
550
502
 
551
- if (!current.voidElement && !inComponent && nextChar && nextChar !== '<') {
503
+ if (!current.voidElement && nextChar && nextChar !== '<') {
552
504
  current.children.push({
553
505
  type: 'text',
554
506
  content: html.slice(start, html.indexOf('<', start)),
@@ -575,7 +527,7 @@ const parseHtml = (html, options) => {
575
527
  // move current up a level to match the end tag
576
528
  current = level === -1 ? result : arr[level];
577
529
  }
578
- if (!inComponent && nextChar !== '<' && nextChar) {
530
+ if (nextChar !== '<' && nextChar) {
579
531
  // trailing text node
580
532
  // if we're at the root, push a base text node. otherwise add as
581
533
  // a child to the current node.
@@ -650,6 +602,19 @@ const hydrate = (node) => {
650
602
  }
651
603
  };
652
604
 
605
+ const wrapAttribute = (attrName, suffix, text, v) => {
606
+ let buffer = text;
607
+ const hasQuote = suffix && suffix.includes(`="`);
608
+ if (attrName && !hasQuote) {
609
+ buffer += `"`;
610
+ }
611
+ buffer += v;
612
+ if (attrName && !hasQuote) {
613
+ buffer += `"`;
614
+ }
615
+ return buffer;
616
+ };
617
+
653
618
  export const render = isBrowser
654
619
  ? litRender
655
620
  : (template) => {
@@ -721,60 +686,6 @@ export const unsafeHTML = isBrowser
721
686
  })
722
687
  : (value) => value;
723
688
 
724
- const previousClassesCache = new WeakMap();
725
- export const classMap = isBrowser
726
- ? directive((classInfo) => (part) => {
727
- if (!(part instanceof AttributePart) || part instanceof PropertyPart || part.committer.name !== 'class' || part.committer.parts.length > 1) {
728
- throw new Error('The `classMap` directive must be used in the `class` attribute ' + 'and must be the only part in the attribute.');
729
- }
730
- const { committer } = part;
731
- const { element } = committer;
732
- let previousClasses = previousClassesCache.get(part);
733
- if (previousClasses === undefined) {
734
- // Write static classes once
735
- // Use setAttribute() because className isn't a string on SVG elements
736
- element.setAttribute('class', committer.strings.join(' '));
737
- previousClassesCache.set(part, (previousClasses = new Set()));
738
- }
739
- const classList = element.classList;
740
- // Remove old classes that no longer apply
741
- // We use forEach() instead of for-of so that re don't require down-level
742
- // iteration.
743
- previousClasses.forEach((name) => {
744
- if (!(name in classInfo)) {
745
- classList.remove(name);
746
- previousClasses.delete(name);
747
- }
748
- });
749
- // Add or remove classes based on their classMap value
750
- for (const name in classInfo) {
751
- const value = classInfo[name];
752
- if (value != previousClasses.has(name)) {
753
- // We explicitly want a loose truthy check of `value` because it seems
754
- // more convenient that '' and 0 are skipped.
755
- if (value) {
756
- classList.add(name);
757
- previousClasses.add(name);
758
- } else {
759
- classList.remove(name);
760
- previousClasses.delete(name);
761
- }
762
- }
763
- }
764
- if (typeof classList.commit === 'function') {
765
- classList.commit();
766
- }
767
- })
768
- : (classes) => {
769
- let value = '';
770
- for (const key in classes) {
771
- if (classes[key]) {
772
- value += `${value.length ? ' ' : ''}${key}`;
773
- }
774
- }
775
- return value;
776
- };
777
-
778
689
  const logError = (msg) => {
779
690
  if (isBrowser ? window.__DEV__ : global.__DEV) {
780
691
  console.warn(msg);
@@ -820,6 +731,13 @@ const validator = (type, validate) => (innerType) => {
820
731
  };
821
732
  return common;
822
733
  };
734
+ common.action = (name, fn) => {
735
+ if (!common.__handlers) {
736
+ common.__handlers = {};
737
+ }
738
+ common.__handlers[name] = fn;
739
+ return common;
740
+ };
823
741
  return common;
824
742
  };
825
743
 
@@ -847,17 +765,7 @@ export const array = validator('array', (innerType, context, data) => {
847
765
  });
848
766
 
849
767
  const fifo = (q) => q.shift();
850
- const filo = (q) => q.pop();
851
768
  const microtask = (flush) => () => queueMicrotask(flush);
852
- const task = (flush) => {
853
- if (isBrowser) {
854
- const ch = new window.MessageChannel();
855
- ch.port1.onmessage = flush;
856
- return () => ch.port2.postMessage(null);
857
- } else {
858
- return () => setImmediate(flush);
859
- }
860
- };
861
769
 
862
770
  const registry = {};
863
771
  const BaseElement = isBrowser ? window.HTMLElement : class {};
@@ -887,13 +795,46 @@ export class AtomsElement extends BaseElement {
887
795
 
888
796
  constructor(ssrAttributes) {
889
797
  super();
798
+ this.ssrAttributes = ssrAttributes;
890
799
  this._dirty = false;
891
800
  this._connected = false;
801
+ this.attrs = {};
892
- this._state = {};
802
+ this.state = {};
893
- this.ssrAttributes = ssrAttributes;
894
803
  this.config = isBrowser ? window.config : global.config;
895
804
  this.location = isBrowser ? window.location : global.location;
805
+ this.prevClassList = [];
806
+ this.initState();
807
+ this.initAttrs();
808
+ }
809
+
810
+ initAttrs() {
811
+ Object.keys(this.constructor.attrTypes).forEach((key) => {
812
+ const attrType = this.constructor.attrTypes[key];
813
+ const newValue = isBrowser ? this.getAttribute(key.toLowerCase()) : this.ssrAttributes[key.toLowerCase()];
814
+ const data = attrType.parse(newValue);
815
+ attrType.validate(`<${this.constructor.name}> ${key}`, data);
896
- this.stylesMounted = false;
816
+ this.attrs[key] = data;
817
+ });
818
+ }
819
+
820
+ initState() {
821
+ Object.keys(this.constructor.stateTypes).forEach((key) => {
822
+ const stateType = this.constructor.stateTypes[key];
823
+ if (!this.state[key] && typeof stateType.__default !== 'undefined') {
824
+ this.state[key] = typeof stateType.__default === 'function' ? stateType.__default(this.attrs, this.state) : stateType.__default;
825
+ }
826
+ const setKey = `set${key[0].toUpperCase()}${key.slice(1)}`;
827
+ this.state[setKey] = (v) => {
828
+ // TODO: check type on set
829
+ this.state[key] = typeof v === 'function' ? v(this.state[key]) : v;
830
+ this.update();
831
+ };
832
+ if (stateType.__handlers) {
833
+ Object.keys(stateType.__handlers).map((hkey) => {
834
+ this.state[hkey] = () => stateType.__handlers[hkey]({ attrs: this.attrs, state: this.state });
835
+ });
836
+ }
837
+ });
897
838
  }
898
839
 
899
840
  connectedCallback() {
@@ -908,6 +849,7 @@ export class AtomsElement extends BaseElement {
908
849
 
909
850
  attributeChangedCallback(key, oldValue, newValue) {
910
851
  if (this._connected) {
852
+ this.initAttrs();
911
853
  this.update();
912
854
  }
913
855
  }
@@ -942,42 +884,15 @@ export class AtomsElement extends BaseElement {
942
884
  this.batch(microtask, fifo, () => this._performUpdate());
943
885
  }
944
886
 
945
- get attrs() {
946
- return Object.keys(this.constructor.attrTypes).reduceRight((acc, key) => {
947
- const attrType = this.constructor.attrTypes[key];
948
- const newValue = isBrowser ? this.getAttribute(key.toLowerCase()) : this.ssrAttributes[key.toLowerCase()];
949
- const data = attrType.parse(newValue);
950
- attrType.validate(`<${this.constructor.name}> ${key}`, data);
951
- acc[key] = data;
952
- return acc;
953
- }, {});
954
- }
955
-
956
- get state() {
957
- return Object.keys(this.constructor.stateTypes).reduceRight((acc, key) => {
958
- const stateType = this.constructor.stateTypes[key];
959
- if (!this._state[key] && typeof stateType.__default !== 'undefined') {
960
- this._state[key] = typeof stateType.__default === 'function' ? stateType.__default(this.attrs, this._state) : stateType.__default;
961
- }
962
- acc[key] = this._state[key];
963
- acc[`set${key[0].toUpperCase()}${key.slice(1)}`] = (v) => {
964
- // TODO: check type on set
965
- this._state[key] = typeof v === 'function' ? v(this._state[key]) : v;
966
- this.update();
967
- };
968
- return acc;
969
- }, {});
970
- }
971
-
972
887
  get computed() {
973
888
  return Object.keys(this.constructor.computedTypes).reduceRight((acc, key) => {
974
889
  const type = this.constructor.computedTypes[key];
975
890
  const state = this.state;
976
- const values = type.__compute.deps.reduce((acc, key) => {
891
+ const values = type.__compute.deps.reduce((dacc, key) => {
977
892
  if (typeof state[key] !== undefined) {
978
- acc.push(state[key]);
893
+ dacc.push(state[key]);
979
894
  }
980
- return acc;
895
+ return dacc;
981
896
  }, []);
982
897
  acc[key] = type.__compute.fn(...values);
983
898
  return acc;
@@ -986,25 +901,45 @@ export class AtomsElement extends BaseElement {
986
901
 
987
902
  renderTemplate() {
988
903
  const template = this.render();
989
- const result = render(template, this);
990
904
  if (isBrowser) {
991
- if (!this.stylesMounted) {
905
+ if (!this.styleElement) {
906
+ render(template, this);
907
+ const classList = getClassList(template);
992
- const twStyles = getTWStyleSheet(template);
908
+ const styleSheet = getStyleSheet(classList);
909
+ this.prevClassList = classList;
993
- this.appendChild(document.createElement('style')).textContent = twStyles;
910
+ this.styleElement = document.createElement('style');
911
+ this.appendChild(this.styleElement).textContent = styleSheet;
912
+ } else {
913
+ const missingClassList = template.values
994
- this.stylesMounted = true;
914
+ .filter((item) => {
915
+ if (typeof item === 'string') {
916
+ const list = item.split(' ');
917
+ return list.filter((cls) => classLookup[cls] && !this.prevClassList.includes(cls)).length > 0;
918
+ }
919
+ return false;
920
+ })
921
+ .reduce((acc, str) => acc.concat(str.split(' ')), []);
922
+ if (missingClassList.length > 0) {
923
+ const styleSheet = getStyleSheet(missingClassList);
924
+ this.styleElement.textContent += '\n' + styleSheet;
925
+ this.prevClassList.push(...missingClassList);
926
+ }
927
+ render(template, this);
995
928
  }
996
929
  } else {
930
+ const result = render(template, this);
931
+ const classList = getClassList(template);
997
- const twStyles = getTWStyleSheet(template);
932
+ const styleSheet = getStyleSheet(classList);
998
933
  return `
999
934
  ${result}
1000
935
  <style>
1001
- ${twStyles}
936
+ ${styleSheet}
1002
937
  </style>
1003
938
  `;
1004
939
  }
1005
940
  }
1006
941
  }
1007
- export const getConfig = () => (isBrowser ? window.config : global.config);
942
+ export const getConfig = () => (isBrowser ? window.props.config : global.config);
1008
943
  export const getLocation = () => (isBrowser ? window.location : global.location);
1009
944
 
1010
945
  export const createElement = ({ name, attrTypes, stateTypes, computedTypes, render }) => {
@@ -1033,15 +968,14 @@ export const createElement = ({ name, attrTypes, stateTypes, computedTypes, rend
1033
968
  };
1034
969
 
1035
970
  export const createPage = ({ route, datapaths, head, body }) => {
1036
- return ({ config, data, item, headScript, bodyScript }) => {
971
+ return ({ headScript, bodyScript, lang, props }) => {
1037
972
  const isProd = process.env.NODE_ENV === 'production';
1038
- const props = { config, data, item };
1039
973
  const headHtml = render(head(props));
1040
974
  const bodyTemplate = body(props);
1041
975
  const bodyHtml = render(bodyTemplate);
1042
976
  return `
1043
977
  <!DOCTYPE html>
1044
- <html lang="${config.lang}">
978
+ <html lang="${lang}">
1045
979
  <head>
1046
980
  <meta charset="utf-8" />
1047
981
  <meta http-equiv="x-ua-compatible" content="ie=edge" />
@@ -1051,7 +985,7 @@ export const createPage = ({ route, datapaths, head, body }) => {
1051
985
  <link rel="icon" type="image/png" href="/assets/icon.png" />
1052
986
  ${headHtml}
1053
987
  <style>
1054
- ${getTWStyleSheet(bodyTemplate)}
988
+ ${getStyleSheet(getClassList(bodyTemplate))}
1055
989
  </style>
1056
990
  ${headScript}
1057
991
  </head>
@@ -1059,9 +993,7 @@ export const createPage = ({ route, datapaths, head, body }) => {
1059
993
  ${bodyHtml}
1060
994
  <script>
1061
995
  window.__DEV__ = ${!isProd};
1062
- window.config = ${JSON.stringify(config)};
1063
- window.data = ${JSON.stringify(data)};
996
+ window.props = ${JSON.stringify(props)};
1064
- window.item = ${JSON.stringify(item)};
1065
997
  </script>
1066
998
  ${bodyScript}
1067
999
  </body>
index.test.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { expect, test, jest } from '@jest/globals';
2
- import { AtomsElement, createElement, createPage, html, render, number, boolean, string, array, object, unsafeHTML, css, classMap } from './index.js';
2
+ import { AtomsElement, createElement, createPage, html, render, number, boolean, string, array, object, unsafeHTML, css } from './index.js';
3
3
 
4
4
  global.__DEV = true;
5
5
 
@@ -132,11 +132,6 @@ test('array', () => {
132
132
 
133
133
  test('css', () => {
134
134
  const styles = css({
135
- __global__: {
136
- html: {
137
- fontSize: '16px',
138
- },
139
- },
140
135
  button: {
141
136
  color: 'magenta',
142
137
  fontSize: '10px',
@@ -158,42 +153,7 @@ test('css', () => {
158
153
  justifyContent: 'center',
159
154
  },
160
155
  });
161
- expect(styles.render()).toEqual(`
156
+ expect(styles).toMatchSnapshot();
162
- html {
163
- font-size: 16px;
164
-
165
- }
166
-
167
-
168
- .button-1t9ijgh {
169
- color: magenta;
170
- font-size: 10px;
171
-
172
- @media screen and (min-width:40em) {
173
- font-size: 64px;
174
-
175
- }
176
- :hover {
177
- color: black;
178
-
179
- }
180
- @media screen and (min-width:56em) {
181
-
182
- :hover {
183
- color: navy;
184
-
185
- }
186
- }
187
- }
188
-
189
- .container-1dvem0h {
190
- flex: 1;
191
- align-items: center;
192
- justify-content: center;
193
-
194
- }
195
-
196
- `);
197
157
  });
198
158
 
199
159
  test('render', async () => {
@@ -207,11 +167,7 @@ test('render', async () => {
207
167
  </div>
208
168
  `;
209
169
  const res = await render(template);
210
- expect(res).toEqual(`
170
+ expect(res).toMatchSnapshot();
211
- <div>
212
- <app-counter name="123" class="abc high" age="1" details1="{'name':'123','address':{'street':'1'}}" items="[1,2,3]"></app-counter>
213
- </div>
214
- `);
215
171
  });
216
172
 
217
173
  test('render attribute keys', async () => {
@@ -221,11 +177,7 @@ test('render attribute keys', async () => {
221
177
  </div>
222
178
  `;
223
179
  const res = await render(template);
224
- expect(res).toEqual(`
180
+ expect(res).toMatchSnapshot();
225
- <div>
226
- <app-counter name="123" perPage="1"></app-counter>
227
- </div>
228
- `);
229
181
  });
230
182
 
231
183
  test('render attributes within quotes', async () => {
@@ -239,38 +191,20 @@ test('render attributes within quotes', async () => {
239
191
  </div>
240
192
  `;
241
193
  const res = await render(template);
242
- expect(res).toEqual(`
194
+ expect(res).toMatchSnapshot();
243
- <div>
244
- <app-counter name="123" class="high" age="1" details1="{'name':'123','address':{'street':'1'}}" items="[1,2,3]"></app-counter>
245
- </div>
246
- `);
247
195
  });
248
196
 
249
197
  test('render unsafeHTML', async () => {
250
198
  const textContent = `<div><p class="123">this is unsafe</p></div>`;
251
199
  const template = html` <div>${unsafeHTML(textContent)}</div> `;
252
200
  const res = await render(template);
253
- expect(res).toEqual(` <div><div><p class="123">this is unsafe</p></div></div> `);
254
- });
255
-
256
- test('render classMap show', async () => {
257
- const hide = false;
258
- const template = html` <div class="abc ${classMap({ show: !hide })}"></div> `;
259
- const res = await render(template);
201
+ expect(res).toMatchSnapshot();
260
- expect(res).toEqual(` <div class="abc show"></div> `);
261
- });
262
-
263
- test('render classMap hide', async () => {
264
- const hide = true;
265
- const template = html` <div class="abc ${classMap({ show: !hide })}"></div> `;
266
- const res = await render(template);
267
- expect(res).toEqual(` <div class="abc "></div> `);
268
202
  });
269
203
 
270
204
  test('render single template', async () => {
271
205
  const template = html` <div>${html`NoCountry ${false}`}</div> `;
272
206
  const res = await render(template);
273
- expect(res).toEqual(` <div>NoCountry false</div> `);
207
+ expect(res).toMatchSnapshot();
274
208
  });
275
209
 
276
210
  test('render multi template', async () => {
@@ -286,19 +220,7 @@ test('render multi template', async () => {
286
220
  </div>
287
221
  `;
288
222
  const res = await render(template);
289
- expect(res).toEqual(`
223
+ expect(res).toMatchSnapshot();
290
- <div>
291
-
292
- <app-item meta="{'index':1}">
293
- <button>+</button>
294
- </app-item>
295
-
296
- <app-item meta="{'index':2}">
297
- <button>+</button>
298
- </app-item>
299
-
300
- </div>
301
- `);
302
224
  });
303
225
 
304
226
  test('AtomsElement', async () => {
@@ -350,31 +272,15 @@ test('AtomsElement', async () => {
350
272
  AppItem.register();
351
273
  const Clazz = AtomsElement.getElement('app-item');
352
274
  expect(Clazz.name).toEqual(AppItem.name);
353
- const instance = new AppItem([
275
+ const instance = new AppItem({
354
- { name: 'address', value: JSON.stringify({ street: '123' }).replace(/"/g, `'`) },
276
+ address: JSON.stringify({ street: '123' }).replace(/"/g, `'`),
355
- { name: 'perpage', value: '1' },
277
+ perpage: '1',
356
- ]);
278
+ });
357
279
  instance.renderItem = () => html`<div><p>render item 1</p></div>`;
358
280
  expect(AppItem.observedAttributes).toEqual(['perpage', 'address']);
281
+ console.log('instance', instance.attrs);
359
282
  const res = instance.renderTemplate();
360
- expect(res).toEqual(`
283
+ expect(res).toMatchSnapshot();
361
-
362
- <div perPage="1">
363
- <p>street: 123</p>
364
- <p>count: 0</p>
365
- <p>sum: 10</p>
366
- <div><p>render item 1</p></div>
367
- </div>
368
-
369
- <style>
370
- .div-1gao8uk {
371
- color: red;
372
-
373
- }
374
-
375
-
376
- </style>
377
- `);
378
284
  });
379
285
 
380
286
  test('createElement ', async () => {
@@ -387,7 +293,7 @@ test('createElement ', async () => {
387
293
  const Clazz = AtomsElement.getElement('base-element');
388
294
  const instance = new Clazz();
389
295
  const res = instance.renderTemplate();
390
- expect(res).toEqual(` <div></div> `);
296
+ expect(res).toMatchSnapshot();
391
297
  });
392
298
 
393
299
  test('Page', () => {
@@ -419,48 +325,6 @@ test('Page', () => {
419
325
  body,
420
326
  });
421
327
  const scripts = '<script type="module"><script>';
422
- const res = renderPage({ config: { lang: 'en', title: '123' }, headScript: scripts, bodyScript: scripts });
328
+ const res = renderPage({ lang: 'en', props: { config: { title: '123' } }, headScript: scripts, bodyScript: scripts });
423
- expect(res).toEqual(`
329
+ expect(res).toMatchSnapshot();
424
- <!DOCTYPE html>
425
- <html lang="en">
426
- <head>
427
- <meta charset="utf-8" />
428
- <meta http-equiv="x-ua-compatible" content="ie=edge" />
429
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
430
- <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no">
431
- <link rel="sitemap" type="application/xml" href="/sitemap.xml" />
432
- <link rel="icon" type="image/png" href="/assets/icon.png" />
433
-
434
- <title>123</title>
435
- <meta name="title" content="123">
436
- <meta name="description" content="123">
437
-
438
- <style>
439
- .div-1gao8uk {
440
- color: red;
441
-
442
- }
443
-
444
- </style>
445
- <script type="module"><script>
446
- </head>
447
- <body>
448
-
449
- <div>
450
- <app-header></app-header>
451
- <main class="flex flex-1 flex-col mt-20 items-center">
452
- <h1 class="text-5xl">123</h1>
453
- </main>
454
- </div>
455
-
456
- <script>
457
- window.__DEV__ = true;
458
- window.config = {"lang":"en","title":"123"};
459
- window.data = undefined;
460
- window.item = undefined;
461
- </script>
462
- <script type="module"><script>
463
- </body>
464
- </html>
465
- `);
466
330
  });