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


1d4524c3 Peter John

4 years ago
improve structure
src/index.d.ts → element.d.ts RENAMED
File without changes
src/index.js → element.js RENAMED
@@ -3,6 +3,56 @@ import { html, render as litRender, directive, NodePart, AttributePart, Property
3
3
  const isBrowser = typeof window !== 'undefined';
4
4
  export { html, isBrowser };
5
5
 
6
+ const constructionToken = Symbol();
7
+
8
+ export class CSSResult {
9
+ constructor(cssText, safeToken) {
10
+ if (safeToken !== constructionToken) {
11
+ throw new Error('CSSResult is not constructable. Use `unsafeCSS` or `css` instead.');
12
+ }
13
+ this.cssText = cssText;
14
+ }
15
+
16
+ toString() {
17
+ return this.cssText;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Wrap a value for interpolation in a [[`css`]] tagged template literal.
23
+ *
24
+ * This is unsafe because untrusted CSS text can be used to phone home
25
+ * or exfiltrate data to an attacker controlled site. Take care to only use
26
+ * this with trusted input.
27
+ */
28
+ export const unsafeCSS = (value) => {
29
+ return new CSSResult(String(value), constructionToken);
30
+ };
31
+
32
+ const textFromCSSResult = (value) => {
33
+ if (value instanceof CSSResult) {
34
+ return value.cssText;
35
+ } else if (typeof value === 'number') {
36
+ return value;
37
+ } else {
38
+ throw new Error(
39
+ `Value passed to 'css' function must be a 'css' function result: ${value}. Use 'unsafeCSS' to pass non-literal values, but
40
+ take care to ensure page security.`,
41
+ );
42
+ }
43
+ };
44
+
45
+ /**
46
+ * Template tag which which can be used with LitElement's [[LitElement.styles |
47
+ * `styles`]] property to set element styles. For security reasons, only literal
48
+ * string values may be used. To incorporate non-literal values [[`unsafeCSS`]]
49
+ * may be used inside a template string part.
50
+ */
51
+ export const css = (strings, ...values) => {
52
+ const cssText = values.reduce((acc, v, idx) => acc + textFromCSSResult(v) + strings[idx + 1], strings[0]);
53
+ return new CSSResult(cssText, constructionToken);
54
+ };
55
+
6
56
  const lastAttributeNameRegex =
7
57
  /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
8
58
 
@@ -310,10 +360,10 @@ const enqueueLayoutEffects = batch(microtask, filo, (c) => c._flushEffects('layo
310
360
  const enqueueEffects = batch(task, filo, (c) => c._flushEffects('effects'));
311
361
  const enqueueUpdate = batch(microtask, fifo, (c) => c._performUpdate());
312
362
 
313
- const BaseElement = isBrowser ? window.HTMLElement : class {};
314
- const count = [0];
315
363
  const registry = {};
364
+ const BaseElement = isBrowser ? window.HTMLElement : class {};
365
+
316
- export class AtomsElement extends BaseElement {
366
+ export default class AtomsElement extends BaseElement {
317
367
  static register() {
318
368
  registry[this.name] = this;
319
369
  if (isBrowser) {
@@ -329,23 +379,15 @@ export class AtomsElement extends BaseElement {
329
379
  return registry[name];
330
380
  }
331
381
 
332
- static getNextIndex() {
333
- count[0] = count[0] + 1;
334
- return count[0];
335
- }
336
-
337
382
  static get observedAttributes() {
338
383
  if (!this.attrTypes) {
339
384
  return [];
340
385
  }
341
- return Object.keys(this.attrTypes)
386
+ return Object.keys(this.attrTypes).map((k) => k.toLowerCase());
342
- .filter((key) => this.attrTypes[key].type !== 'function')
343
- .map((k) => k.toLowerCase());
344
387
  }
345
388
 
346
389
  constructor(attrs) {
347
390
  super();
348
- this._id = `atoms:${this.constructor.getNextIndex()}`;
349
391
  this._dirty = false;
350
392
  this._connected = false;
351
393
  this.hooks = {
@@ -444,15 +486,13 @@ export class AtomsElement extends BaseElement {
444
486
  }
445
487
 
446
488
  getAttrs() {
447
- return Object.keys(this.constructor.attrTypes)
489
+ return Object.keys(this.constructor.attrTypes).reduceRight((acc, key) => {
448
- .filter((key) => this.constructor.attrTypes[key].type !== 'function')
449
- .reduceRight((acc, key) => {
450
- const attrType = this.constructor.attrTypes[key];
490
+ const attrType = this.constructor.attrTypes[key];
451
- const newValue = isBrowser ? this.getAttribute(key.toLowerCase()) : this.attrs.find((item) => item.name === key.toLowerCase()).value;
491
+ const newValue = isBrowser ? this.getAttribute(key.toLowerCase()) : this.attrs.find((item) => item.name === key.toLowerCase())?.value;
452
- const data = attrType.parse(newValue);
492
+ const data = attrType.parse(newValue);
453
- attrType.validate(`<${this.constructor.name}> ${key}`, data);
493
+ attrType.validate(`<${this.constructor.name}> ${key}`, data);
454
- acc[key] = data;
494
+ acc[key] = data;
455
- return acc;
495
+ return acc;
456
- }, {});
496
+ }, {});
457
497
  }
458
498
  }
test/index.test.js → element.test.js RENAMED
@@ -1,5 +1,5 @@
1
1
  import { expect, test, jest } from '@jest/globals';
2
- import { AtomsElement, html, render, number, boolean, string, array, func, object, unsafeHTML, classMap } from '../src/index.js';
2
+ import AtomsElement, { html, render, number, boolean, string, array, object, unsafeHTML, css, classMap } from './element.js';
3
3
 
4
4
  global.__DEV = true;
5
5
 
@@ -239,10 +239,15 @@ test('AtomsElement', async () => {
239
239
  address: object({
240
240
  street: string.isRequired,
241
241
  }).isRequired,
242
- renderItem: func(),
243
242
  };
244
243
 
244
+ styles() {
245
- static css = ``;
245
+ return css`
246
+ div {
247
+ color: red;
248
+ }
249
+ `;
250
+ }
246
251
 
247
252
  render() {
248
253
  const {
example/index.js ADDED
@@ -0,0 +1,40 @@
1
+ import AtomsElement, { html, object, number, string } from '../element.js';
2
+ import Page from '../page.js';
3
+
4
+ class Counter extends AtomsElement {
5
+ static name = 'app-counter';
6
+
7
+ static attrTypes = {
8
+ name: string.isRequired,
9
+ meta: object({
10
+ start: number,
11
+ }),
12
+ };
13
+
14
+ static styles = `
15
+ .container {
16
+ }
17
+ `;
18
+
19
+ render() {
20
+ const { name, meta } = this.getAttrs();
21
+ const [count, setCount] = this.useState(meta?.start || 0);
22
+
23
+ return html`
24
+ <div>
25
+ <div class="font-bold mb-2">Counter: ${name}</div>
26
+ <div class="flex flex-1 flex-row text-3xl text-gray-700">
27
+ <button @click=${() => setCount((v) => v - 1)}>-</button>
28
+ <div class="mx-20">
29
+ <h1 class="text-1xl">${count}</h1>
30
+ </div>
31
+ <button @click=${() => setCount((v) => v + 1)}>+</button>
32
+ </div>
33
+ </div>
34
+ `;
35
+ }
36
+ }
37
+
38
+ Counter.register();
39
+
40
+ console.log(ssr(html`<app-counter name="1"></app-counter>`));
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>`));
src/lit-html.js → lit-html.js RENAMED
File without changes
package.json CHANGED
@@ -20,12 +20,6 @@
20
20
  "fuco"
21
21
  ],
22
22
  "license": "MIT",
23
- "main": "src/index.js",
24
- "files": [
25
- "src",
26
- "example"
27
- ],
28
- "typings": "src/index.d.ts",
29
23
  "author": "pyros.sh",
30
24
  "type": "module",
31
25
  "engines": {
page.js ADDED
@@ -0,0 +1,70 @@
1
+ import parse5 from 'parse5';
2
+ import AtomsElement, { render } from './element.js';
3
+
4
+ export default class Page {
5
+ constructor({ config, data, item, headScript, bodyScript }) {
6
+ this.config = config;
7
+ this.data = data;
8
+ this.item = item;
9
+ this.headScript = headScript;
10
+ this.bodyScript = bodyScript;
11
+ }
12
+
13
+ find(node) {
14
+ for (const child of node.childNodes) {
15
+ if (AtomsElement.getElement(child.tagName)) {
16
+ const Clazz = AtomsElement.getElement(child.tagName);
17
+ const instance = new Clazz(child.attrs);
18
+ const res = render(instance.render());
19
+ const frag = parse5.parseFragment(res);
20
+ child.childNodes.push(...frag.childNodes);
21
+ }
22
+ if (child.childNodes) {
23
+ this.find(child);
24
+ }
25
+ }
26
+ }
27
+
28
+ ssr(template) {
29
+ const text = render(template);
30
+ const h = parse5.parseFragment(text);
31
+ this.find(h);
32
+ return parse5.serialize(h);
33
+ }
34
+
35
+ render() {
36
+ const isProd = process.env.NODE_ENV === 'production';
37
+ const props = { config: this.config, data: this.data, item: this.item };
38
+ const headHtml = this.ssr(this.head(props));
39
+ const stylesCss = this.styles(props).cssText;
40
+ const bodyHtml = this.ssr(this.body(props));
41
+ return `
42
+ <!DOCTYPE html>
43
+ <html lang="${this.config.lang}">
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
+ ${headHtml}
52
+ <style>
53
+ ${stylesCss}
54
+ </style>
55
+ ${this.headScript}
56
+ </head>
57
+ <body>
58
+ ${bodyHtml}
59
+ <script>
60
+ window.__DEV__ = ${!isProd};
61
+ window.config = ${JSON.stringify(this.config)};
62
+ window.data = ${JSON.stringify(this.data)};
63
+ window.item = ${JSON.stringify(this.item)};
64
+ </script>
65
+ ${this.bodyScript}
66
+ </body>
67
+ </html>
68
+ `;
69
+ }
70
+ }
page.test.js ADDED
@@ -0,0 +1,85 @@
1
+ import { expect, test } from '@jest/globals';
2
+ import { html, css } from './element.js';
3
+ import Page from './page.js';
4
+
5
+ test('Page', () => {
6
+ class MainPage extends Page {
7
+ route({ config }) {
8
+ const langPart = config.lang === 'en' ? '' : `/${config.lang}`;
9
+ return `${langPart}`;
10
+ }
11
+
12
+ styles({ config }) {
13
+ return css`
14
+ div {
15
+ color: red;
16
+ }
17
+ `;
18
+ }
19
+
20
+ head({ config }) {
21
+ return html`
22
+ <title>${config.title}</title>
23
+ <meta name="title" content=${config.title} />
24
+ <meta name="description" content=${config.title} />
25
+ `;
26
+ }
27
+
28
+ body({ config }) {
29
+ return html`
30
+ <div>
31
+ <app-header></app-header>
32
+ <main class="flex flex-1 flex-col mt-20 items-center">
33
+ <h1 class="text-5xl">${config.title}</h1>
34
+ </main>
35
+ </div>
36
+ `;
37
+ }
38
+ }
39
+ const scripts = '<script type="module"><script>';
40
+ const mainPage = new MainPage({ config: { lang: 'en', title: '123' }, headScript: scripts, bodyScript: scripts });
41
+ const res = mainPage.render();
42
+ expect(res).toEqual(`
43
+ <!DOCTYPE html>
44
+ <html lang="en">
45
+ <head>
46
+ <meta charset="utf-8" />
47
+ <meta http-equiv="x-ua-compatible" content="ie=edge" />
48
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
49
+ <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no">
50
+ <link rel="sitemap" type="application/xml" href="/sitemap.xml" />
51
+ <link rel="icon" type="image/png" href="/assets/icon.png" />
52
+
53
+ <title>123</title>
54
+ <meta name="title" content="123">
55
+ <meta name="description" content="123">
56
+
57
+ <style>
58
+
59
+ div {
60
+ color: red;
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
+ });
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
- };