~repos /atoms-element
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
- __snapshots__/index.test.js.snap +301 -25
- example/app-counter.js +3 -3
- index.d.ts +4 -8
- index.js +20 -18
- 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[`
|
|
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
|
-
|
|
18
|
+
border-color: rgba(0, 0, 0, 1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.p-4 {
|
|
7
|
-
|
|
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
|
-
|
|
34
|
+
height: 5rem;
|
|
9
|
-
|
|
10
|
-
|
|
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[`
|
|
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,
|
|
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
|
|
54
|
+
export function createPage(props: { head: Handler, body: Handler}): (props: PageRenderProps) => string;
|
|
55
55
|
|
|
56
56
|
export type State<P, Q> = {
|
|
57
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
949
|
+
actions,
|
|
942
|
-
initial = reduce(initial, v);
|
|
943
|
-
subs.forEach((sub) => {
|
|
944
|
-
|
|
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 +=
|
|
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 =
|
|
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 =
|
|
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
|
-
${
|
|
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 {
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
99
|
+
const res = await renderHtml(template);
|
|
257
100
|
expect(res).toMatchSnapshot();
|
|
258
101
|
});
|
|
259
102
|
|
|
260
|
-
test('
|
|
103
|
+
test('createState', () => {
|
|
261
|
-
|
|
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
|
-
|
|
105
|
+
state: {
|
|
285
|
-
|
|
106
|
+
count: 0,
|
|
286
|
-
|
|
107
|
+
},
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
|
|
108
|
+
reducer: {
|
|
290
|
-
const {
|
|
291
|
-
perPage,
|
|
292
|
-
address: { street },
|
|
293
|
-
} = this.attrs;
|
|
294
|
-
|
|
109
|
+
increment: (state, a) => ({ ...state, count: state.count + a }),
|
|
295
|
-
const { sum } = this.computed;
|
|
296
|
-
return html`
|
|
297
|
-
<div perPage=${perPage}>
|
|
298
|
-
|
|
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
|
|
113
|
+
const mock = jest.fn();
|
|
114
|
+
countState.subscribe(mock);
|
|
115
|
+
countState.increment(4);
|
|
116
|
+
expect(countState.getValue().count).toEqual(4);
|
|
316
|
-
expect(
|
|
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:
|
|
128
|
+
name: 'base-element',
|
|
322
129
|
render: () => {
|
|
323
130
|
return html` <div></div> `;
|
|
324
131
|
},
|
|
325
132
|
});
|
|
326
|
-
const Clazz =
|
|
133
|
+
const Clazz = getElement('base-element');
|
|
327
134
|
const instance = new Clazz();
|
|
328
|
-
const res = instance.
|
|
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('
|
|
175
|
+
test('createPage', () => {
|
|
333
176
|
const route = () => {
|
|
334
177
|
const langPart = this.config.lang === 'en' ? '' : `/${this.config.lang}`;
|
|
335
178
|
return `${langPart}`;
|