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


24398b1b Peter John

4 years ago
improve element and example
example/app-counter.js DELETED
@@ -1,43 +0,0 @@
1
- import { createElement, html } from '../index.js';
2
-
3
- export default createElement({
4
- name: 'app-counter',
5
- attrs: {
6
- name: String,
7
- meta: {
8
- start: String,
9
- },
10
- },
11
- state: {
12
- count: 0,
13
- },
14
- reducer: {
15
- increment: (state) => ({ ...state, count: state.count + 1 }),
16
- decrement: (state) => ({ ...state, count: state.count - 1 }),
17
- },
18
- render: ({ attrs, state, actions }) => {
19
- const { name, meta } = attrs;
20
- const { count } = state;
21
- const sum = count + 10;
22
- const warningClass = count > 10 ? 'text-red-500' : '';
23
-
24
- return html`
25
- <div>
26
- <div class="mb-2">
27
- Counter: ${name}
28
- <span>starts at ${meta?.start}</span>
29
- </div>
30
- <div class="flex flex-1 flex-row items-center text-gray-700">
31
- <button class="bg-gray-300 text-gray-700 rounded hover:bg-gray-200 px-4 py-2 text-3xl focus:outline-none" @click=${actions.decrement}>-</button>
32
- <div class="mx-20">
33
- <h1 class="text-3xl font-mono ${warningClass}">${count}</h1>
34
- </div>
35
- <button class="bg-gray-300 text-gray-700 rounded hover:bg-gray-200 px-4 py-2 text-3xl focus:outline-none" @click=${actions.increment}>+</button>
36
- </div>
37
- <div class="mx-20">
38
- <h1 class="text-xl font-mono">Sum: ${sum}</h1>
39
- </div>
40
- </div>
41
- `;
42
- },
43
- });
example/elements/app-counter.js ADDED
@@ -0,0 +1,41 @@
1
+ import { createElement, html, useReducer } from '../../index.js';
2
+ import { totalReducer } from '../store.js';
3
+
4
+ const Counter = ({ name, meta }) => {
5
+ const { count, actions } = useReducer({
6
+ initial: {
7
+ count: 0,
8
+ },
9
+ reducer: {
10
+ increment: (state) => ({ ...state, count: state.count + 1 }),
11
+ decrement: (state) => ({ ...state, count: state.count - 1 }),
12
+ },
13
+ });
14
+ const increment = () => {
15
+ actions.increment();
16
+ totalReducer.actions.increment(count + 1);
17
+ };
18
+ const decrement = () => {
19
+ actions.decrement();
20
+ totalReducer.actions.decrement(count - 1);
21
+ };
22
+ const warningClass = count > 10 ? 'text-red-500' : '';
23
+
24
+ return html`
25
+ <div class="mt-10">
26
+ <div class="mb-2">
27
+ Counter: ${name}
28
+ <span>starts at ${meta?.start}</span>
29
+ </div>
30
+ <div class="flex flex-1 flex-row items-center text-gray-700">
31
+ <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>
32
+ <div class="mx-20">
33
+ <h1 class="text-3xl font-mono ${warningClass}">${count}</h1>
34
+ </div>
35
+ <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>
36
+ </div>
37
+ </div>
38
+ `;
39
+ };
40
+
41
+ export default createElement(import.meta, Counter);
example/elements/app-total.js ADDED
@@ -0,0 +1,13 @@
1
+ import { createElement, html, useReducer } from '../../index.js';
2
+ import { totalReducer } from '../store.js';
3
+
4
+ const Total = () => {
5
+ const { total } = useReducer(totalReducer);
6
+ return html`
7
+ <div class="m-20">
8
+ <h1 class="text-xl font-mono">Total of 2 Counters: ${total}</h1>
9
+ </div>
10
+ `;
11
+ };
12
+
13
+ export default createElement(import.meta, Total);
example/page.js DELETED
@@ -1,19 +0,0 @@
1
- import { createPage, html } from '../index.js';
2
- import './app-counter.js';
3
-
4
- const head = ({ config }) => {
5
- return html` <title>${config.title}</title> `;
6
- };
7
-
8
- const body = () => {
9
- return html`
10
- <div class="flex flex-1 items-center justify-center">
11
- <app-counter name="1" meta="{'start': 5}"></app-counter>
12
- </div>
13
- `;
14
- };
15
-
16
- export default createPage({
17
- head,
18
- body,
19
- });
example/pages/index.js ADDED
@@ -0,0 +1,24 @@
1
+ import { createPage, html } from '../../index.js';
2
+ import '../elements/app-counter.js';
3
+ import '../elements/app-total.js';
4
+
5
+ const head = ({ config }) => {
6
+ return html` <title>${config.title}</title> `;
7
+ };
8
+
9
+ const body = () => {
10
+ return html`
11
+ <div class="flex flex-1 flex-col items-center justify-center">
12
+ <app-counter name="1" meta="{'start': 5}"></app-counter>
13
+ <app-counter name="2" meta="{'start': 7}"></app-counter>
14
+ </div class="mt-10">
15
+ <app-total></app-total>
16
+ </div>
17
+ </div>
18
+ `;
19
+ };
20
+
21
+ export default createPage({
22
+ head,
23
+ body,
24
+ });
example/server.js CHANGED
@@ -2,17 +2,20 @@ import http from 'http';
2
2
  import fs from 'fs';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { dirname } from 'path';
5
- import renderPage from './page.js';
5
+ import renderPage from './pages/index.js';
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = dirname(__filename);
9
-
10
9
  const port = process.argv[2] || 3000;
10
+ const elements = ['app-counter.js', 'app-total.js'];
11
11
  const srcMap = {
12
12
  '/index.js': `${__dirname}/../index.js`,
13
13
  '/lit-html.js': `${__dirname}/../lit-html.js`,
14
- '/app-counter.js': `${__dirname}/app-counter.js`,
14
+ '/store.js': `${__dirname}/store.js`,
15
15
  };
16
+ elements.forEach((el) => {
17
+ srcMap['/elements/' + el] = `${__dirname}/elements/${el}`;
18
+ });
16
19
 
17
20
  http
18
21
  .createServer((req, res) => {
@@ -27,7 +30,7 @@ http
27
30
  headScript: '',
28
31
  bodyScript: `
29
32
  <script type="module">
30
- import './app-counter.js';
33
+ ${elements.map((el) => `import './elements/${el}';`).join('\n')}
31
34
  </script>
32
35
  `,
33
36
  });
example/store.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createReducer } from '../index.js';
2
+
3
+ export const totalReducer = createReducer({
4
+ initial: {
5
+ total: 0,
6
+ },
7
+ reducer: {
8
+ increment: (state) => ({ ...state, total: state.total + 1 }),
9
+ decrement: (state) => ({ ...state, total: state.total - 1 }),
10
+ },
11
+ });
index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare type Config = {
1
+ export type Config = {
2
2
  version: string
3
3
  url: string
4
4
  image: string
@@ -13,7 +13,7 @@ export declare type Config = {
13
13
  themes: {[key: string]: any}
14
14
  }
15
15
 
16
- export declare type Location = {
16
+ export type Location = {
17
17
  readonly ancestorOrigins: DOMStringList;
18
18
  hash: string;
19
19
  host: string;
@@ -30,42 +30,16 @@ export declare type Location = {
30
30
  toString: () => string;
31
31
  }
32
32
 
33
- export declare type Data = any;
34
- export declare type Item = any;
35
33
 
36
- export class AtomsElement {
37
- static register(): () => void;
38
- static observedAttributes: Array<string>;
39
- static getElement: (name: string) => AtomsElement | undefined;
40
- attrs: {[key: string]: any};
41
- state: {[key: string]: any};
42
- computed: {[key: string]: any};
43
- }
44
- export const getConfig = () => Config;
45
- export const getLocation = () => Location;
46
-
47
- export type PageRenderProps = {
48
- props: any;
49
- headScript: string;
50
- bodyScript: string;
51
- }
52
- export type Handler = (props: any) => string;
53
-
54
- export function createPage(props: { head: Handler, body: Handler}): (props: PageRenderProps) => string;
55
-
56
- export type State<P, Q> = {
34
+ export type Reducer<P, Q> = {
57
35
  getValue: () => P;
58
36
  subscribe: (fn: (v: P) => void) => void;
59
37
  } & { actions: { [K in keyof Q]: (v: any) => void}}
60
38
 
61
- export function createState<P, Q extends {[k: string]: (state: P, v: any) => P}>(props: { state: P, reducer: Q }): State<P, Q>;
39
+ export function createReducer<P, Q extends {[k: string]: (state: P, v: any) => P}>(props: { initial: P, reducer: Q }): Reducer<P, Q>;
62
40
 
63
- export type CreateElementProps<N, P, Q> = {
64
- name: string;
65
- attrs: N;
66
- state: P;
67
- reducer: Q
68
- render: (props: { attrs: N, state: P, actions: { [K in keyof Q]: (v: any) => void;} }) => any
41
+ export function useReducer<P, Q extends {[k: string]: (state: P, v: any) => P}>(props: Reducer<P, Q> | { initial: P, reducer: Q }) : P & Reducer<P, Q>;
69
- }
70
42
 
43
+ export function createElement(meta: any, renderFn: any): any
44
+ export type Handler = (props: any) => string;
71
- export function createElement<N, P, Q extends {[k: string]: (state: P, v: any) => P}>(props: CreateElementProps<N, P, Q>): CreateElementProps<N, P, Q>;
45
+ export function createPage(props: { head: Handler, body: Handler}): (props: { props: any, headScript: string, bodyScript: string }) => string;
index.js CHANGED
@@ -9,6 +9,14 @@ const tagRE = /<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
9
9
  const whitespaceRE = /^\s*$/;
10
10
  const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;
11
11
  const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
12
+ const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm;
13
+ const ARGUMENT_NAMES = /([^\s,]+)/g;
14
+
15
+ const parseFuncParams = (func) => {
16
+ const fnStr = func.toString().replace(STRIP_COMMENTS, '');
17
+ const result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
18
+ return (result || []).filter((it) => it !== '{' && it !== '}');
19
+ };
12
20
 
13
21
  const parseTag = (tag) => {
14
22
  const res = {
@@ -925,21 +933,25 @@ export const unsafeHTML = isBrowser
925
933
  const fifo = (q) => q.shift();
926
934
  const microtask = (flush) => () => queueMicrotask(flush);
927
935
 
936
+ export const createAttrs = (attrs) => attrs;
937
+
928
- export const createState = ({ state, reducer }) => {
938
+ export const createReducer = ({ initial, reducer }) => {
929
- let initial = state;
939
+ let value = initial;
930
940
  const subs = new Set();
931
941
  const actions = Object.keys(reducer).reduce((acc, key) => {
932
942
  const reduce = reducer[key];
933
943
  acc[key] = (v) => {
934
- initial = reduce(initial, v);
944
+ value = reduce(value, v);
935
945
  subs.forEach((sub) => {
936
- sub(initial);
946
+ sub(value);
937
947
  });
938
948
  };
939
949
  return acc;
940
950
  }, {});
941
951
  return {
952
+ initial,
953
+ reducer,
942
- getValue: () => initial,
954
+ getValue: () => value,
943
955
  subscribe: (fn) => {
944
956
  subs.add(fn);
945
957
  },
@@ -947,52 +959,64 @@ export const createState = ({ state, reducer }) => {
947
959
  subs.remove(fn);
948
960
  },
949
961
  actions,
950
- ...actions,
951
962
  };
952
963
  };
953
964
 
954
- const registry = {};
965
+ const currentComponent = {
955
- export const getConfig = () => (isBrowser ? window.props.config : global.props.config);
956
- export const getLocation = () => (isBrowser ? window.location : global.location);
966
+ current: undefined,
967
+ set(v) {
968
+ this.current = v;
957
- export const getElement = (name) => registry[name];
969
+ this.current.hooks.index = 0;
970
+ },
971
+ get() {
972
+ return this.current;
973
+ },
974
+ };
975
+
958
- export const registerElement = (name, clazz) => {
976
+ export const useReducer = (reducer) => {
977
+ const comp = currentComponent.get();
959
- registry[name] = clazz;
978
+ const index = comp.hooks.index++;
960
- if (isBrowser) {
961
- if (window.customElements.get(name)) {
979
+ if (!comp.hooks.data[index]) {
962
- return;
980
+ comp.hooks.data[index] = reducer.subscribe ? reducer : createReducer(reducer);
963
- } else {
964
- window.customElements.define(name, registry[name]);
981
+ comp.hooks.data[index].subscribe(() => comp.update());
965
- }
966
982
  }
983
+ const state = comp.hooks.data[index].getValue();
984
+ return { ...state, actions: comp.hooks.data[index].actions };
967
985
  };
986
+
987
+ const registry = {};
988
+ export const getElement = (name) => registry[name];
968
989
  const BaseElement = isBrowser ? window.HTMLElement : class {};
969
- export const createElement = ({ name, attrs, state, reducer, render: renderFn }) => {
990
+ export const createElement = (meta, renderFn) => {
991
+ const funcParams = parseFuncParams(renderFn);
970
- const Element = class extends BaseElement {
992
+ const RenderElement = class extends BaseElement {
971
993
  static get observedAttributes() {
972
- return Object.keys(attrs || {}).map((k) => k.toLowerCase());
994
+ return funcParams.map((k) => k.toLowerCase());
973
995
  }
974
996
 
975
997
  constructor(ssrAttrs) {
976
998
  super();
977
999
  this._dirty = false;
978
1000
  this._connected = false;
979
- this.attrs = ssrAttrs || attrs;
1001
+ this.attrs = ssrAttrs || {};
980
- this.state = state ? createState({ state, reducer }) : null;
1002
+ this.state = {};
981
- this.config = isBrowser ? window.config : global.config;
1003
+ this.hooks = {
982
- this.location = isBrowser ? window.location : global.location;
1004
+ index: 0,
1005
+ data: {},
1006
+ };
983
1007
  // this.prevClassList = [];
984
1008
  // this.shadow = this.attachShadow({ mode: 'open' });
985
1009
  }
986
1010
 
987
1011
  connectedCallback() {
988
1012
  this._connected = true;
989
- this.state.subscribe(() => this.update());
1013
+ // this.state.subscribe(() => this.update());
990
1014
  this.update();
991
1015
  }
992
1016
 
993
1017
  disconnectedCallback() {
994
1018
  this._connected = false;
995
- this.state.unsubscribe(() => this.update());
1019
+ // this.state.unsubscribe(() => this.update());
996
1020
  }
997
1021
 
998
1022
  attributeChangedCallback(key, oldValue, newValue) {
@@ -1033,10 +1057,11 @@ export const createElement = ({ name, attrs, state, reducer, render: renderFn })
1033
1057
  }
1034
1058
 
1035
1059
  render() {
1060
+ currentComponent.set(this);
1036
1061
  const template = renderFn({
1037
- attrs: this.attrs,
1062
+ ...this.attrs,
1038
- state: this.state ? this.state.getValue() : {},
1063
+ config: isBrowser ? window.props.config : global?.props?.config,
1039
- actions: this.state ? this.state.actions : {},
1064
+ location: isBrowser ? window.location : global?.location,
1040
1065
  });
1041
1066
  if (isBrowser) {
1042
1067
  // TODO: this can be optimized when we know whether the value belongs in a class (AttributePart)
@@ -1070,8 +1095,17 @@ export const createElement = ({ name, attrs, state, reducer, render: renderFn })
1070
1095
  }
1071
1096
  }
1072
1097
  };
1098
+ const parts = meta.url.split('/');
1099
+ const name = parts[parts.length - 1].replace('.js', '');
1073
- registerElement(name, Element);
1100
+ registry[name] = RenderElement;
1101
+ if (isBrowser) {
1102
+ if (window.customElements.get(name)) {
1103
+ return;
1104
+ } else {
1074
- return { name, attrs, state, render: renderFn };
1105
+ window.customElements.define(name, registry[name]);
1106
+ }
1107
+ }
1108
+ return renderFn;
1075
1109
  };
1076
1110
 
1077
1111
  export const createPage = ({ head, body }) => {