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


987a5b1f Peter John

4 years ago
improve state api
Files changed (5) hide show
  1. __snapshots__/index.test.js.snap +301 -25
  2. example/app-counter.js +3 -3
  3. index.d.ts +4 -8
  4. index.js +20 -18
  5. index.test.js +73 -230
__snapshots__/index.test.js.snap CHANGED
@@ -1,17 +1,51 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`AtomsElement 1`] = `
3
+ exports[`compileTw 1`] = `
4
4
  "
5
-
5
+ .text-white {
6
+ color: rgba(255, 255, 255, 1);
7
+ }
8
+
9
+ .font-bold {
10
+ font-weight: 700;
11
+ }
12
+
13
+ .border {
14
+ border-width: 1px;
15
+ }
16
+
17
+ .border-black {
6
- <div perPage=\\"1\\">
18
+ border-color: rgba(0, 0, 0, 1);
19
+ }
20
+
21
+ .p-4 {
7
- <p>street: 123</p> <p>count: 0</p> <p>sum: 10</p> <div><p>render item 1</p></div> </div>
22
+ padding: 1rem;
23
+ }
24
+
25
+ .m-4 {
26
+ margin: 1rem;
27
+ }
28
+
29
+ .w-20 {
30
+ width: 5rem;
31
+ }
32
+
33
+ .h-20 {
8
- <style>
34
+ height: 5rem;
9
-
10
- </style>
35
+ }
11
36
  "
12
37
  `;
13
38
 
39
+ exports[`createElement with attrs and state 1`] = `
40
+ "
41
+ <div>
42
+ <div>
43
+ <span>perPage: 5</span> </div> </div> <span>Count: 3</span> <button>Set</button> "
44
+ `;
45
+
46
+ exports[`createElement without attrs and state 1`] = `" <div></div>"`;
47
+
14
- exports[`Page 1`] = `
48
+ exports[`createPage 1`] = `
15
49
  "
16
50
  <!DOCTYPE html>
17
51
  <html lang=\\"en\\">
@@ -24,7 +58,259 @@ exports[`Page 1`] = `
24
58
  <link rel=\\"icon\\" type=\\"image/png\\" href=\\"/assets/icon.png\\" />
25
59
 
26
60
  <title>123</title> <meta name=\\"title\\" content=\\"123\\"/> <meta name=\\"description\\" content=\\"123\\"/>
27
- <style>
61
+ <style id=\\"global\\">
62
+ *, ::before, ::after {
63
+
64
+ box-sizing: border-box;
65
+ border-width: 0;
66
+ border-style: solid;
67
+ border-color: #e5e7eb;
68
+
69
+ }
70
+ hr {
71
+
72
+ height: 0;
73
+ color: inherit;
74
+ border-top-width: 1px;
75
+
76
+ }
77
+ abbr[title] {
78
+
79
+ -webkit-text-decoration: underline dotted;
80
+ text-decoration: underline dotted;
81
+
82
+ }
83
+ b, strong {
84
+
85
+ font-weight: bolder;
86
+
87
+ }
88
+ code, kbd, samp, pre {
89
+
90
+ font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
91
+ font-size: 1em;
92
+
93
+ }
94
+ small {
95
+
96
+ font-size: 80%;
97
+
98
+ }
99
+ sub, sup {
100
+
101
+ font-size: 75%;
102
+ line-height: 0;
103
+ position: relative;
104
+ vertical-align: baseline;
105
+
106
+ }
107
+ sub {
108
+
109
+ bottom: -0.25em;
110
+
111
+ }
112
+ sup {
113
+
114
+ top: -0.5em;
115
+
116
+ }
117
+ table {
118
+
119
+ text-indent: 0;
120
+ border-color: inherit;
121
+ border-collapse: collapse;
122
+
123
+ }
124
+ button, input, optgroup, select, textarea {
125
+
126
+ font-size: 100%;
127
+ margin: 0;
128
+ padding: 0;
129
+ line-height: inherit;
130
+ color: inherit;
131
+
132
+ }
133
+ button, select {
134
+
135
+
136
+ }
137
+ button, [type='button'], [type='reset'], [type='submit'] {
138
+
139
+
140
+ }
141
+ ::-moz-focus-inner {
142
+
143
+ border-style: none;
144
+ padding: 0;
145
+
146
+ }
147
+ :-moz-focusring {
148
+
149
+ outline: 1px dotted ButtonText;
150
+
151
+ }
152
+ :-moz-ui-invalid {
153
+
154
+ box-shadow: none;
155
+
156
+ }
157
+ legend {
158
+
159
+ padding: 0;
160
+
161
+ }
162
+ progress {
163
+
164
+ vertical-align: baseline;
165
+
166
+ }
167
+ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
168
+
169
+ height: auto;
170
+
171
+ }
172
+ [type='search'] {
173
+
174
+ -webkit-appearance: textfield;
175
+ outline-offset: -2px;
176
+
177
+ }
178
+ ::-webkit-search-decoration {
179
+
180
+ -webkit-appearance: none;
181
+
182
+ }
183
+ ::-webkit-file-upload-button {
184
+
185
+ -webkit-appearance: button;
186
+ font: inherit;
187
+
188
+ }
189
+ summary {
190
+
191
+ display: list-item;
192
+
193
+ }
194
+ blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre {
195
+
196
+ margin: 0;
197
+
198
+ }
199
+ button {
200
+
201
+ background-image: none;
202
+
203
+ outline: 1px dotted, 5px auto -webkit-focus-ring-color;
204
+
205
+ }
206
+ fieldset {
207
+
208
+ margin: 0;
209
+ padding: 0;
210
+
211
+ }
212
+ ol, ul {
213
+
214
+ list-style: none;
215
+ margin: 0;
216
+ padding: 0;
217
+
218
+ }
219
+ img {
220
+
221
+ border-style: solid;
222
+
223
+ }
224
+ textarea {
225
+
226
+ resize: vertical;
227
+
228
+ }
229
+ input::-moz-placeholder, textarea::-moz-placeholder {
230
+
231
+ opacity: 1;
232
+ color: #9ca3af;
233
+
234
+ }
235
+ input:-ms-input-placeholder, textarea:-ms-input-placeholder {
236
+
237
+ opacity: 1;
238
+ color: #9ca3af;
239
+
240
+ }
241
+ input::placeholder, textarea::placeholder {
242
+
243
+ opacity: 1;
244
+ color: #9ca3af;
245
+
246
+ }
247
+ button, [role='button'] {
248
+
249
+ cursor: pointer;
250
+
251
+ }
252
+ h1, h2, h3, h4, h5, h6 {
253
+
254
+ font-size: inherit;
255
+ font-weight: inherit;
256
+
257
+ }
258
+ a {
259
+
260
+ color: inherit;
261
+ text-decoration: inherit;
262
+
263
+ }
264
+ pre, code, kbd, samp {
265
+
266
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
267
+
268
+ }
269
+ img, svg, video, canvas, audio, iframe, embed, object {
270
+
271
+ display: block;
272
+ vertical-align: middle;
273
+
274
+ }
275
+ img, video {
276
+
277
+ max-width: 100%;
278
+ height: auto;
279
+
280
+ }
281
+ html {
282
+
283
+ -moz-tab-size: 4;
284
+ -o-tab-size: 4;
285
+ tab-size: 4;
286
+ line-height: 1.5;
287
+ -webkit-text-size-adjust: 100%;
288
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
289
+ width: 100%;
290
+ height: 100%;
291
+
292
+ }
293
+ body {
294
+
295
+ margin: 0px;
296
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
297
+ line-height: 1.4;
298
+ background-color: white;
299
+ width: 100%;
300
+ height: 100%;
301
+ display: flex;
302
+ flex-direction: column;
303
+ flex: 1 1 0%;
304
+ min-width: 320px;
305
+ min-height: 100vh;
306
+ font-weight: 400;
307
+ color: rgba(44, 62, 80, 1);
308
+ direction: ltr;
309
+ font-synthesis: none;
310
+ text-rendering: optimizeLegibility;
311
+
312
+ }
313
+
28
314
 
29
315
  .flex {
30
316
  display: flex;
@@ -60,7 +346,6 @@ line-height: 1;
60
346
  <app-header></app-header> <main class=\\"flex flex-1 flex-col mt-20 items-center\\">
61
347
  <h1 class=\\"text-5xl\\">123</h1> </main> </div>
62
348
  <script>
63
- window.__DEV__ = true;
64
349
  window.props = {\\"config\\":{\\"title\\":\\"123\\"}};
65
350
  </script>
66
351
  <script type=\\"module\\"><script>
@@ -69,15 +354,6 @@ line-height: 1;
69
354
  "
70
355
  `;
71
356
 
72
- exports[`createElement 1`] = `
73
- "
74
- <div></div>
75
- <style>
76
-
77
- </style>
78
- "
79
- `;
80
-
81
357
  exports[`css 1`] = `
82
358
  "button {
83
359
 
@@ -102,12 +378,6 @@ container {
102
378
  "
103
379
  `;
104
380
 
105
- exports[`render 1`] = `
106
- "
107
- <div>
108
- <app-counter name=\\"123\\" class=\\"abc high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
109
- `;
110
-
111
381
  exports[`render attribute keys 1`] = `
112
382
  "
113
383
  <div>
@@ -132,3 +402,9 @@ exports[`render multi template 1`] = `
132
402
  exports[`render single template 1`] = `" <div>NoCountry false</div>"`;
133
403
 
134
404
  exports[`render unsafeHTML 1`] = `" <div><div><p class=\\"123\\">this is unsafe</p></div></div>"`;
405
+
406
+ exports[`renderHtml 1`] = `
407
+ "
408
+ <div>
409
+ <app-counter name=\\"123\\" class=\\"abc high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
410
+ `;
example/app-counter.js CHANGED
@@ -1,11 +1,11 @@
1
- import { createElement, createState, html } from '../index.js';
1
+ import { createElement, html } from '../index.js';
2
2
 
3
3
  export default createElement({
4
4
  name: 'app-counter',
5
5
  attrs: {
6
- name: '',
6
+ name: String,
7
7
  meta: {
8
- start: '',
8
+ start: String,
9
9
  },
10
10
  },
11
11
  state: {
index.d.ts CHANGED
@@ -51,23 +51,19 @@ export type PageRenderProps = {
51
51
  }
52
52
  export type Handler = (props: any) => string;
53
53
 
54
- export const createPage = (props: { head: Handler, body: Handler}) => (props: PageRenderProps) => string;
54
+ export function createPage(props: { head: Handler, body: Handler}): (props: PageRenderProps) => string;
55
55
 
56
56
  export type State<P, Q> = {
57
- getState: () => P;
57
+ getValue: () => P;
58
58
  subscribe: (fn: (v: P) => void) => void;
59
- } & { [K in keyof Q]: (v: any) => void}
59
+ } & { actions: { [K in keyof Q]: (v: any) => void}}
60
60
 
61
61
  export function createState<P, Q extends {[k: string]: (state: P, v: any) => P}>(props: { state: P, reducer: Q }): State<P, Q>;
62
62
 
63
-
64
- export type ElementState<P, Q> = P & { [K in keyof Q]: (v: any) => void}
65
-
66
-
67
63
  export type CreateElementProps<N, P, Q> = {
68
64
  name: string;
69
65
  attrs: N;
70
- state: State<P, Q>;
66
+ state: P;
71
67
  reducer: Q
72
68
  render: (props: { attrs: N, state: P, actions: { [K in keyof Q]: (v: any) => void;} }) => any
73
69
  }
index.js CHANGED
@@ -345,7 +345,7 @@ const getClassList = (template) => {
345
345
  return classes;
346
346
  };
347
347
 
348
- export const generateTWStyleSheet = (classList) => {
348
+ export const compileTw = (classList) => {
349
349
  let styleSheet = ``;
350
350
  classList.forEach((cls) => {
351
351
  const item = classLookup[cls];
@@ -928,6 +928,16 @@ const microtask = (flush) => () => queueMicrotask(flush);
928
928
  export const createState = ({ state, reducer }) => {
929
929
  let initial = state;
930
930
  const subs = new Set();
931
+ const actions = Object.keys(reducer).reduce((acc, key) => {
932
+ const reduce = reducer[key];
933
+ acc[key] = (v) => {
934
+ initial = reduce(initial, v);
935
+ subs.forEach((sub) => {
936
+ sub(initial);
937
+ });
938
+ };
939
+ return acc;
940
+ }, {});
931
941
  return {
932
942
  getValue: () => initial,
933
943
  subscribe: (fn) => {
@@ -936,16 +946,8 @@ export const createState = ({ state, reducer }) => {
936
946
  unsubscribe: (fn) => {
937
947
  subs.remove(fn);
938
948
  },
939
- actions: Object.keys(reducer).reduce((acc, key) => {
940
- const reduce = reducer[key];
941
- acc[key] = (v) => {
949
+ actions,
942
- initial = reduce(initial, v);
943
- subs.forEach((sub) => {
944
- sub(initial);
950
+ ...actions,
945
- });
946
- };
947
- return acc;
948
- }, {}),
949
951
  };
950
952
  };
951
953
 
@@ -975,7 +977,7 @@ export const createElement = ({ name, attrs, state, reducer, render: renderFn })
975
977
  this._dirty = false;
976
978
  this._connected = false;
977
979
  this.attrs = ssrAttrs || attrs;
978
- this.state = createState({ state, reducer });
980
+ this.state = state ? createState({ state, reducer }) : null;
979
981
  this.config = isBrowser ? window.config : global.config;
980
982
  this.location = isBrowser ? window.location : global.location;
981
983
  // this.prevClassList = [];
@@ -1033,8 +1035,8 @@ export const createElement = ({ name, attrs, state, reducer, render: renderFn })
1033
1035
  render() {
1034
1036
  const template = renderFn({
1035
1037
  attrs: this.attrs,
1036
- state: this.state.getValue(),
1038
+ state: this.state ? this.state.getValue() : {},
1037
- actions: this.state.actions,
1039
+ actions: this.state ? this.state.actions : {},
1038
1040
  });
1039
1041
  if (isBrowser) {
1040
1042
  // TODO: this can be optimized when we know whether the value belongs in a class (AttributePart)
@@ -1044,20 +1046,20 @@ export const createElement = ({ name, attrs, state, reducer, render: renderFn })
1044
1046
  return !globalStyles.includes('.' + cls);
1045
1047
  });
1046
1048
  if (newClassList.length > 0) {
1047
- document.getElementById('global').textContent += generateTWStyleSheet(newClassList);
1049
+ document.getElementById('global').textContent += compileTw(newClassList);
1048
1050
  }
1049
1051
  renderHtml(template, this);
1050
1052
  // For shadows only
1051
1053
  // if (!this.styleElement) {
1052
1054
  // render(template, this.shadow);
1053
- // const styleSheet = generateTWStyleSheet(classList);
1055
+ // const styleSheet = compileTw(classList);
1054
1056
  // this.prevClassList = classList;
1055
1057
  // this.styleElement = document.createElement('style');
1056
1058
  // this.shadow.appendChild(this.styleElement).textContent = css(pageStyles) + styleSheet;
1057
1059
  // } else {
1058
1060
  // const missingClassList = classList.filter((cls) => !this.prevClassList.includes(cls));
1059
1061
  // if (missingClassList.length > 0) {
1060
- // const styleSheet = generateTWStyleSheet(missingClassList);
1062
+ // const styleSheet = compileTw(missingClassList);
1061
1063
  // this.styleElement.textContent += '\n' + styleSheet;
1062
1064
  // this.prevClassList.push(...missingClassList);
1063
1065
  // }
@@ -1090,7 +1092,7 @@ export const createPage = ({ head, body }) => {
1090
1092
  ${headHtml}
1091
1093
  <style id="global">
1092
1094
  ${css(pageStyles)}
1093
- ${generateTWStyleSheet(new Set(classes.split(' ')))}
1095
+ ${compileTw(new Set(classes.split(' ')))}
1094
1096
  </style>
1095
1097
  ${headScript}
1096
1098
  </head>
index.test.js CHANGED
@@ -1,169 +1,8 @@
1
1
  import { expect, test, jest } from '@jest/globals';
2
- import { AtomsElement, createElement, createPage, createState, html, render, number, boolean, string, array, object, unsafeHTML, css } from './index.js';
2
+ import { getElement, createElement, createPage, createState, html, renderHtml, unsafeHTML, css, compileTw } from './index.js';
3
3
 
4
4
  global.__DEV = true;
5
5
 
6
- const primitives = [
7
- {
8
- type: 'number',
9
- valid: [123, 40.5, 6410],
10
- invalid: ['123', false, {}, [], new Date()],
11
- },
12
- {
13
- type: 'boolean',
14
- valid: [false, true],
15
- invalid: ['123', 123, {}, [], new Date()],
16
- },
17
- {
18
- type: 'string',
19
- valid: ['', '123'],
20
- invalid: [123, false, {}, [], new Date()],
21
- },
22
- ];
23
- const primitiveTypes = {
24
- number: number,
25
- boolean: boolean,
26
- string: string,
27
- };
28
-
29
- primitives.forEach((value) =>
30
- it(`${value.type}`, () => {
31
- const spy = jest.spyOn(global.console, 'warn').mockImplementation();
32
- const context = 'key';
33
- const validator = primitiveTypes[value.type]();
34
- const validatorReq = primitiveTypes[value.type]().required();
35
- expect(validator.type).toEqual(value.type);
36
- expect(validator.__required).toEqual(undefined);
37
- expect(validatorReq.__required).toEqual(true);
38
- validator.validate(context);
39
- for (const v of value.valid) {
40
- validator.validate(context, v);
41
- validatorReq.validate(context, v);
42
- }
43
- validatorReq.validate(context);
44
- expect(console.warn).toHaveBeenCalledWith(`'key' Field is required`);
45
- for (const v of value.invalid) {
46
- validator.validate(context, v);
47
- expect(console.warn).toHaveBeenCalledWith(`'key' Expected type '${value.type}' got type '${typeof v}'`);
48
- }
49
- spy.mockRestore();
50
- }),
51
- );
52
-
53
- test.only('createState', () => {
54
- // computed: {
55
- // sum: (state, v) => state.count + v,
56
- // },
57
- const countState = createState({
58
- state: {
59
- count: 0,
60
- },
61
- reducer: {
62
- increment: (state, a) => ({ ...state, count: state.count + a }),
63
- decrement: (state, a) => ({ ...state, count: state.count - a }),
64
- },
65
- });
66
- createElement({
67
- name: 'hello',
68
- attrs: {
69
- name: '123',
70
- },
71
- state: countState,
72
- render: ({ attrs, state, actions }) => {
73
- console.log('attrs', attrs);
74
- },
75
- });
76
- // countState.subscribe((v) => {
77
- // console.log(v);
78
- // });
79
- // countState.increment(4);
80
- // console.log(countState.sum(21));
81
- // countState.decrement(1);
82
- // console.log(countState.sum(21));
83
- // countState.decrement(2);
84
- // console.log(countState.sum(21));
85
- });
86
-
87
- test('object', () => {
88
- const spy = jest.spyOn(global.console, 'warn').mockImplementation();
89
- const context = 'data';
90
- object({}).validate(context, { name: '123' });
91
- object({ name: string() }).validate(context, { name: '123' });
92
- object({ name: string().required() }).validate(context, { name: '' });
93
- object({ name: string().required() }).validate(context, {});
94
- expect(console.warn).toHaveBeenCalledWith(`'data.name' Field is required`);
95
-
96
- const schema = object({
97
- address: object({
98
- street: string(),
99
- }),
100
- });
101
- schema.validate(context, {});
102
- schema.validate(context, '123');
103
- expect(console.warn).toHaveBeenCalledWith(`'data' Expected object literal '{}' got 'string'`);
104
- schema.validate(context, {
105
- address: {},
106
- });
107
- schema.validate(context, {
108
- address: '123',
109
- });
110
- expect(console.warn).toHaveBeenCalledWith(`'data.address' Expected object literal '{}' got 'string'`);
111
- schema.validate(context, {
112
- address: {
113
- street: 'avenue 1',
114
- },
115
- });
116
- schema.validate(context, {
117
- address: {
118
- street: false,
119
- },
120
- });
121
- expect(console.warn).toHaveBeenCalledWith(`'data.address.street' Expected type 'string' got type 'boolean'`);
122
-
123
- const schema2 = object({
124
- address: object({
125
- street: string().required(),
126
- }),
127
- });
128
- schema2.validate(context, {});
129
- schema2.validate(context, {
130
- address: {
131
- street: '11th avenue',
132
- },
133
- });
134
- schema2.validate(context, {
135
- address: {},
136
- });
137
- expect(console.warn).toHaveBeenCalledWith(`'data.address.street' Field is required`);
138
- spy.mockRestore();
139
- });
140
-
141
- test('array', () => {
142
- const spy = jest.spyOn(global.console, 'warn').mockImplementation();
143
- const context = 'items';
144
- array(string()).validate(context, ['123']);
145
- array(string()).validate(context, [123]);
146
- expect(console.warn).toHaveBeenCalledWith(`'items[0]' Expected type 'string' got type 'number'`);
147
- array(array(string())).validate(context, [['123']]);
148
- array(array(string())).validate(context, [[123]]);
149
- expect(console.warn).toHaveBeenCalledWith(`'items[0][0]' Expected type 'string' got type 'number'`);
150
-
151
- const schema = object({
152
- street: string().required(),
153
- });
154
- array(schema).validate(context, []);
155
- array(schema).validate(context, [{ street: '123' }, { street: '456' }, { street: '789' }]);
156
- array(schema).validate(context, [{}]);
157
- expect(console.warn).toHaveBeenCalledWith(`'items[0].street' Field is required`);
158
- array(schema).validate(context, [{ street: false }]);
159
- expect(console.warn).toHaveBeenCalledWith(`'items[0].street' Expected type 'string' got type 'boolean'`);
160
- array(schema).validate(context, [{ street: '123' }, {}]);
161
- expect(console.warn).toHaveBeenCalledWith(`'items[1].street' Field is required`);
162
- array(schema).validate(context, [{ street: '123' }, { street: false }]);
163
- expect(console.warn).toHaveBeenCalledWith(`'items[1].street' Expected type 'string' got type 'boolean'`);
164
- spy.mockRestore();
165
- });
166
-
167
6
  test('css', () => {
168
7
  const styles = css({
169
8
  button: {
@@ -190,7 +29,11 @@ test('css', () => {
190
29
  expect(styles).toMatchSnapshot();
191
30
  });
192
31
 
32
+ test('compileTw', () => {
33
+ expect(compileTw('text-white font-bold border border-black p-4 m-4 w-20 h-20'.split(' '))).toMatchSnapshot();
34
+ });
35
+
193
- test('render', async () => {
36
+ test('renderHtml', async () => {
194
37
  const age = 1;
195
38
  const data = { name: '123', address: { street: '1' } };
196
39
  const items = [1, 2, 3];
@@ -200,7 +43,7 @@ test('render', async () => {
200
43
  <app-counter name="123" class="abc ${highlight}" age=${age} details1=${data} items=${items}></app-counter>
201
44
  </div>
202
45
  `;
203
- const res = await render(template);
46
+ const res = await renderHtml(template);
204
47
  expect(res).toMatchSnapshot();
205
48
  });
206
49
 
@@ -210,7 +53,7 @@ test('render attribute keys', async () => {
210
53
  <app-counter name="123" perPage="1"></app-counter>
211
54
  </div>
212
55
  `;
213
- const res = await render(template);
56
+ const res = await renderHtml(template);
214
57
  expect(res).toMatchSnapshot();
215
58
  });
216
59
 
@@ -224,20 +67,20 @@ test('render attributes within quotes', async () => {
224
67
  <app-counter name="123" class=${classes} age="${age}" details1="${data}" items="${items}"></app-counter>
225
68
  </div>
226
69
  `;
227
- const res = await render(template);
70
+ const res = await renderHtml(template);
228
71
  expect(res).toMatchSnapshot();
229
72
  });
230
73
 
231
74
  test('render unsafeHTML', async () => {
232
75
  const textContent = `<div><p class="123">this is unsafe</p></div>`;
233
76
  const template = html` <div>${unsafeHTML(textContent)}</div> `;
234
- const res = await render(template);
77
+ const res = await renderHtml(template);
235
78
  expect(res).toMatchSnapshot();
236
79
  });
237
80
 
238
81
  test('render single template', async () => {
239
82
  const template = html` <div>${html`NoCountry ${false}`}</div> `;
240
- const res = await render(template);
83
+ const res = await renderHtml(template);
241
84
  expect(res).toMatchSnapshot();
242
85
  });
243
86
 
@@ -253,83 +96,83 @@ test('render multi template', async () => {
253
96
  )}
254
97
  </div>
255
98
  `;
256
- const res = await render(template);
99
+ const res = await renderHtml(template);
257
100
  expect(res).toMatchSnapshot();
258
101
  });
259
102
 
260
- test('AtomsElement', async () => {
103
+ test('createState', () => {
261
- class AppItem extends AtomsElement {
104
+ const countState = createState({
262
- static name = 'app-item';
263
-
264
- static attrTypes = {
265
- perPage: string().required(),
266
- address: object({
267
- street: string().required(),
268
- }).required(),
269
- };
270
-
271
- static stateTypes = {
272
- count: number().required().default(0),
273
- };
274
-
275
- static computedTypes = {
276
- sum: number()
277
- .required()
278
- .compute('count', (count) => {
279
- return count + 10;
280
- }),
281
- };
282
-
283
- static styles = css({
284
- div: {
105
+ state: {
285
- color: 'red',
106
+ count: 0,
286
- },
107
+ },
287
- });
288
-
289
- render() {
108
+ reducer: {
290
- const {
291
- perPage,
292
- address: { street },
293
- } = this.attrs;
294
- const { count, setCount } = this.state;
109
+ increment: (state, a) => ({ ...state, count: state.count + a }),
295
- const { sum } = this.computed;
296
- return html`
297
- <div perPage=${perPage}>
298
- <p>street: ${street}</p>
110
+ decrement: (state, a) => ({ ...state, count: state.count - a }),
299
- <p>count: ${count}</p>
300
- <p>sum: ${sum}</p>
301
- ${this.renderItem()}
302
- </div>
303
- `;
304
- }
111
+ },
305
- }
306
- AppItem.register();
307
- const Clazz = AtomsElement.getElement('app-item');
308
- expect(Clazz.name).toEqual(AppItem.name);
309
- const instance = new AppItem({
310
- address: { street: '123' },
311
- perPage: '1',
312
112
  });
313
- instance.renderItem = () => html`<div><p>render item 1</p></div>`;
314
- expect(AppItem.observedAttributes).toEqual(['perpage', 'address']);
315
- const res = instance.renderTemplate();
113
+ const mock = jest.fn();
114
+ countState.subscribe(mock);
115
+ countState.increment(4);
116
+ expect(countState.getValue().count).toEqual(4);
316
- expect(res).toMatchSnapshot();
117
+ expect(mock).toBeCalledWith({ count: 4 });
118
+ countState.decrement(1);
119
+ expect(countState.getValue().count).toEqual(3);
120
+ expect(mock).toBeCalledWith({ count: 3 });
121
+ countState.decrement(2);
122
+ expect(countState.getValue().count).toEqual(1);
123
+ expect(mock).toBeCalledWith({ count: 1 });
317
124
  });
318
125
 
319
- test('createElement ', async () => {
126
+ test('createElement without attrs and state', async () => {
320
127
  createElement({
321
- name: () => 'base-element',
128
+ name: 'base-element',
322
129
  render: () => {
323
130
  return html` <div></div> `;
324
131
  },
325
132
  });
326
- const Clazz = AtomsElement.getElement('base-element');
133
+ const Clazz = getElement('base-element');
327
134
  const instance = new Clazz();
328
- const res = instance.renderTemplate();
135
+ const res = instance.render();
136
+ expect(res).toMatchSnapshot();
137
+ });
138
+
139
+ test('createElement with attrs and state', async () => {
140
+ createElement({
141
+ name: 'base-element',
142
+ attrs: {
143
+ perPage: '',
144
+ },
145
+ state: {
146
+ count: 3,
147
+ },
148
+ reducer: {
149
+ setValue: (state, v) => ({ ...state, count: v }),
150
+ },
151
+ render: ({ attrs, state, actions }) => {
152
+ const { perPage } = attrs;
153
+ const { count } = state;
154
+
155
+ return html`
156
+ <div>
157
+ <div>
158
+ <span>perPage: ${perPage}</span>
159
+ </div>
160
+ </div>
161
+ <span>Count: ${count}</span>
162
+ </div>
163
+ <button @click=${actions.setValue}>Set</button>
164
+ </div>
165
+ `;
166
+ },
167
+ });
168
+ const Clazz = getElement('base-element');
169
+ const instance = new Clazz({ perPage: 5 });
170
+ const res = instance.render();
171
+ expect(Clazz.observedAttributes).toEqual(['perpage']);
329
172
  expect(res).toMatchSnapshot();
330
173
  });
331
174
 
332
- test('Page', () => {
175
+ test('createPage', () => {
333
176
  const route = () => {
334
177
  const langPart = this.config.lang === 'en' ? '' : `/${this.config.lang}`;
335
178
  return `${langPart}`;