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


00aa1505 Peter John

4 years ago
improve ssr
Files changed (7) hide show
  1. package.json +3 -4
  2. readme.md +7 -4
  3. src/index.d.ts +60 -0
  4. src/index.js +248 -207
  5. src/lit-html.js +93 -95
  6. test/index.test.js +69 -16
  7. test/setup.js +0 -2
package.json CHANGED
@@ -15,6 +15,7 @@
15
15
  ],
16
16
  "license": "MIT",
17
17
  "main": "src/index.js",
18
+ "typings": "src/index.d.ts",
18
19
  "author": "pyros.sh",
19
20
  "type": "module",
20
21
  "engines": {
@@ -27,8 +28,6 @@
27
28
  "jest": "^27.0.5"
28
29
  },
29
30
  "jest": {
30
- "setupFilesAfterEnv": [
31
+ "transform": {}
31
- "./test/setup.js"
32
- ]
33
32
  }
34
- }
33
+ }
readme.md CHANGED
@@ -21,14 +21,17 @@ npm i atoms-element
21
21
  ## Usage
22
22
 
23
23
  ```js
24
- import { defineElement, html, render, number } from 'atoms-element.js';
24
+ import { defineElement, html, render, object, number, string } from 'atoms-element';
25
25
 
26
26
  const propTypes = {
27
27
  name: string.isRequired,
28
+ meta: object({
29
+ start: number,
30
+ }),
28
31
  };
29
32
 
30
- const Counter = ({ name }) => {
33
+ const Counter = ({ name, meta }) => {
31
- const [count, setCount] = useState(0);
34
+ const [count, setCount] = useState(meta?.start || 0);
32
35
 
33
36
  return html`
34
37
  <div>
@@ -46,5 +49,5 @@ const Counter = ({ name }) => {
46
49
 
47
50
  defineElement('app-counter', Counter, propTypes);
48
51
 
49
- render(html`<app-counter name="1"></app-counter>`, document.body);
52
+ console.log(render(html`<app-counter name="1"></app-counter>`));
50
53
  ```
src/index.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ export declare type Destructor = () => void | undefined;
2
+ export declare type EffectCallback = () => (void | Destructor);
3
+ export declare type SetStateAction<S> = S | ((prevState: S) => S);
4
+ export declare type Dispatch<A> = (value: A) => void;
5
+ export declare type DispatchWithoutAction = () => void;
6
+ export declare type DependencyList = ReadonlyArray<any>;
7
+ export declare type Reducer<S, A> = (prevState: S, action: A) => S;
8
+ export declare type ReducerWithoutAction<S> = (prevState: S) => S;
9
+ export declare type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any> ? S : never;
10
+ export declare type ReducerAction<R extends Reducer<any, any>> = R extends Reducer<any, infer A> ? A : never;
11
+ export declare type ReducerStateWithoutAction<R extends ReducerWithoutAction<any>> = R extends ReducerWithoutAction<infer S> ? S : never;
12
+ export declare interface MutableRefObject<T> {
13
+ current: T | null | undefined;
14
+ }
15
+ export declare type Config = {
16
+ version: string
17
+ url: string
18
+ image: string
19
+ author: string
20
+ languages: Array<string>
21
+ title: string
22
+ description: string
23
+ keywords: string
24
+ categories: Array<string>
25
+ tags: Array<string>
26
+ strings: {[key: string]: string}
27
+ }
28
+ export declare type Location = {
29
+ readonly ancestorOrigins: DOMStringList;
30
+ hash: string;
31
+ host: string;
32
+ hostname: string;
33
+ href: string;
34
+ readonly origin: string;
35
+ pathname: string;
36
+ port: string;
37
+ protocol: string;
38
+ search: string;
39
+ assign: (url: string | URL) => void;
40
+ reload: () => void;
41
+ replace: (url: string | URL) => void;
42
+ toString: () => string;
43
+ }
44
+
45
+ export declare const useState: <S>(initialState: S | (() => S)) => [S, Dispatch<SetStateAction<S>>];
46
+ export declare const useEffect: (effect: EffectCallback, deps?: DependencyList) => void;
47
+ export declare const useLayoutEffect: (effect: EffectCallback, deps?: DependencyList) => void;
48
+ export declare const useReducer: <R extends ReducerWithoutAction<any>, I>(
49
+ reducer: R,
50
+ initializerArg: I,
51
+ initializer: (arg: I) => ReducerStateWithoutAction<R>
52
+ ) => [ReducerStateWithoutAction<R>, DispatchWithoutAction];
53
+
54
+ export declare const useCallback: <T extends (...args: any[]) => any>(callback: T, deps: DependencyList) => T;
55
+ export declare const useMemo: <T>(factory: () => T, deps: DependencyList | undefined) => T;
56
+ // function useImperativeHandle<T, R extends T>(ref: Ref<T>|undefined, init: () => R, deps?: DependencyList): void;
57
+ export declare const useRef: <T>(initialValue: T | null | undefined) => MutableRefObject<T>;
58
+
59
+ export declare const useConfig: () => Config;
60
+ export declare const useLocation: () => Location;
src/index.js CHANGED
@@ -1,21 +1,20 @@
1
1
  import { html as litHtml, render as litRender, directive as litDirective, NodePart, AttributePart, PropertyPart, isPrimitive } from './lit-html.js';
2
2
  import { html as litServerHtml, directive as litServerDirective, isNodePart, isAttributePart, unsafePrefixString, renderToString } from './lit-html-server.js';
3
3
 
4
- export const registry = {};
4
+ const registry = {};
5
- const isBrowser = typeof window.alert !== "undefined";
5
+ const isBrowser = typeof window !== 'undefined';
6
6
  export const html = isBrowser ? litHtml : litServerHtml;
7
7
  export const render = isBrowser ? litRender : renderToString;
8
8
  export const directive = isBrowser ? litDirective : litServerDirective;
9
9
 
10
10
  const previousValues = new WeakMap();
11
- export const unsafeHTML = directive((value) => (part) => {
11
+ export const unsafeHTML = directive(value => part => {
12
12
  if (isBrowser) {
13
13
  if (!(part instanceof NodePart)) {
14
14
  throw new Error('unsafeHTML can only be used in text bindings');
15
15
  }
16
16
  const previousValue = previousValues.get(part);
17
- if (previousValue !== undefined && isPrimitive(value) &&
18
- value === previousValue.value && part.value === previousValue.fragment) {
17
+ if (previousValue !== undefined && isPrimitive(value) && value === previousValue.value && part.value === previousValue.fragment) {
19
18
  return;
20
19
  }
21
20
  const template = document.createElement('template');
@@ -32,12 +31,10 @@ export const unsafeHTML = directive((value) => (part) => {
32
31
  });
33
32
 
34
33
  const previousClassesCache = new WeakMap();
35
- export const classMap = directive((classInfo) => (part) => {
34
+ export const classMap = directive(classInfo => part => {
36
35
  if (isBrowser) {
37
- if (!(part instanceof AttributePart) || (part instanceof PropertyPart) ||
38
- part.committer.name !== 'class' || part.committer.parts.length > 1) {
36
+ if (!(part instanceof AttributePart) || part instanceof PropertyPart || part.committer.name !== 'class' || part.committer.parts.length > 1) {
39
- throw new Error('The `classMap` directive must be used in the `class` attribute ' +
37
+ throw new Error('The `classMap` directive must be used in the `class` attribute ' + 'and must be the only part in the attribute.');
40
- 'and must be the only part in the attribute.');
41
38
  }
42
39
  const { committer } = part;
43
40
  const { element } = committer;
@@ -46,13 +43,13 @@ export const classMap = directive((classInfo) => (part) => {
46
43
  // Write static classes once
47
44
  // Use setAttribute() because className isn't a string on SVG elements
48
45
  element.setAttribute('class', committer.strings.join(' '));
49
- previousClassesCache.set(part, previousClasses = new Set());
46
+ previousClassesCache.set(part, (previousClasses = new Set()));
50
47
  }
51
48
  const classList = element.classList;
52
49
  // Remove old classes that no longer apply
53
50
  // We use forEach() instead of for-of so that re don't require down-level
54
51
  // iteration.
55
- previousClasses.forEach((name) => {
52
+ previousClasses.forEach(name => {
56
53
  if (!(name in classInfo)) {
57
54
  classList.remove(name);
58
55
  previousClasses.delete(name);
@@ -67,8 +64,7 @@ export const classMap = directive((classInfo) => (part) => {
67
64
  if (value) {
68
65
  classList.add(name);
69
66
  previousClasses.add(name);
70
- }
71
- else {
67
+ } else {
72
68
  classList.remove(name);
73
69
  previousClasses.delete(name);
74
70
  }
@@ -81,7 +77,7 @@ export const classMap = directive((classInfo) => (part) => {
81
77
  if (!isAttributePart(part) || part.name !== 'class') {
82
78
  throw Error('The `classMap` directive can only be used in the `class` attribute');
83
79
  }
84
- const classes = (classInfo);
80
+ const classes = classInfo;
85
81
  let value = '';
86
82
  for (const key in classes) {
87
83
  if (classes[key]) {
@@ -94,26 +90,25 @@ export const classMap = directive((classInfo) => (part) => {
94
90
 
95
91
  let currentCursor;
96
92
  let currentComponent;
93
+ let logError = msg => {
94
+ console.warn(msg);
95
+ };
97
96
 
98
- export const logError = (msg) => {
97
+ export const setLogError = fn => {
99
- if (window.logError) {
100
- window.logError(msg);
98
+ logError = fn;
101
- } else {
102
- console.warn(msg);
103
- }
99
+ };
104
- }
105
100
 
106
101
  const checkRequired = (context, data) => {
107
102
  if (data === null || typeof data === 'undefined') {
108
103
  logError(`'${context}' Field is required`);
109
104
  }
110
- }
105
+ };
111
106
 
112
- const checkPrimitive = (primitiveType) => {
107
+ const checkPrimitive = primitiveType => {
113
108
  const common = {
114
109
  type: primitiveType,
115
- parse: (attr) => attr,
110
+ parse: attr => attr,
116
- }
111
+ };
117
112
  const validate = (context, data) => {
118
113
  if (data === null || typeof data === 'undefined') {
119
114
  return;
@@ -122,7 +117,7 @@ const checkPrimitive = (primitiveType) => {
122
117
  if (dataType !== primitiveType) {
123
118
  logError(`'${context}' Expected type '${primitiveType}' got type '${dataType}'`);
124
119
  }
125
- }
120
+ };
126
121
  return {
127
122
  validate,
128
123
  ...common,
@@ -131,35 +126,35 @@ const checkPrimitive = (primitiveType) => {
131
126
  validate: (context, data) => {
132
127
  checkRequired(context, data);
133
128
  validate(context, data);
134
- }
129
+ },
135
- }
130
+ },
136
- }
131
+ };
137
- }
132
+ };
138
133
 
139
134
  const checkComplex = (complexType, validate) => {
140
135
  const common = {
141
136
  type: complexType,
142
- parse: (attr) => attr ? JSON.parse(attr.replace(/'/g, `"`)) : null
137
+ parse: attr => (attr ? JSON.parse(attr.replace(/'/g, `"`)) : null),
143
138
  };
144
- return (innerType) => {
139
+ return innerType => {
145
140
  return {
146
141
  ...common,
147
142
  validate: (context, data) => {
148
143
  if (!data) {
149
144
  return;
150
145
  }
151
- validate(innerType, context, data)
146
+ validate(innerType, context, data);
152
147
  },
153
148
  isRequired: {
154
149
  ...common,
155
150
  validate: (context, data) => {
156
151
  checkRequired(context, data);
157
152
  validate(innerType, context, data);
158
- }
153
+ },
159
154
  },
160
- }
155
+ };
161
- }
156
+ };
162
- }
157
+ };
163
158
 
164
159
  export const number = checkPrimitive('number');
165
160
  export const string = checkPrimitive('string');
@@ -171,7 +166,7 @@ export const object = checkComplex('object', (innerType, context, data) => {
171
166
  for (const key of Object.keys(innerType)) {
172
167
  const fieldValidator = innerType[key];
173
168
  const item = data[key];
174
- fieldValidator.validate(`${context}.${key}`, item)
169
+ fieldValidator.validate(`${context}.${key}`, item);
175
170
  }
176
171
  });
177
172
  export const array = checkComplex('array', (innerType, context, data) => {
@@ -180,13 +175,12 @@ export const array = checkComplex('array', (innerType, context, data) => {
180
175
  }
181
176
  for (let i = 0; i < data.length; i++) {
182
177
  const item = data[i];
183
- innerType.validate(`${context}[${i}]`, item)
178
+ innerType.validate(`${context}[${i}]`, item);
184
179
  }
185
180
  });
186
- export const func = checkComplex('function', (innerType, context, data) => {
181
+ export const func = checkComplex('function', (innerType, context, data) => {});
187
- });
188
182
 
189
- export const hooks = (config) => {
183
+ export const hooks = config => {
190
184
  const h = currentComponent.hooks;
191
185
  const c = currentComponent;
192
186
  const index = currentCursor++;
@@ -197,205 +191,252 @@ export const hooks = (config) => {
197
191
  h.values[index] = config.onupdate(h, c, index);
198
192
  }
199
193
  return h.values[index];
200
- }
201
-
202
- export const defaultHooks = () => ({
203
- values: [],
204
- deps: [],
205
- effects: [],
206
- layoutEffects: [],
207
- cleanup: []
208
- });
194
+ };
209
195
 
210
- export const __setCurrent__ = (c) => {
196
+ export const __setCurrent__ = c => {
211
197
  currentComponent = c;
212
198
  currentCursor = 0;
213
- }
199
+ };
214
200
 
215
- export const useDispatchEvent = (name) => hooks({
201
+ export const useDispatchEvent = name =>
202
+ hooks({
216
- oncreate: (_, c) => (data) => c.dispatchEvent(new CustomEvent(name, data))
203
+ oncreate: (_, c) => data => c.dispatchEvent(new CustomEvent(name, data)),
217
- });
204
+ });
218
- export const useRef = (initialValue) => hooks({
205
+ export const useRef = initialValue =>
206
+ hooks({
219
- oncreate: (_h, _c) => ({ current: initialValue })
207
+ oncreate: (_h, _c) => ({ current: initialValue }),
220
- });
208
+ });
221
- export const useState = (initialState) => hooks({
209
+ export const useState = initialState =>
210
+ hooks({
222
- oncreate: (h, c, i) => [
211
+ oncreate: (h, c, i) => [
223
- typeof initialState === "function"
212
+ typeof initialState === 'function' ? initialState() : initialState,
224
- ? initialState()
225
- : initialState,
226
- function setState(nextState) {
213
+ function setState(nextState) {
227
- const state = h.values[i][0];
214
+ const state = h.values[i][0];
228
- if (typeof nextState === "function") {
215
+ if (typeof nextState === 'function') {
229
- nextState = nextState(state);
216
+ nextState = nextState(state);
217
+ }
218
+ if (!Object.is(state, nextState)) {
219
+ h.values[i][0] = nextState;
220
+ c.update();
221
+ }
222
+ },
223
+ ],
224
+ });
225
+ export const useReducer = (reducer, initialState) =>
226
+ hooks({
227
+ oncreate: (h, c, i) => [
228
+ initialState,
229
+ function dispatch(action) {
230
+ const state = h.values[i][0];
231
+ const nextState = reducer(state, action);
232
+ if (!Object.is(state, nextState)) {
233
+ h.values[i][0] = nextState;
234
+ c.update();
235
+ }
236
+ },
237
+ ],
238
+ });
239
+ const depsChanged = (prev, next) => prev == null || next.some((f, i) => !Object.is(f, prev[i]));
240
+ export const useEffect = (handler, deps) =>
241
+ hooks({
242
+ onupdate(h, _, i) {
243
+ if (!deps || depsChanged(h.deps[i], deps)) {
244
+ h.deps[i] = deps || [];
245
+ h.effects[i] = handler;
230
246
  }
247
+ },
248
+ });
249
+ export const useLayoutEffect = (handler, deps) =>
250
+ hooks({
251
+ onupdate(h, _, i) {
231
- if (!Object.is(state, nextState)) {
252
+ if (!deps || depsChanged(h.deps[i], deps)) {
232
- h.values[i][0] = nextState;
253
+ h.deps[i] = deps || [];
233
- c.update();
254
+ h.layoutEffects[i] = handler;
234
255
  }
235
- }
256
+ },
236
- ]
237
- });
257
+ });
238
- export const useReducer = (reducer, initialState) => hooks({
258
+ export const useMemo = (fn, deps) =>
259
+ hooks({
239
- oncreate: (h, c, i) => [
260
+ onupdate(h, _, i) {
240
- initialState,
241
- function dispatch(action) {
242
- const state = h.values[i][0];
261
+ let value = h.values[i];
243
- const nextState = reducer(state, action);
244
- if (!Object.is(state, nextState)) {
262
+ if (!deps || depsChanged(h.deps[i], deps)) {
245
- h.values[i][0] = nextState;
263
+ h.deps[i] = deps || [];
246
- c.update();
264
+ value = fn();
247
265
  }
266
+ return value;
248
- }
267
+ },
249
- ]
250
- });
268
+ });
251
- const depsChanged = (prev, next) => prev == null || next.some((f, i) => !Object.is(f, prev[i]));
269
+ export const useCallback = (callback, deps) => useMemo(() => callback, deps);
252
- export const useEffect = (handler, deps) => hooks({
270
+ export const useConfig = () => {
253
- onupdate(h, _, i) {
271
+ if (isBrowser) {
254
- if (!deps || depsChanged(h.deps[i], deps)) {
255
- h.deps[i] = deps || [];
272
+ return window.config;
256
- h.effects[i] = handler;
257
- }
258
- }
259
- });
260
- export const useLayoutEffect = (handler, deps) => hooks({
261
- onupdate(h, _, i) {
262
- if (!deps || depsChanged(h.deps[i], deps)) {
263
- h.deps[i] = deps || [];
264
- h.layoutEffects[i] = handler;
265
- }
266
273
  }
274
+ return global.config;
267
- });
275
+ };
268
- export const useMemo = (fn, deps) => hooks({
276
+ export const useLocation = () => {
269
- onupdate(h, _, i) {
277
+ if (isBrowser) {
270
- let value = h.values[i];
271
- if (!deps || depsChanged(h.deps[i], deps)) {
272
- h.deps[i] = deps || [];
273
- value = fn();
274
- }
275
- return value;
278
+ return window.location;
276
279
  }
280
+ return global.location;
277
- });
281
+ };
278
- export const useCallback = (callback, deps) => useMemo(() => callback, deps);
279
282
 
280
283
  const batch = (runner, pick, callback) => {
281
284
  const q = [];
282
285
  const flush = () => {
283
286
  let p;
284
- while ((p = pick(q)))
287
+ while ((p = pick(q))) callback(p);
285
- callback(p);
286
288
  };
287
289
  const run = runner(flush);
288
- return (c) => q.push(c) === 1 && run();
290
+ return c => q.push(c) === 1 && run();
289
291
  };
290
- const fifo = (q) => q.shift();
292
+ const fifo = q => q.shift();
291
- const filo = (q) => q.pop();
293
+ const filo = q => q.pop();
292
- const microtask = (flush) => {
294
+ const microtask = flush => {
293
295
  return () => queueMicrotask(flush);
294
296
  };
295
- const task = (flush) => {
297
+ const task = flush => {
296
298
  if (isBrowser) {
297
- const ch = new MessageChannel();
299
+ const ch = new window.MessageChannel();
298
300
  ch.port1.onmessage = flush;
299
301
  return () => ch.port2.postMessage(null);
300
- }
301
- else {
302
+ } else {
302
303
  return () => setImmediate(flush);
303
304
  }
304
305
  };
305
- const enqueueLayoutEffects = batch(microtask, filo, c => c._flushEffects("layoutEffects"));
306
+ const enqueueLayoutEffects = batch(microtask, filo, c => c._flushEffects('layoutEffects'));
306
- const enqueueEffects = batch(task, filo, c => c._flushEffects("effects"));
307
+ const enqueueEffects = batch(task, filo, c => c._flushEffects('effects'));
307
308
  const enqueueUpdate = batch(microtask, fifo, c => c._performUpdate());
308
309
 
310
+ const BaseElement = isBrowser ? window.HTMLElement : class {};
311
+
309
- export function defineElement(name, fn, propTypes = {}) {
312
+ export class AtomsElement extends BaseElement {
313
+ constructor() {
314
+ super();
315
+ this._dirty = false;
310
- registry[name] = fn;
316
+ this._connected = false;
311
- const keys = Object.keys(propTypes);
312
- const funcKeys = keys.filter((key) => propTypes[key].type === 'function');
313
- const attributes = keys.filter((key) => propTypes[key].type !== 'function').reduce((acc, key) => {
314
- acc[key.toLowerCase()] = {
317
+ this.hooks = {
318
+ values: [],
319
+ deps: [],
315
- propName: key,
320
+ effects: [],
316
- propType: propTypes[key],
321
+ layoutEffects: [],
322
+ cleanup: [],
317
323
  };
318
- return acc;
324
+ this.props = {};
319
- }, {});
325
+ this.attrTypes = {};
326
+ this.name = '';
327
+ this.renderer = () => {};
328
+ this.attrTypes = {};
329
+ this.funcKeys = [];
330
+ this.attrTypesMap = {};
331
+ }
332
+ connectedCallback() {
333
+ this._connected = true;
320
- if (isBrowser) {
334
+ if (isBrowser) {
321
- if (customElements.get(name)) {
335
+ this.update();
322
- return;
336
+ } else {
337
+ __setCurrent__(this);
323
338
  }
324
- customElements.define(name, class extends HTMLElement {
339
+ }
325
- constructor() {
340
+ disconnectedCallback() {
326
- super(...arguments);
327
- this._dirty = false;
328
- this._connected = false;
341
+ this._connected = false;
342
+ let cleanup;
329
- this.hooks = defaultHooks();
343
+ while ((cleanup = this.hooks.cleanup.shift())) {
330
- this.props = {};
344
+ cleanup();
331
- this.renderer = fn;
332
- }
345
+ }
333
- connectedCallback() {
334
- this._connected = true;
335
- this.update();
336
- }
346
+ }
337
- disconnectedCallback() {
338
- this._connected = false;
339
- let cleanup;
340
- while ((cleanup = this.hooks.cleanup.shift())) {
341
- cleanup();
342
- }
343
- }
344
347
 
345
- static get observedAttributes() {
348
+ attributeChangedCallback(key, oldValue, newValue) {
346
- return Object.keys(attributes);
349
+ const attr = this.attrTypesMap[key];
350
+ if (!attr) {
351
+ return;
347
- }
352
+ }
353
+ const data = attr.propType.parse(newValue);
354
+ attr.propType.validate(`<${this.name}> ${key}`, data);
355
+ this.props[attr.propName] = data;
356
+ if (this._connected) {
357
+ this.update();
358
+ }
359
+ }
348
360
 
361
+ update() {
349
- attributeChangedCallback(key, oldValue, newValue) {
362
+ if (this._dirty) {
363
+ return;
364
+ }
350
- const attr = attributes[key];
365
+ this._dirty = true;
351
- const data = attr.propType.parse(newValue);
352
- attr.propType.validate(`<${name}> ${key}`, data);
353
- this.props[attr.propName] = data;
366
+ enqueueUpdate(this);
367
+ }
368
+ _performUpdate() {
354
- if (this._connected) {
369
+ if (!this._connected) {
370
+ return;
371
+ }
372
+ __setCurrent__(this);
355
- this.update();
373
+ this.render();
374
+ enqueueLayoutEffects(this);
375
+ enqueueEffects(this);
376
+ this._dirty = false;
377
+ }
378
+ _flushEffects(effectKey) {
379
+ const effects = this.hooks[effectKey];
380
+ const cleanups = this.hooks.cleanup;
381
+ for (let i = 0, len = effects.length; i < len; i++) {
382
+ if (effects[i]) {
383
+ cleanups[i] && cleanups[i]();
384
+ const cleanup = effects[i]();
385
+ if (cleanup) {
386
+ cleanups[i] = cleanup;
356
387
  }
388
+ delete effects[i];
357
389
  }
390
+ }
391
+ }
358
392
 
359
- update() {
393
+ render() {
360
- if (this._dirty) {
394
+ if (isBrowser) {
395
+ this.funcKeys.forEach(key => {
396
+ this.props[key] = this[key];
397
+ });
398
+ render(this.renderer(this.props), this);
361
- return;
399
+ } else {
400
+ __setCurrent__(this);
401
+ return render(this.renderer(this.props), this);
362
- }
402
+ }
363
- this._dirty = true;
364
- enqueueUpdate(this);
365
- }
403
+ }
366
- _performUpdate() {
367
- if (!this._connected) {
368
- return;
369
- }
404
+ }
370
- __setCurrent__(this);
371
- this.render();
372
- enqueueLayoutEffects(this);
373
- enqueueEffects(this);
374
- this._dirty = false;
375
- }
376
- _flushEffects(effectKey) {
377
- const effects = this.hooks[effectKey];
378
- const cleanups = this.hooks.cleanup;
379
- for (let i = 0, len = effects.length; i < len; i++) {
405
+ export const getElement = name => registry[name];
380
- if (effects[i]) {
381
- cleanups[i] && cleanups[i]();
382
- const cleanup = effects[i]();
383
- if (cleanup) {
384
- cleanups[i] = cleanup;
385
- }
386
- delete effects[i];
387
- }
388
- }
389
- }
390
406
 
407
+ export function defineElement(name, fn, attrTypes = {}) {
408
+ const keys = Object.keys(attrTypes);
409
+ registry[name] = class extends AtomsElement {
410
+ constructor(attrs) {
391
- render() {
411
+ super();
412
+ this.name = name;
413
+ this.renderer = fn;
414
+ this.attrTypes = attrTypes;
415
+ this.funcKeys = keys.filter(key => attrTypes[key].type === 'function');
416
+ this.attrTypesMap = keys
417
+ .filter(key => attrTypes[key].type !== 'function')
418
+ .reduce((acc, key) => {
419
+ acc[key.toLowerCase()] = {
420
+ propName: key,
421
+ propType: attrTypes[key],
422
+ };
423
+ return acc;
424
+ }, {});
425
+ if (attrs) {
392
- funcKeys.forEach((key) => {
426
+ attrs.forEach(item => {
393
- this.props[key] = this[key]
427
+ this.attributeChangedCallback(item.name, null, item.value);
394
- })
428
+ });
395
- render(this.renderer(this.props), this);
396
429
  }
430
+ }
431
+
432
+ static get observedAttributes() {
433
+ return keys;
434
+ }
397
- });
435
+ };
436
+ if (isBrowser) {
437
+ if (window.customElements.get(name)) {
438
+ return;
439
+ }
440
+ window.customElements.define(name, registry[name]);
398
441
  }
399
442
  }
400
-
401
- export const getElement = (name) => registry[name];
src/lit-html.js CHANGED
@@ -52,12 +52,14 @@ const directives = new WeakMap();
52
52
  * }
53
53
  * });
54
54
  */
55
- const directive = (f) => ((...args) => {
55
+ const directive =
56
+ f =>
57
+ (...args) => {
56
- const d = f(...args);
58
+ const d = f(...args);
57
- directives.set(d, true);
59
+ directives.set(d, true);
58
- return d;
60
+ return d;
59
- });
61
+ };
60
- const isDirective = (o) => {
62
+ const isDirective = o => {
61
63
  return typeof o === 'function' && directives.has(o);
62
64
  };
63
65
 
@@ -77,10 +79,7 @@ const isDirective = (o) => {
77
79
  /**
78
80
  * True if the custom elements polyfill is in use.
79
81
  */
80
- const isCEPolyfill = typeof window !== 'undefined' &&
82
+ const isCEPolyfill = typeof window !== 'undefined' && window.customElements != null && window.customElements.polyfillWrapFlushCallback !== undefined;
81
- window.customElements != null &&
82
- window.customElements.polyfillWrapFlushCallback !==
83
- undefined;
84
83
  /**
85
84
  * Reparents nodes, starting from `start` (inclusive) to `end` (exclusive),
86
85
  * into another container (could be the same container), before `before`. If
@@ -173,7 +172,10 @@ class Template {
173
172
  let lastPartIndex = 0;
174
173
  let index = -1;
175
174
  let partIndex = 0;
175
+ const {
176
+ strings,
176
- const { strings, values: { length } } = result;
177
+ values: { length },
178
+ } = result;
177
179
  while (partIndex < length) {
178
180
  const node = walker.nextNode();
179
181
  if (node === null) {
@@ -223,8 +225,7 @@ class Template {
223
225
  stack.push(node);
224
226
  walker.currentNode = node.content;
225
227
  }
226
- }
227
- else if (node.nodeType === 3 /* Node.TEXT_NODE */) {
228
+ } else if (node.nodeType === 3 /* Node.TEXT_NODE */) {
228
229
  const data = node.data;
229
230
  if (data.indexOf(marker) >= 0) {
230
231
  const parent = node.parentNode;
@@ -237,12 +238,10 @@ class Template {
237
238
  let s = strings[i];
238
239
  if (s === '') {
239
240
  insert = createMarker();
240
- }
241
- else {
241
+ } else {
242
242
  const match = lastAttributeNameRegex.exec(s);
243
243
  if (match !== null && endsWith(match[2], boundAttributeSuffix)) {
244
- s = s.slice(0, match.index) + match[1] +
245
- match[2].slice(0, -boundAttributeSuffix.length) + match[3];
244
+ s = s.slice(0, match.index) + match[1] + match[2].slice(0, -boundAttributeSuffix.length) + match[3];
246
245
  }
247
246
  insert = document.createTextNode(s);
248
247
  }
@@ -254,15 +253,13 @@ class Template {
254
253
  if (strings[lastIndex] === '') {
255
254
  parent.insertBefore(createMarker(), node);
256
255
  nodesToRemove.push(node);
257
- }
258
- else {
256
+ } else {
259
257
  node.data = strings[lastIndex];
260
258
  }
261
259
  // We have a part for each match found
262
260
  partIndex += lastIndex;
263
261
  }
264
- }
265
- else if (node.nodeType === 8 /* Node.COMMENT_NODE */) {
262
+ } else if (node.nodeType === 8 /* Node.COMMENT_NODE */) {
266
263
  if (node.data === marker) {
267
264
  const parent = node.parentNode;
268
265
  // Add a new marker node to be the startNode of the Part if any of
@@ -279,14 +276,12 @@ class Template {
279
276
  // Else, we can remove it to save future costs.
280
277
  if (node.nextSibling === null) {
281
278
  node.data = '';
282
- }
283
- else {
279
+ } else {
284
280
  nodesToRemove.push(node);
285
281
  index--;
286
282
  }
287
283
  partIndex++;
288
- }
289
- else {
284
+ } else {
290
285
  let i = -1;
291
286
  while ((i = node.data.indexOf(marker, i + 1)) !== -1) {
292
287
  // Comment node has a binding marker inside, make an inactive part
@@ -309,7 +304,7 @@ const endsWith = (str, suffix) => {
309
304
  const index = str.length - suffix.length;
310
305
  return index >= 0 && str.slice(index) === suffix;
311
306
  };
312
- const isTemplatePartActive = (part) => part.index !== -1;
307
+ const isTemplatePartActive = part => part.index !== -1;
313
308
  // Allows `document.createComment('')` to be renamed for a
314
309
  // small manual size-savings.
315
310
  const createMarker = () => document.createComment('');
@@ -419,9 +414,7 @@ class TemplateInstance {
419
414
  // The Custom Elements v1 polyfill supports upgrade(), so the order when
420
415
  // polyfilled is the more ideal: Clone, Process, Adopt, Upgrade, Update,
421
416
  // Connect.
422
- const fragment = isCEPolyfill ?
423
- this.template.element.content.cloneNode(true) :
424
- document.importNode(this.template.element.content, true);
417
+ const fragment = isCEPolyfill ? this.template.element.content.cloneNode(true) : document.importNode(this.template.element.content, true);
425
418
  const stack = [];
426
419
  const parts = this.template.parts;
427
420
  // Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null
@@ -461,8 +454,7 @@ class TemplateInstance {
461
454
  const part = this.processor.handleTextExpression(this.options);
462
455
  part.insertAfterNode(node.previousSibling);
463
456
  this.__parts.push(part);
464
- }
465
- else {
457
+ } else {
466
458
  this.__parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings, this.options));
467
459
  }
468
460
  partIndex++;
@@ -496,8 +488,7 @@ class TemplateInstance {
496
488
  * before any untrusted expressions have been mixed in. Therefor it is
497
489
  * considered safe by construction.
498
490
  */
499
- const policy = window.trustedTypes &&
500
- trustedTypes.createPolicy('lit-html', { createHTML: (s) => s });
491
+ const policy = typeof window !== 'undefined' && window.trustedTypes && trustedTypes.createPolicy('lit-html', { createHTML: s => s });
501
492
  const commentMarker = ` ${marker} `;
502
493
  /**
503
494
  * The return type of `html`, which holds a Template and the values from
@@ -540,8 +531,7 @@ class TemplateResult {
540
531
  // We're in comment position if we have a comment open with no following
541
532
  // comment close. Because <-- can appear in an attribute value there can
542
533
  // be false positives.
543
- isCommentBinding = (commentOpen > -1 || isCommentBinding) &&
534
+ isCommentBinding = (commentOpen > -1 || isCommentBinding) && s.indexOf('-->', commentOpen + 1) === -1;
544
- s.indexOf('-->', commentOpen + 1) === -1;
545
535
  // Check to see if we have an attribute-like sequence preceding the
546
536
  // expression. This can match "name=value" like structures in text,
547
537
  // comments, and attribute values, so there can be false-positives.
@@ -553,14 +543,11 @@ class TemplateResult {
553
543
  // <!-- foo=${'bar'}--> are handled correctly in the attribute branch
554
544
  // below.
555
545
  html += s + (isCommentBinding ? commentMarker : nodeMarker);
556
- }
557
- else {
546
+ } else {
558
547
  // For attributes we use just a marker sentinel, and also append a
559
548
  // $lit$ suffix to the name to opt-out of attribute-specific parsing
560
549
  // that IE and Edge do for style and certain SVG attributes.
561
- html += s.substr(0, attributeMatch.index) + attributeMatch[1] +
550
+ html += s.substr(0, attributeMatch.index) + attributeMatch[1] + attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] + marker;
562
- attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] +
563
- marker;
564
551
  }
565
552
  }
566
553
  html += this.strings[l];
@@ -614,14 +601,15 @@ class SVGTemplateResult extends TemplateResult {
614
601
  * subject to an additional IP rights grant found at
615
602
  * http://polymer.github.io/PATENTS.txt
616
603
  */
617
- const isPrimitive = (value) => {
604
+ const isPrimitive = value => {
618
- return (value === null ||
619
- !(typeof value === 'object' || typeof value === 'function'));
605
+ return value === null || !(typeof value === 'object' || typeof value === 'function');
620
606
  };
621
- const isIterable = (value) => {
607
+ const isIterable = value => {
608
+ return (
622
- return Array.isArray(value) ||
609
+ Array.isArray(value) ||
623
610
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
624
- !!(value && value[Symbol.iterator]);
611
+ !!(value && value[Symbol.iterator])
612
+ );
625
613
  };
626
614
  /**
627
615
  * Writes attribute values to the DOM for a group of AttributeParts bound to a
@@ -679,8 +667,7 @@ class AttributeCommitter {
679
667
  const v = part.value;
680
668
  if (isPrimitive(v) || !isIterable(v)) {
681
669
  text += typeof v === 'string' ? v : String(v);
682
- }
683
- else {
670
+ } else {
684
671
  for (const t of v) {
685
672
  text += typeof t === 'string' ? t : String(t);
686
673
  }
@@ -772,8 +759,8 @@ class NodePart {
772
759
  * This part must be empty, as its contents are not automatically moved.
773
760
  */
774
761
  appendIntoPart(part) {
775
- part.__insert(this.startNode = createMarker());
762
+ part.__insert((this.startNode = createMarker()));
776
- part.__insert(this.endNode = createMarker());
763
+ part.__insert((this.endNode = createMarker()));
777
764
  }
778
765
  /**
779
766
  * Inserts this part after the `ref` part.
@@ -781,7 +768,7 @@ class NodePart {
781
768
  * This part must be empty, as its contents are not automatically moved.
782
769
  */
783
770
  insertAfterPart(ref) {
784
- ref.__insert(this.startNode = createMarker());
771
+ ref.__insert((this.startNode = createMarker()));
785
772
  this.endNode = ref.endNode;
786
773
  ref.endNode = this.startNode;
787
774
  }
@@ -805,21 +792,16 @@ class NodePart {
805
792
  if (value !== this.value) {
806
793
  this.__commitText(value);
807
794
  }
808
- }
809
- else if (value instanceof TemplateResult) {
795
+ } else if (value instanceof TemplateResult) {
810
796
  this.__commitTemplateResult(value);
811
- }
812
- else if (value instanceof Node) {
797
+ } else if (value instanceof Node) {
813
798
  this.__commitNode(value);
814
- }
815
- else if (isIterable(value)) {
799
+ } else if (isIterable(value)) {
816
800
  this.__commitIterable(value);
817
- }
818
- else if (value === nothing) {
801
+ } else if (value === nothing) {
819
802
  this.value = nothing;
820
803
  this.clear();
821
- }
822
- else {
804
+ } else {
823
805
  // Fallback, will render the string representation
824
806
  this.__commitText(value);
825
807
  }
@@ -841,25 +823,21 @@ class NodePart {
841
823
  // If `value` isn't already a string, we explicitly convert it here in case
842
824
  // it can't be implicitly converted - i.e. it's a symbol.
843
825
  const valueAsString = typeof value === 'string' ? value : String(value);
844
- if (node === this.endNode.previousSibling &&
845
- node.nodeType === 3 /* Node.TEXT_NODE */) {
826
+ if (node === this.endNode.previousSibling && node.nodeType === 3 /* Node.TEXT_NODE */) {
846
827
  // If we only have a single text node between the markers, we can just
847
828
  // set its value, rather than replacing it.
848
829
  // TODO(justinfagnani): Can we just check if this.value is primitive?
849
830
  node.data = valueAsString;
850
- }
851
- else {
831
+ } else {
852
832
  this.__commitNode(document.createTextNode(valueAsString));
853
833
  }
854
834
  this.value = value;
855
835
  }
856
836
  __commitTemplateResult(value) {
857
837
  const template = this.options.templateFactory(value);
858
- if (this.value instanceof TemplateInstance &&
838
+ if (this.value instanceof TemplateInstance && this.value.template === template) {
859
- this.value.template === template) {
860
839
  this.value.update(value.values);
861
- }
862
- else {
840
+ } else {
863
841
  // Make sure we propagate the template processor from the TemplateResult
864
842
  // so that we use its syntax extension, etc. The template factory comes
865
843
  // from the render function options so that it can control template
@@ -899,8 +877,7 @@ class NodePart {
899
877
  itemParts.push(itemPart);
900
878
  if (partIndex === 0) {
901
879
  itemPart.appendIntoPart(this);
902
- }
903
- else {
880
+ } else {
904
881
  itemPart.insertAfterPart(itemParts[partIndex - 1]);
905
882
  }
906
883
  }
@@ -952,8 +929,7 @@ class BooleanAttributePart {
952
929
  if (this.value !== value) {
953
930
  if (value) {
954
931
  this.element.setAttribute(this.name, '');
955
- }
956
- else {
932
+ } else {
957
933
  this.element.removeAttribute(this.name);
958
934
  }
959
935
  this.value = value;
@@ -973,8 +949,7 @@ class BooleanAttributePart {
973
949
  class PropertyCommitter extends AttributeCommitter {
974
950
  constructor(element, name, strings) {
975
951
  super(element, name, strings);
976
- this.single =
977
- (strings.length === 2 && strings[0] === '' && strings[1] === '');
952
+ this.single = strings.length === 2 && strings[0] === '' && strings[1] === '';
978
953
  }
979
954
  _createPart() {
980
955
  return new PropertyPart(this);
@@ -993,8 +968,7 @@ class PropertyCommitter extends AttributeCommitter {
993
968
  }
994
969
  }
995
970
  }
996
- class PropertyPart extends AttributePart {
971
+ class PropertyPart extends AttributePart {}
997
- }
998
972
  // Detect event listener options support. If the `capture` property is read
999
973
  // from the options object, then options are supported. If not, then the third
1000
974
  // argument to add/removeEventListener is interpreted as the boolean capture
@@ -1008,14 +982,13 @@ let eventOptionsSupported = false;
1008
982
  get capture() {
1009
983
  eventOptionsSupported = true;
1010
984
  return false;
1011
- }
985
+ },
1012
986
  };
1013
987
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1014
988
  window.addEventListener('test', options, options);
1015
989
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1016
990
  window.removeEventListener('test', options, options);
1017
- }
1018
- catch (_e) {
991
+ } catch (_e) {
1019
992
  // event options not supported
1020
993
  }
1021
994
  })();
@@ -1026,7 +999,7 @@ class EventPart {
1026
999
  this.element = element;
1027
1000
  this.eventName = eventName;
1028
1001
  this.eventContext = eventContext;
1029
- this.__boundHandleEvent = (e) => this.handleEvent(e);
1002
+ this.__boundHandleEvent = e => this.handleEvent(e);
1030
1003
  }
1031
1004
  setValue(value) {
1032
1005
  this.__pendingValue = value;
@@ -1042,11 +1015,10 @@ class EventPart {
1042
1015
  }
1043
1016
  const newListener = this.__pendingValue;
1044
1017
  const oldListener = this.value;
1045
- const shouldRemoveListener = newListener == null ||
1018
+ const shouldRemoveListener =
1019
+ newListener == null ||
1046
- oldListener != null &&
1020
+ (oldListener != null &&
1047
- (newListener.capture !== oldListener.capture ||
1021
+ (newListener.capture !== oldListener.capture || newListener.once !== oldListener.once || newListener.passive !== oldListener.passive));
1048
- newListener.once !== oldListener.once ||
1049
- newListener.passive !== oldListener.passive);
1050
1022
  const shouldAddListener = newListener != null && (oldListener == null || shouldRemoveListener);
1051
1023
  if (shouldRemoveListener) {
1052
1024
  this.element.removeEventListener(this.eventName, this.__boundHandleEvent, this.__options);
@@ -1061,8 +1033,7 @@ class EventPart {
1061
1033
  handleEvent(event) {
1062
1034
  if (typeof this.value === 'function') {
1063
1035
  this.value.call(this.eventContext || this.element, event);
1064
- }
1065
- else {
1036
+ } else {
1066
1037
  this.value.handleEvent(event);
1067
1038
  }
1068
1039
  }
@@ -1070,10 +1041,7 @@ class EventPart {
1070
1041
  // We copy options because of the inconsistent behavior of browsers when reading
1071
1042
  // the third argument of add/removeEventListener. IE11 doesn't support options
1072
1043
  // at all. Chrome 41 only reads `capture` if the argument is an object.
1073
- const getOptions = (o) => o &&
1074
- (eventOptionsSupported ?
1075
- { capture: o.capture, passive: o.passive, once: o.once } :
1044
+ const getOptions = o => o && (eventOptionsSupported ? { capture: o.capture, passive: o.passive, once: o.once } : o.capture);
1076
- o.capture);
1077
1045
 
1078
1046
  /**
1079
1047
  * @license
@@ -1148,7 +1116,7 @@ function templateFactory(result) {
1148
1116
  if (templateCache === undefined) {
1149
1117
  templateCache = {
1150
1118
  stringsArray: new WeakMap(),
1151
- keyString: new Map()
1119
+ keyString: new Map(),
1152
1120
  };
1153
1121
  templateCaches.set(result.type, templateCache);
1154
1122
  }
@@ -1206,7 +1174,7 @@ const render = (result, container, options) => {
1206
1174
  let part = parts.get(container);
1207
1175
  if (part === undefined) {
1208
1176
  removeNodes(container, container.firstChild);
1209
- parts.set(container, part = new NodePart(Object.assign({ templateFactory }, options)));
1177
+ parts.set(container, (part = new NodePart(Object.assign({ templateFactory }, options))));
1210
1178
  part.appendInto(container);
1211
1179
  }
1212
1180
  part.setValue(result);
@@ -1243,4 +1211,34 @@ const html = (strings, ...values) => new TemplateResult(strings, values, 'html',
1243
1211
  */
1244
1212
  const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg', defaultTemplateProcessor);
1245
1213
 
1246
- export { AttributeCommitter, AttributePart, BooleanAttributePart, DefaultTemplateProcessor, EventPart, NodePart, PropertyCommitter, PropertyPart, SVGTemplateResult, Template, TemplateInstance, TemplateResult, createMarker, defaultTemplateProcessor, directive, html, isDirective, isIterable, isPrimitive, isTemplatePartActive, noChange, nothing, parts, removeNodes, render, reparentNodes, svg, templateCaches, templateFactory };
1214
+ export {
1215
+ AttributeCommitter,
1216
+ AttributePart,
1217
+ BooleanAttributePart,
1218
+ DefaultTemplateProcessor,
1219
+ EventPart,
1220
+ NodePart,
1221
+ PropertyCommitter,
1222
+ PropertyPart,
1223
+ SVGTemplateResult,
1224
+ Template,
1225
+ TemplateInstance,
1226
+ TemplateResult,
1227
+ createMarker,
1228
+ defaultTemplateProcessor,
1229
+ directive,
1230
+ html,
1231
+ isDirective,
1232
+ isIterable,
1233
+ isPrimitive,
1234
+ isTemplatePartActive,
1235
+ noChange,
1236
+ nothing,
1237
+ parts,
1238
+ removeNodes,
1239
+ render,
1240
+ reparentNodes,
1241
+ svg,
1242
+ templateCaches,
1243
+ templateFactory,
1244
+ };
test/index.test.js CHANGED
@@ -1,10 +1,24 @@
1
- import { expect, test, jest } from '@jest/globals'
1
+ import { expect, test, jest } from '@jest/globals';
2
+ import {
3
+ html,
4
+ render,
5
+ number,
6
+ boolean,
7
+ string,
8
+ array,
9
+ object,
10
+ setLogError,
11
+ defineElement,
12
+ getElement,
13
+ useConfig,
14
+ useLocation,
15
+ useState,
2
- import { html, render, number, boolean, string, array, object } from '../src/index.js';
16
+ } from '../src/index.js';
3
17
 
4
18
  const logMock = jest.fn();
5
- window.logError = logMock;
19
+ setLogError(logMock);
6
20
 
7
- const expectError = (msg) => expect(logMock).toHaveBeenCalledWith(msg);
21
+ const expectError = msg => expect(logMock).toHaveBeenCalledWith(msg);
8
22
 
9
23
  const primitives = [
10
24
  {
@@ -24,12 +38,12 @@ const primitives = [
24
38
  validator: string,
25
39
  valid: ['', '123'],
26
40
  invalid: [123, false, {}, [], new Date()],
27
- }
41
+ },
28
- ]
42
+ ];
29
43
 
30
44
  primitives.forEach(value =>
31
45
  it(`${value.type}`, () => {
32
- const context = 'key'
46
+ const context = 'key';
33
47
  expect(value.validator.type).toEqual(value.type);
34
48
  expect(value.validator.isRequired.type).toEqual(value.type);
35
49
  value.validator.validate(context);
@@ -41,7 +55,7 @@ primitives.forEach(value =>
41
55
  expectError(`'key' Field is required`);
42
56
  for (const v of value.invalid) {
43
57
  value.validator.validate(context, v);
44
- expectError(`'key' Expected type '${value.type}' got type '${typeof v}'`)
58
+ expectError(`'key' Expected type '${value.type}' got type '${typeof v}'`);
45
59
  }
46
60
  })
47
61
  );
@@ -57,8 +71,8 @@ test('object', () => {
57
71
  const schema = object({
58
72
  address: object({
59
73
  street: string,
60
- })
74
+ }),
61
- })
75
+ });
62
76
  schema.validate(context, {});
63
77
  schema.validate(context, '123');
64
78
  expectError(`'data' Expected object literal '{}' got 'string'`);
@@ -84,8 +98,8 @@ test('object', () => {
84
98
  const schema2 = object({
85
99
  address: object({
86
100
  street: string.isRequired,
87
- })
101
+ }),
88
- })
102
+ });
89
103
  schema2.validate(context, {});
90
104
  schema2.validate(context, {
91
105
  address: {
@@ -96,7 +110,7 @@ test('object', () => {
96
110
  address: {},
97
111
  });
98
112
  expectError(`'data.address.street' Field is required`);
99
- })
113
+ });
100
114
 
101
115
  test('array', () => {
102
116
  const context = 'items';
@@ -109,7 +123,7 @@ test('array', () => {
109
123
 
110
124
  const schema = object({
111
125
  street: string.isRequired,
112
- })
126
+ });
113
127
  array(schema).validate(context, []);
114
128
  array(schema).validate(context, [{ street: '123' }, { street: '456' }, { street: '789' }]);
115
129
  array(schema).validate(context, [{}]);
@@ -123,7 +137,7 @@ test('array', () => {
123
137
  });
124
138
 
125
139
  test('render', async () => {
126
- const data = { name: '123', address: { street: '1' } }
140
+ const data = { name: '123', address: { street: '1' } };
127
141
  const template = html`
128
142
  <div>
129
143
  <app-counter name="123" details=${data}></app-counter>
@@ -135,4 +149,43 @@ test('render', async () => {
135
149
  <app-counter name=\"123\" details=\"{'name':'123','address':{'street':'1'}}\"></app-counter>
136
150
  </div>
137
151
  `);
138
- })
152
+ });
153
+
154
+ test('defineElement', async () => {
155
+ const attrTypes = {
156
+ address: object({
157
+ street: string.isRequired,
158
+ }).isRequired,
159
+ };
160
+ const AppItem = ({ address: { street } }) => {
161
+ const [count] = useState(0);
162
+ return html`
163
+ <div>
164
+ <p>street: ${street}</p>
165
+ <p>count: ${count}</p>
166
+ </div>
167
+ `;
168
+ };
169
+ defineElement('app-item', AppItem, attrTypes);
170
+ const Clazz = getElement('app-item');
171
+ const instance = new Clazz([{ name: 'address', value: JSON.stringify({ street: '123' }).replace(/"/g, `'`) }]);
172
+ const res = await instance.render();
173
+ expect(res).toEqual(`
174
+ <div>
175
+ <p>street: 123</p>
176
+ <p>count: 0</p>
177
+ </div>
178
+ `);
179
+ });
180
+
181
+ test('useConfig', async () => {
182
+ expect(useConfig()).toBe(undefined);
183
+ global.config = {};
184
+ expect(useConfig()).toMatchObject({});
185
+ });
186
+
187
+ test('useLocation', async () => {
188
+ expect(useLocation()).toBe(undefined);
189
+ global.location = {};
190
+ expect(useLocation()).toMatchObject({});
191
+ });
test/setup.js DELETED
@@ -1,2 +0,0 @@
1
- global.window = {}
2
- global.Document = class { };