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


a3cc1df0 Peter John

4 years ago
improve element and page
element.d.ts CHANGED
@@ -1,19 +1,5 @@
1
1
  import { Config } from './page';
2
2
 
3
- export declare type Destructor = () => void | undefined;
4
- export declare type EffectCallback = () => (void | Destructor);
5
- export declare type SetStateAction<S> = S | ((prevState: S) => S);
6
- export declare type Dispatch<A> = (value: A) => void;
7
- export declare type DispatchWithoutAction = () => void;
8
- export declare type DependencyList = ReadonlyArray<any>;
9
- export declare type Reducer<S, A> = (prevState: S, action: A) => S;
10
- export declare type ReducerWithoutAction<S> = (prevState: S) => S;
11
- export declare type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any> ? S : never;
12
- export declare type ReducerAction<R extends Reducer<any, any>> = R extends Reducer<any, infer A> ? A : never;
13
- export declare type ReducerStateWithoutAction<R extends ReducerWithoutAction<any>> = R extends ReducerWithoutAction<infer S> ? S : never;
14
- export declare interface MutableRefObject<T> {
15
- current: T | null | undefined;
16
- }
17
3
  export declare type Location = {
18
4
  readonly ancestorOrigins: DOMStringList;
19
5
  hash: string;
@@ -38,15 +24,24 @@ export default class AtomsElement {
38
24
  config: Config;
39
25
  location: Location;
40
26
  attrs: {[key: string]: any};
27
+ state: {[key: string]: any};
28
+ computed: {[key: string]: any};
41
29
  styles: () => string;
42
- useState: <S>(initialState: S | (() => S)) => [S, Dispatch<SetStateAction<S>>];
43
- useEffect: (effect: EffectCallback, deps?: DependencyList) => void;
44
- useLayoutEffect: (effect: EffectCallback, deps?: DependencyList) => void;
45
- useReducer: <R extends ReducerWithoutAction<any>, I>(
46
- reducer: R,
47
- initializerArg: I,
48
- initializer: (arg: I) => ReducerStateWithoutAction<R>
49
- ) => [ReducerStateWithoutAction<R>, DispatchWithoutAction];
50
- useCallback: <T extends (...args: any[]) => any>(callback: T, deps: DependencyList) => T;
51
- useMemo: <T>(factory: () => T, deps: DependencyList | undefined) => T;
52
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 CHANGED
@@ -3,72 +3,63 @@ 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();
6
+ // Taken from emotion
7
-
8
- class CSSResult {
9
- constructor(cssText, safeToken) {
7
+ const murmur2 = (str) => {
8
+ var h = 0;
9
+ var k,
10
+ i = 0,
11
+ len = str.length;
10
- if (safeToken !== constructionToken) {
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);
11
- throw new Error('CSSResult is not constructable. Use `unsafeCSS` or `css` instead.');
14
+ k = (k & 0xffff) * 0x5bd1e995 + (((k >>> 16) * 0xe995) << 16);
12
- }
13
- this.cssText = cssText;
15
+ k ^= k >>> 24;
16
+ h = ((k & 0xffff) * 0x5bd1e995 + (((k >>> 16) * 0xe995) << 16)) ^ ((h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0xe995) << 16));
14
17
  }
15
-
16
- toString() {
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:
17
- return this.cssText;
24
+ h ^= str.charCodeAt(i) & 0xff;
25
+ h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0xe995) << 16);
18
26
  }
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
+ h ^= h >>> 13;
27
- */
28
- export const unsafeCSS = (value) => {
28
+ h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0xe995) << 16);
29
- return new CSSResult(String(value), constructionToken);
29
+ return ((h ^ (h >>> 15)) >>> 0).toString(36);
30
30
  };
31
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
32
+ const hyphenate = (s) => s.replace(/[A-Z]|^ms/g, '-$&').toLowerCase();
40
- take care to ensure page security.`,
41
- );
42
- }
43
- };
44
33
 
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) => {
34
+ export const convertStyles = (prefix, obj, parentClassName, indent = '') => {
52
- const cssText = values.reduce((acc, v, idx) => acc + textFromCSSResult(v) + strings[idx + 1], strings[0]);
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`);
53
- return new CSSResult(cssText, constructionToken);
45
+ return { className, cssText: cssText + `\n${indent}}` };
54
46
  };
55
47
 
56
- // const cssSymbol = Symbol();
48
+ export const css = (obj) => {
49
+ Object.keys(obj).forEach((key) => {
50
+ const value = obj[key];
57
- // const hasCSSSymbol = (value: unknown): value is HasCSSSymbol => {
51
+ const { className, cssText } = convertStyles(key, value);
58
- // return value && (value as HasCSSSymbol)[cssSymbol] != null;
59
- // };
52
+ obj[key] = className;
53
+ obj[className] = cssText;
54
+ });
55
+ obj.toString = () => {
60
- // const resolve = (value: unknown): string => {
56
+ return Object.keys(obj).reduce((acc, key) => {
61
- // if (typeof value === "number") return String(value);
62
- // if (hasCSSSymbol(value)) return value[cssSymbol];
63
- // throw new TypeError(`${value} is not supported type.`);
64
- // };
65
- // export const css = (strings: readonly string[], ...values: unknown[]) => ({
66
- // [cssSymbol]: strings
67
- // .slice(1)
68
- // .reduce((acc, s, i) => acc + resolve(values[i]) + s, strings[0]),
57
+ acc += key.includes('-') ? obj[key] + '\n\n' : '';
58
+ return acc;
69
- // });
59
+ }, '');
60
+ };
70
- // export const unsafeCSS = (css: string) => ({ [cssSymbol]: css });
61
+ return obj;
71
- // root.appendChild(document.createElement("style")).textContent = cssStyle;
62
+ };
72
63
 
73
64
  const lastAttributeNameRegex =
74
65
  /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
@@ -275,7 +266,6 @@ export const array = validator('array', (innerType, context, data) => {
275
266
  }
276
267
  });
277
268
 
278
- 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}`;
279
269
  const fifo = (q) => q.shift();
280
270
  const filo = (q) => q.pop();
281
271
  const microtask = (flush) => () => queueMicrotask(flush);
@@ -292,7 +282,7 @@ const task = (flush) => {
292
282
  const registry = {};
293
283
  const BaseElement = isBrowser ? window.HTMLElement : class {};
294
284
 
295
- export default class AtomsElement extends BaseElement {
285
+ export class AtomsElement extends BaseElement {
296
286
  static register() {
297
287
  registry[this.name] = this;
298
288
  if (isBrowser) {
@@ -419,11 +409,10 @@ export default class AtomsElement extends BaseElement {
419
409
  const result = render(template, this);
420
410
  if (isBrowser) {
421
411
  if (!this.stylesMounted) {
422
- this.appendChild(document.createElement('style')).textContent = normalizeCss + '\n' + this.constructor.styles.toString();
412
+ this.appendChild(document.createElement('style')).textContent = this.constructor.styles.toString();
423
413
  this.stylesMounted = true;
424
414
  }
425
415
  } else {
426
- // ${normalizeCss}
427
416
  return `
428
417
  ${result}
429
418
  <style>
@@ -433,3 +422,32 @@ export default class AtomsElement extends BaseElement {
433
422
  }
434
423
  }
435
424
  }
425
+
426
+ 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
+ };
452
+
453
+ export default createElement;
element.test.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { expect, test, jest } from '@jest/globals';
2
- import AtomsElement, { html, render, number, boolean, string, array, object, unsafeHTML, css, classMap } from './element.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
 
@@ -130,6 +130,60 @@ test('array', () => {
130
130
  spy.mockRestore();
131
131
  });
132
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
+
133
187
  test('render', async () => {
134
188
  const age = 1;
135
189
  const data = { name: '123', address: { street: '1' } };
@@ -258,11 +312,11 @@ test('AtomsElement', async () => {
258
312
  }),
259
313
  };
260
314
 
261
- static styles = css`
315
+ static styles = css({
262
- div {
316
+ div: {
263
- color: red;
317
+ color: 'red',
264
- }
318
+ },
265
- `;
319
+ });
266
320
 
267
321
  render() {
268
322
  const {
@@ -301,11 +355,12 @@ test('AtomsElement', async () => {
301
355
  </div>
302
356
 
303
357
  <style>
304
-
305
- div {
358
+ .div-1gao8uk {
306
- color: red;
359
+ color: red;
360
+
307
- }
361
+ }
308
-
362
+
363
+
309
364
  </style>
310
365
  `);
311
366
  });
example/app-counter.js CHANGED
@@ -1,53 +1,87 @@
1
- import AtomsElement, { html, css, object, number, string } from '../element.js';
1
+ import createElement, { html, css, object, number, string } from '../element.js';
2
2
 
3
- class Counter extends AtomsElement {
4
- static name = 'app-counter';
3
+ const name = () => 'app-counter';
5
4
 
6
- static attrTypes = {
5
+ const attrTypes = () => ({
7
- name: string().required(),
6
+ name: string().required(),
8
- meta: object({
7
+ meta: object({
9
- start: number(),
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;
10
23
  }),
11
- };
12
-
13
- static stateTypes = {
14
- count: number()
15
- .required()
16
- .default((attrs) => attrs.meta?.start || 0),
17
- };
18
-
19
- static computedTypes = {
20
- sum: number()
21
- .required()
22
- .compute('count', (count) => {
23
- return count + 10;
24
- }),
24
+ });
25
- };
26
-
27
- static styles = css`
28
- .container {
29
- }
30
- `;
31
25
 
26
+ const styles = css({
27
+ title: {
28
+ fontSize: '24px',
29
+ marginBottom: '0.5rem',
30
+ },
32
- render() {
31
+ container: {
32
+ display: 'flex',
33
+ flex: 1,
34
+ flexDirection: 'row',
35
+ fontSize: '32px',
36
+ color: 'gray',
37
+ },
38
+ mx: {
39
+ marginLeft: '40px',
40
+ marginRight: '40px',
41
+ },
42
+ button: {
43
+ // margin: 0,
44
+ // padding: 0,
45
+ // cursor: 'pointer',
46
+ // backgroundImage: 'none',
47
+ // '-webkitAppearance': 'button',
48
+ // textTransform: 'none',
49
+ // fontSize: '100%',
50
+ paddingTop: '0.5rem',
51
+ paddingBottom: '0.5rem',
52
+ paddingLeft: '1rem',
53
+ paddingRight: '1rem',
54
+ color: 'rgba(55, 65, 81, 1)',
55
+ borderRadius: '0.25rem',
56
+ backgroundColor: 'rgba(209, 213, 219, 1)',
57
+ },
58
+ });
59
+
60
+ const render = ({ attrs, state, computed }) => {
33
- const { name } = this.attrs;
61
+ const { name } = attrs;
34
- const { count, setCount } = this.state;
62
+ const { count, setCount } = state;
35
- const { sum } = this.computed;
63
+ const { sum } = computed;
36
-
64
+
37
- return html`
65
+ return html`
38
- <div>
66
+ <div>
39
- <div class="font-bold mb-2">Counter: ${name}</div>
67
+ <div class=${styles.title}>Counter: ${name}</div>
40
- <div class="flex flex-1 flex-row text-3xl text-gray-700">
68
+ <div class=${styles.container}>
41
- <button @click=${() => setCount((v) => v - 1)}>-</button>
69
+ <button class=${styles.button} @click=${() => setCount((v) => v - 1)}>-</button>
42
- <div class="mx-20">
70
+ <div class=${styles.mx}>
43
- <h1 class="text-1xl">${count}</h1>
71
+ <h1>${count}</h1>
44
- <h1 class="text-1xl">${sum}</h1>
72
+ <h1>${sum}</h1>
45
- </div>
46
- <button @click=${() => setCount((v) => v + 1)}>+</button>
47
73
  </div>
74
+ <button class=${styles.button} @click=${() => setCount((v) => v + 1)}>+</button>
48
75
  </div>
76
+ </div>
49
- `;
77
+ `;
50
- }
78
+ };
51
- }
52
79
 
80
+ export default createElement({
81
+ name,
82
+ attrTypes,
83
+ stateTypes,
53
- Counter.register();
84
+ computedTypes,
85
+ styles,
86
+ render,
87
+ });
example/index.html DELETED
@@ -1,10 +0,0 @@
1
- <html>
2
- <body>
3
- <div>
4
- <app-counter name="1"></app-counter>
5
- </div>
6
- <script type="module">
7
- import './app-counter.js';
8
- </script>
9
- </body>
10
- </html>
example/index.js CHANGED
@@ -1,32 +1,31 @@
1
1
  import { html, css } from '../element.js';
2
- import Page from '../page.js';
2
+ import createPage from '../page.js';
3
3
  import './app-counter.js';
4
4
 
5
- class CounterPage extends Page {
5
+ const route = () => {
6
- route() {
7
- return '/counter';
6
+ return '/counter';
8
- }
7
+ };
9
8
 
10
- datapaths() {
11
- return '/data/items/**';
9
+ const styles = () => css({});
12
- }
13
10
 
14
- styles() {
11
+ const head = ({ config }) => {
15
- return css``;
12
+ return html`
13
+ <title>${config.title}</title>
14
+ <link href="/modern-normalize.css" rel="stylesheet" as="style" />
15
+ `;
16
- }
16
+ };
17
17
 
18
- head({ config }) {
18
+ const body = () => {
19
+ return html`
20
+ <div>
19
- return html` <title>${config.title}</title> `;
21
+ <app-counter name="1" meta="{'start': 5}"></app-counter>
22
+ </div>
23
+ `;
20
- }
24
+ };
21
25
 
26
+ export default createPage({
27
+ route,
28
+ styles,
29
+ head,
22
- body() {
30
+ body,
23
- return html`
24
- <div>
25
- <app-counter name="1"></app-counter>
26
- </div>
27
- `;
31
+ });
28
- }
29
- }
30
-
31
- const counterPage = new CounterPage({ config: { lang: 'en', title: 'Counter App' } });
32
- console.log(counterPage.render());
example/main.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
+ '/modern-normalize.css': `${__dirname}/modern-normalize.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/modern-normalize.css ADDED
@@ -0,0 +1,271 @@
1
+ /*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */
2
+
3
+ /*
4
+ Document
5
+ ========
6
+ */
7
+
8
+ /**
9
+ Use a better box model (opinionated).
10
+ */
11
+
12
+ *,
13
+ ::before,
14
+ ::after {
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ /**
19
+ 1. Correct the line height in all browsers.
20
+ 2. Prevent adjustments of font size after orientation changes in iOS.
21
+ 3. Use a more readable tab size (opinionated).
22
+ */
23
+
24
+ html {
25
+ line-height: 1.15; /* 1 */
26
+ -webkit-text-size-adjust: 100%; /* 2 */
27
+ -moz-tab-size: 4; /* 3 */
28
+ tab-size: 4; /* 3 */
29
+ }
30
+
31
+ /*
32
+ Sections
33
+ ========
34
+ */
35
+
36
+ /**
37
+ 1. Remove the margin in all browsers.
38
+ 2. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
39
+ */
40
+
41
+ body {
42
+ margin: 0; /* 1 */
43
+ font-family: system-ui, -apple-system, /* Firefox supports this but not yet system-ui */ 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji',
44
+ 'Segoe UI Emoji'; /* 2 */
45
+ }
46
+
47
+ /*
48
+ Grouping content
49
+ ================
50
+ */
51
+
52
+ /**
53
+ 1. Add the correct height in Firefox.
54
+ 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
55
+ */
56
+
57
+ hr {
58
+ height: 0; /* 1 */
59
+ color: inherit; /* 2 */
60
+ }
61
+
62
+ /*
63
+ Text-level semantics
64
+ ====================
65
+ */
66
+
67
+ /**
68
+ Add the correct text decoration in Chrome, Edge, and Safari.
69
+ */
70
+
71
+ abbr[title] {
72
+ text-decoration: underline dotted;
73
+ }
74
+
75
+ /**
76
+ Add the correct font weight in Edge and Safari.
77
+ */
78
+
79
+ b,
80
+ strong {
81
+ font-weight: bolder;
82
+ }
83
+
84
+ /**
85
+ 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
86
+ 2. Correct the odd 'em' font sizing in all browsers.
87
+ */
88
+
89
+ code,
90
+ kbd,
91
+ samp,
92
+ pre {
93
+ font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; /* 1 */
94
+ font-size: 1em; /* 2 */
95
+ }
96
+
97
+ /**
98
+ Add the correct font size in all browsers.
99
+ */
100
+
101
+ small {
102
+ font-size: 80%;
103
+ }
104
+
105
+ /**
106
+ Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.
107
+ */
108
+
109
+ sub,
110
+ sup {
111
+ font-size: 75%;
112
+ line-height: 0;
113
+ position: relative;
114
+ vertical-align: baseline;
115
+ }
116
+
117
+ sub {
118
+ bottom: -0.25em;
119
+ }
120
+
121
+ sup {
122
+ top: -0.5em;
123
+ }
124
+
125
+ /*
126
+ Tabular data
127
+ ============
128
+ */
129
+
130
+ /**
131
+ 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)
132
+ 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)
133
+ */
134
+
135
+ table {
136
+ text-indent: 0; /* 1 */
137
+ border-color: inherit; /* 2 */
138
+ }
139
+
140
+ /*
141
+ Forms
142
+ =====
143
+ */
144
+
145
+ /**
146
+ 1. Change the font styles in all browsers.
147
+ 2. Remove the margin in Firefox and Safari.
148
+ */
149
+
150
+ button,
151
+ input,
152
+ optgroup,
153
+ select,
154
+ textarea {
155
+ font-family: inherit; /* 1 */
156
+ font-size: 100%; /* 1 */
157
+ line-height: 1.15; /* 1 */
158
+ margin: 0; /* 2 */
159
+ }
160
+
161
+ /**
162
+ Remove the inheritance of text transform in Edge and Firefox.
163
+ */
164
+
165
+ button,
166
+ select {
167
+ text-transform: none;
168
+ }
169
+
170
+ /**
171
+ Correct the inability to style clickable types in iOS and Safari.
172
+ */
173
+
174
+ button,
175
+ [type='button'],
176
+ [type='reset'],
177
+ [type='submit'] {
178
+ -webkit-appearance: button;
179
+ }
180
+
181
+ /**
182
+ Remove the inner border and padding in Firefox.
183
+ */
184
+
185
+ ::-moz-focus-inner {
186
+ border-style: none;
187
+ padding: 0;
188
+ }
189
+
190
+ /**
191
+ Restore the focus styles unset by the previous rule.
192
+ */
193
+
194
+ :-moz-focusring {
195
+ outline: 1px dotted ButtonText;
196
+ }
197
+
198
+ /**
199
+ Remove the additional ':invalid' styles in Firefox.
200
+ See: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737
201
+ */
202
+
203
+ :-moz-ui-invalid {
204
+ box-shadow: none;
205
+ }
206
+
207
+ /**
208
+ Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.
209
+ */
210
+
211
+ legend {
212
+ padding: 0;
213
+ }
214
+
215
+ /**
216
+ Add the correct vertical alignment in Chrome and Firefox.
217
+ */
218
+
219
+ progress {
220
+ vertical-align: baseline;
221
+ }
222
+
223
+ /**
224
+ Correct the cursor style of increment and decrement buttons in Safari.
225
+ */
226
+
227
+ ::-webkit-inner-spin-button,
228
+ ::-webkit-outer-spin-button {
229
+ height: auto;
230
+ }
231
+
232
+ /**
233
+ 1. Correct the odd appearance in Chrome and Safari.
234
+ 2. Correct the outline style in Safari.
235
+ */
236
+
237
+ [type='search'] {
238
+ -webkit-appearance: textfield; /* 1 */
239
+ outline-offset: -2px; /* 2 */
240
+ }
241
+
242
+ /**
243
+ Remove the inner padding in Chrome and Safari on macOS.
244
+ */
245
+
246
+ ::-webkit-search-decoration {
247
+ -webkit-appearance: none;
248
+ }
249
+
250
+ /**
251
+ 1. Correct the inability to style clickable types in iOS and Safari.
252
+ 2. Change font properties to 'inherit' in Safari.
253
+ */
254
+
255
+ ::-webkit-file-upload-button {
256
+ -webkit-appearance: button; /* 1 */
257
+ font: inherit; /* 2 */
258
+ }
259
+
260
+ /*
261
+ Interactive
262
+ ===========
263
+ */
264
+
265
+ /*
266
+ Add the correct display in Chrome and Safari.
267
+ */
268
+
269
+ summary {
270
+ display: list-item;
271
+ }
package-lock.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "atoms-element",
3
- "version": "1.2.1",
3
+ "version": "2.0.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
- "version": "1.2.1",
8
+ "version": "2.0.0",
9
9
  "license": "MIT",
10
10
  "dependencies": {
11
11
  "parse5": "^6.0.1"
package.json CHANGED
@@ -33,7 +33,7 @@
33
33
  "node": ">=14.0.0"
34
34
  },
35
35
  "scripts": {
36
- "example": "npx serve ./",
36
+ "example": "node example/main.js",
37
37
  "test": "NODE_OPTIONS=--experimental-vm-modules jest"
38
38
  },
39
39
  "devDependencies": {
page.js CHANGED
@@ -1,57 +1,38 @@
1
1
  import parse5 from 'parse5';
2
- import AtomsElement, { render } from './element.js';
2
+ import { AtomsElement, render } from './element.js';
3
3
 
4
- export default class Page {
4
+ const find = (node) => {
5
+ for (const child of node.childNodes) {
6
+ if (AtomsElement.getElement(child.tagName)) {
5
- constructor({ config, data, item, headScript, bodyScript }) {
7
+ const Clazz = AtomsElement.getElement(child.tagName);
6
- this.config = config;
7
- this.data = data;
8
+ const instance = new Clazz(child.attrs);
8
- this.item = item;
9
- this.headScript = headScript;
9
+ const res = instance.renderTemplate();
10
+ const frag = parse5.parseFragment(res);
10
- this.bodyScript = bodyScript;
11
+ child.childNodes.push(...frag.childNodes);
11
- }
12
+ }
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 = instance.renderTemplate();
19
- const frag = parse5.parseFragment(res);
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
- });
32
- }
33
- if (child.childNodes) {
13
+ if (child.childNodes) {
34
- this.find(child);
14
+ find(child);
35
- }
36
15
  }
37
16
  }
17
+ };
38
18
 
39
- ssr(template) {
19
+ const ssr = (template) => {
40
- const text = render(template);
20
+ const text = render(template);
41
- const h = parse5.parseFragment(text);
21
+ const h = parse5.parseFragment(text);
42
- this.find(h);
22
+ find(h);
43
- return parse5.serialize(h);
23
+ return parse5.serialize(h);
44
- }
24
+ };
45
25
 
26
+ const createPage = ({ route, datapaths, head, body, styles }) => {
46
- render() {
27
+ return ({ config, data, item, headScript, bodyScript }) => {
47
28
  const isProd = process.env.NODE_ENV === 'production';
48
- const props = { config: this.config, data: this.data, item: this.item };
29
+ const props = { config, data, item };
49
- const headHtml = this.ssr(this.head(props));
30
+ const headHtml = ssr(head(props));
31
+ const bodyHtml = ssr(body(props));
50
- const stylesCss = this.styles(props);
32
+ const stylesCss = styles(props);
51
- const bodyHtml = this.ssr(this.body(props));
52
33
  return `
53
34
  <!DOCTYPE html>
54
- <html lang="${this.config.lang}">
35
+ <html lang="${config.lang}">
55
36
  <head>
56
37
  <meta charset="utf-8" />
57
38
  <meta http-equiv="x-ua-compatible" content="ie=edge" />
@@ -61,21 +42,23 @@ export default class Page {
61
42
  <link rel="icon" type="image/png" href="/assets/icon.png" />
62
43
  ${headHtml}
63
44
  <style>
64
- ${stylesCss}
45
+ ${stylesCss.toString()}
65
46
  </style>
66
- ${this.headScript}
47
+ ${headScript}
67
48
  </head>
68
49
  <body>
69
50
  ${bodyHtml}
70
51
  <script>
71
52
  window.__DEV__ = ${!isProd};
72
- window.config = ${JSON.stringify(this.config)};
53
+ window.config = ${JSON.stringify(config)};
73
- window.data = ${JSON.stringify(this.data)};
54
+ window.data = ${JSON.stringify(data)};
74
- window.item = ${JSON.stringify(this.item)};
55
+ window.item = ${JSON.stringify(item)};
75
56
  </script>
76
- ${this.bodyScript}
57
+ ${bodyScript}
77
58
  </body>
78
59
  </html>
79
60
  `;
80
- }
61
+ };
81
- }
62
+ };
63
+
64
+ export default createPage;
page.test.js CHANGED
@@ -1,46 +1,44 @@
1
1
  import { expect, test } from '@jest/globals';
2
2
  import { html, css } from './element.js';
3
- import Page from './page.js';
3
+ import createPage from './page.js';
4
4
 
5
5
  test('Page', () => {
6
- class MainPage extends Page {
6
+ const route = () => {
7
- route() {
8
- const langPart = this.config.lang === 'en' ? '' : `/${this.config.lang}`;
7
+ const langPart = this.config.lang === 'en' ? '' : `/${this.config.lang}`;
9
- return `${langPart}`;
8
+ return `${langPart}`;
10
- }
9
+ };
10
+ const styles = () =>
11
+ css({
12
+ div: {
13
+ color: 'red',
14
+ },
15
+ });
16
+ const head = ({ config }) => {
17
+ return html`
18
+ <title>${config.title}</title>
19
+ <meta name="title" content=${config.title} />
20
+ <meta name="description" content=${config.title} />
21
+ `;
22
+ };
11
23
 
12
- styles() {
24
+ const body = ({ config }) => {
13
- return css`
25
+ return html`
14
- div {
15
- color: red;
26
+ <div>
27
+ <app-header></app-header>
28
+ <main class="flex flex-1 flex-col mt-20 items-center">
29
+ <h1 class="text-5xl">${config.title}</h1>
30
+ </main>
16
- }
31
+ </div>
17
- `;
32
+ `;
18
- }
19
-
20
- head() {
21
- const { title } = this.config;
22
- return html`
23
- <title>${title}</title>
24
- <meta name="title" content=${title} />
25
- <meta name="description" content=${title} />
26
- `;
33
+ };
34
+ const renderPage = createPage({
35
+ route,
27
- }
36
+ head,
28
-
29
- body() {
37
+ body,
30
- const { title } = this.config;
31
- return html`
32
- <div>
38
+ styles,
33
- <app-header></app-header>
34
- <main class="flex flex-1 flex-col mt-20 items-center">
35
- <h1 class="text-5xl">${title}</h1>
36
- </main>
37
- </div>
38
- `;
39
+ });
39
- }
40
- }
41
40
  const scripts = '<script type="module"><script>';
42
- const mainPage = new MainPage({ config: { lang: 'en', title: '123' }, headScript: scripts, bodyScript: scripts });
41
+ const res = renderPage({ config: { lang: 'en', title: '123' }, headScript: scripts, bodyScript: scripts });
43
- const res = mainPage.render();
44
42
  expect(res).toEqual(`
45
43
  <!DOCTYPE html>
46
44
  <html lang="en">
@@ -52,28 +50,29 @@ test('Page', () => {
52
50
  <link rel="sitemap" type="application/xml" href="/sitemap.xml" />
53
51
  <link rel="icon" type="image/png" href="/assets/icon.png" />
54
52
 
55
- <title>123</title>
53
+ <title>123</title>
56
- <meta name="title" content="123">
54
+ <meta name="title" content="123">
57
- <meta name="description" content="123">
55
+ <meta name="description" content="123">
58
-
56
+
59
57
  <style>
60
-
61
- div {
58
+ .div-1gao8uk {
62
- color: red;
59
+ color: red;
60
+
63
- }
61
+ }
64
-
62
+
63
+
65
64
  </style>
66
65
  <script type="module"><script>
67
66
  </head>
68
67
  <body>
69
68
 
70
- <div>
69
+ <div>
71
- <app-header></app-header>
70
+ <app-header></app-header>
72
- <main class="flex flex-1 flex-col mt-20 items-center">
71
+ <main class="flex flex-1 flex-col mt-20 items-center">
73
- <h1 class="text-5xl">123</h1>
72
+ <h1 class="text-5xl">123</h1>
74
- </main>
73
+ </main>
75
- </div>
74
+ </div>
76
-
75
+
77
76
  <script>
78
77
  window.__DEV__ = true;
79
78
  window.config = {"lang":"en","title":"123"};