ripple 0.2.186 → 0.2.188
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.188",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -86,6 +86,6 @@
|
|
|
86
86
|
"vscode-languageserver-types": "^3.17.5"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
|
-
"ripple": "0.2.
|
|
89
|
+
"ripple": "0.2.188"
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -422,25 +422,11 @@ const visitors = {
|
|
|
422
422
|
context.visit(node.discriminant, context.state);
|
|
423
423
|
|
|
424
424
|
for (const switch_case of node.cases) {
|
|
425
|
-
//
|
|
425
|
+
// Skip empty cases
|
|
426
426
|
if (switch_case.consequent.length === 0) {
|
|
427
427
|
continue;
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
-
// Validate that each cases ends in a break statement, except for the last case
|
|
431
|
-
const last = switch_case.consequent?.[switch_case.consequent.length - 1];
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
last.type !== 'BreakStatement' &&
|
|
435
|
-
node.cases.indexOf(switch_case) !== node.cases.length - 1
|
|
436
|
-
) {
|
|
437
|
-
error(
|
|
438
|
-
'Template switch cases must end with a break statement (with the exception of the last case).',
|
|
439
|
-
context.state.analysis.module.filename,
|
|
440
|
-
switch_case,
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
430
|
node.metadata = {
|
|
445
431
|
...node.metadata,
|
|
446
432
|
has_template: false,
|
|
@@ -1830,31 +1830,41 @@ const visitors = {
|
|
|
1830
1830
|
const statements = [];
|
|
1831
1831
|
const cases = [];
|
|
1832
1832
|
|
|
1833
|
-
let
|
|
1834
|
-
|
|
1833
|
+
let id_gen = 0;
|
|
1834
|
+
let counter = 0;
|
|
1835
1835
|
for (const switch_case of node.cases) {
|
|
1836
1836
|
const case_body = [];
|
|
1837
|
+
const consequent = switch_case.consequent;
|
|
1838
|
+
|
|
1839
|
+
if (consequent.length !== 0) {
|
|
1840
|
+
const consequent_scope = context.state.scopes.get(consequent) || context.state.scope;
|
|
1837
1841
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1842
|
+
const block = transform_body(consequent, {
|
|
1843
|
+
...context,
|
|
1844
|
+
state: { ...context.state, scope: consequent_scope },
|
|
1845
|
+
});
|
|
1846
|
+
const has_break = consequent.some((stmt) => stmt.type === 'BreakStatement');
|
|
1847
|
+
const is_last = counter === node.cases.length - 1;
|
|
1848
|
+
const is_default = switch_case.test == null;
|
|
1841
1849
|
const consequent_id = context.state.scope.generate(
|
|
1842
|
-
'switch_case_' + (
|
|
1850
|
+
'switch_case_' + (is_default ? 'default' : id_gen),
|
|
1843
1851
|
);
|
|
1844
|
-
const consequent = b.block(
|
|
1845
|
-
transform_body(switch_case.consequent, {
|
|
1846
|
-
...context,
|
|
1847
|
-
state: { ...context.state, scope: consequent_scope },
|
|
1848
|
-
}),
|
|
1849
|
-
);
|
|
1850
|
-
|
|
1851
|
-
statements.push(b.var(b.id(consequent_id), b.arrow([b.id('__anchor')], consequent)));
|
|
1852
1852
|
|
|
1853
|
-
|
|
1853
|
+
statements.push(b.var(b.id(consequent_id), b.arrow([b.id('__anchor')], b.block(block))));
|
|
1854
|
+
case_body.push(
|
|
1855
|
+
b.stmt(b.call(b.member(b.id('result'), b.id('push'), false), b.id(consequent_id))),
|
|
1856
|
+
);
|
|
1854
1857
|
|
|
1855
|
-
|
|
1858
|
+
// in js, `default:` can be in the middle without a break
|
|
1859
|
+
// so we only add return for the last case or cases with a break
|
|
1860
|
+
if (has_break || is_last) {
|
|
1861
|
+
case_body.push(b.return(b.id('result')));
|
|
1862
|
+
}
|
|
1863
|
+
id_gen++;
|
|
1856
1864
|
}
|
|
1857
1865
|
|
|
1866
|
+
counter++;
|
|
1867
|
+
|
|
1858
1868
|
cases.push(
|
|
1859
1869
|
b.switch_case(
|
|
1860
1870
|
switch_case.test ? /** @type {AST.Expression} */ (context.visit(switch_case.test)) : null,
|
|
@@ -1870,6 +1880,7 @@ const visitors = {
|
|
|
1870
1880
|
id,
|
|
1871
1881
|
b.thunk(
|
|
1872
1882
|
b.block([
|
|
1883
|
+
b.var(b.id('result'), b.array([])),
|
|
1873
1884
|
b.switch(/** @type {AST.Expression} */ (context.visit(node.discriminant)), cases),
|
|
1874
1885
|
]),
|
|
1875
1886
|
),
|
|
@@ -2,32 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
import { branch, destroy_block, render } from './blocks.js';
|
|
4
4
|
import { SWITCH_BLOCK } from './constants.js';
|
|
5
|
+
import { next_sibling } from './operations.js';
|
|
6
|
+
import { append } from './template.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @param {
|
|
9
|
+
* Moves a block's DOM nodes to before an anchor node
|
|
10
|
+
* @param {Block} block
|
|
11
|
+
* @param {ChildNode} anchor
|
|
9
12
|
* @returns {void}
|
|
10
13
|
*/
|
|
11
|
-
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
function move(block, anchor) {
|
|
15
|
+
var node = block.s.start;
|
|
16
|
+
var end = block.s.end;
|
|
17
|
+
/** @type {Node | null} */
|
|
18
|
+
var sibling;
|
|
19
|
+
|
|
20
|
+
while (node !== null) {
|
|
21
|
+
if (node === end) {
|
|
22
|
+
append(anchor, node);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
sibling = next_sibling(node);
|
|
26
|
+
append(anchor, node);
|
|
27
|
+
node = sibling;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {ChildNode} anchor
|
|
33
|
+
* @param {() => ((anchor: ChildNode) => void)[] | null} fn
|
|
34
|
+
* @returns {void}
|
|
35
|
+
*/
|
|
36
|
+
export function switch_block(anchor, fn) {
|
|
37
|
+
/** @type {((anchor: ChildNode) => void)[]} */
|
|
38
|
+
var prev = [];
|
|
39
|
+
/** @type {Map<(anchor: ChildNode) => void, Block>} */
|
|
40
|
+
var blocks = new Map();
|
|
17
41
|
|
|
18
42
|
render(
|
|
19
43
|
() => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
current_branch = branch_fn;
|
|
44
|
+
var funcs = fn();
|
|
45
|
+
let same = prev.length === funcs?.length || false;
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
for (var i = 0; i < prev.length; i++) {
|
|
48
|
+
var p = prev[i];
|
|
49
|
+
|
|
50
|
+
if (!funcs || funcs.indexOf(p) === -1) {
|
|
51
|
+
same = false;
|
|
52
|
+
destroy_block(/** @type {Block} */ (blocks.get(p)));
|
|
53
|
+
blocks.delete(p);
|
|
54
|
+
}
|
|
27
55
|
}
|
|
28
56
|
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
prev = funcs ?? [];
|
|
58
|
+
|
|
59
|
+
if (same || !funcs) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (var i = 0; i < funcs.length; i++) {
|
|
64
|
+
var n = funcs[i];
|
|
65
|
+
var b = blocks.get(n);
|
|
66
|
+
if (b) {
|
|
67
|
+
move(b, anchor);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
blocks.set(
|
|
72
|
+
n,
|
|
73
|
+
branch(() => n(anchor)),
|
|
74
|
+
);
|
|
31
75
|
}
|
|
32
76
|
},
|
|
33
77
|
null,
|
|
@@ -89,7 +89,7 @@ describe('switch statements', () => {
|
|
|
89
89
|
expect(container.querySelector('div').textContent).toBe('Default for y');
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
it('renders switch using
|
|
92
|
+
it('renders switch using empty case fall-through', () => {
|
|
93
93
|
component App() {
|
|
94
94
|
let value = track('a');
|
|
95
95
|
|
|
@@ -131,7 +131,7 @@ describe('switch statements', () => {
|
|
|
131
131
|
expect(container.querySelector('div').textContent).toBe('DOM check');
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
it('renders switch with template content and
|
|
134
|
+
it('renders switch with template content and reacts to tracked changes', () => {
|
|
135
135
|
component App() {
|
|
136
136
|
let status = track('active');
|
|
137
137
|
let message = track('');
|
|
@@ -180,4 +180,140 @@ describe('switch statements', () => {
|
|
|
180
180
|
expect(container.querySelector('div').textContent).toBe('Status: An error occurred.');
|
|
181
181
|
expect(container.querySelector('.error')).toBeTruthy();
|
|
182
182
|
});
|
|
183
|
+
|
|
184
|
+
it(
|
|
185
|
+
'renders switch with multiple non-empty fall-through cases and reacts to tracked changes without recreating DOM unnecessarily',
|
|
186
|
+
() => {
|
|
187
|
+
component App() {
|
|
188
|
+
let status = track(0);
|
|
189
|
+
<div>
|
|
190
|
+
switch (@status) {
|
|
191
|
+
case -1:
|
|
192
|
+
case 0:
|
|
193
|
+
<p>{' Idle'}</p>
|
|
194
|
+
case 1:
|
|
195
|
+
<p>{' Loading'}</p>
|
|
196
|
+
case 2:
|
|
197
|
+
<p>{' Success'}</p>
|
|
198
|
+
break;
|
|
199
|
+
default:
|
|
200
|
+
<p>{' Unknown status'}</p>
|
|
201
|
+
<p>{' Unknown 2'}</p>
|
|
202
|
+
case 3:
|
|
203
|
+
<p>{' Error'}</p>
|
|
204
|
+
<p>{' Error 2'}</p>
|
|
205
|
+
<p>{' Error 3'}</p>
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
</div>
|
|
209
|
+
<button
|
|
210
|
+
onClick={() => {
|
|
211
|
+
@status = (@status + 1) % 5;
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
{'Next Status'}
|
|
215
|
+
</button>
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
render(App);
|
|
219
|
+
const button = container.querySelector('button');
|
|
220
|
+
|
|
221
|
+
expect(container.querySelector('div').textContent).toBe(' Idle Loading Success');
|
|
222
|
+
|
|
223
|
+
button.click();
|
|
224
|
+
flushSync();
|
|
225
|
+
|
|
226
|
+
expect(container.querySelector('div').textContent).toBe(' Loading Success');
|
|
227
|
+
|
|
228
|
+
button.click();
|
|
229
|
+
flushSync();
|
|
230
|
+
|
|
231
|
+
expect(container.querySelector('div').textContent).toBe(' Success');
|
|
232
|
+
|
|
233
|
+
button.click();
|
|
234
|
+
flushSync();
|
|
235
|
+
|
|
236
|
+
expect(container.querySelector('div').textContent).toBe(' Error Error 2 Error 3');
|
|
237
|
+
|
|
238
|
+
button.click();
|
|
239
|
+
flushSync();
|
|
240
|
+
|
|
241
|
+
expect(container.querySelector('div').textContent).toBe(
|
|
242
|
+
' Unknown status Unknown 2 Error Error 2 Error 3',
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
button.click();
|
|
246
|
+
flushSync();
|
|
247
|
+
|
|
248
|
+
expect(container.querySelector('div').textContent).toBe(' Idle Loading Success');
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
it(
|
|
253
|
+
'renders a fall-through default in the middle of switch cases and reacts to changes without recreating DOM unnecessarily',
|
|
254
|
+
() => {
|
|
255
|
+
component App() {
|
|
256
|
+
let value = track('x');
|
|
257
|
+
|
|
258
|
+
<button onClick={() => (@value = 'a')}>{'Set A'}</button>
|
|
259
|
+
<button onClick={() => (@value = 'b')}>{'Set B'}</button>
|
|
260
|
+
<button onClick={() => (@value = 'c')}>{'Set C'}</button>
|
|
261
|
+
<button onClick={() => (@value = 'x')}>{'Set X'}</button>
|
|
262
|
+
|
|
263
|
+
<div>
|
|
264
|
+
switch (@value) {
|
|
265
|
+
case 'a':
|
|
266
|
+
<div>{' Case A'}</div>
|
|
267
|
+
break;
|
|
268
|
+
// NOTE: This should be the default in the middle of the cases
|
|
269
|
+
// However, jsdom (and other node-based dom libs) has a bug
|
|
270
|
+
// that breaks out of the switch even if the default doesn't have a break
|
|
271
|
+
// The browser works correctly.
|
|
272
|
+
// So, we're just using a defined case in the middle to simulate default.
|
|
273
|
+
case 'x':
|
|
274
|
+
<div>{' Default Case for ' + @value}</div>
|
|
275
|
+
case 'b':
|
|
276
|
+
<div>{' Case B'}</div>
|
|
277
|
+
break;
|
|
278
|
+
case 'c':
|
|
279
|
+
<div>{' Case C'}</div>
|
|
280
|
+
}
|
|
281
|
+
</div>
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
render(App);
|
|
285
|
+
const [buttonA, buttonB, buttonC, buttonX] = container.querySelectorAll('button');
|
|
286
|
+
|
|
287
|
+
expect(container.querySelector('div').textContent).toBe(' Default Case for x Case B');
|
|
288
|
+
expect(container.querySelector('div').querySelectorAll('div').length).toBe(2);
|
|
289
|
+
|
|
290
|
+
buttonA.click();
|
|
291
|
+
flushSync();
|
|
292
|
+
|
|
293
|
+
expect(container.querySelector('div').textContent).toBe(' Case A');
|
|
294
|
+
expect(container.querySelector('div').querySelectorAll('div').length).toBe(1);
|
|
295
|
+
|
|
296
|
+
buttonC.click();
|
|
297
|
+
flushSync();
|
|
298
|
+
|
|
299
|
+
expect(container.querySelector('div').textContent).toBe(' Case C');
|
|
300
|
+
expect(container.querySelector('div').querySelectorAll('div').length).toBe(1);
|
|
301
|
+
|
|
302
|
+
buttonB.click();
|
|
303
|
+
flushSync();
|
|
304
|
+
|
|
305
|
+
const bDiv = container.querySelector('div').querySelectorAll('div')[0];
|
|
306
|
+
expect(bDiv.textContent).toBe(' Case B');
|
|
307
|
+
bDiv.id = 'b';
|
|
308
|
+
|
|
309
|
+
buttonX.click();
|
|
310
|
+
flushSync();
|
|
311
|
+
|
|
312
|
+
// order should be correct with the previously rendered B element
|
|
313
|
+
expect(container.querySelector('div').textContent).toBe(' Default Case for x Case B');
|
|
314
|
+
expect(container.querySelector('div').querySelectorAll('div').length).toBe(2);
|
|
315
|
+
// the previously rendered div should be preserved
|
|
316
|
+
expect(container.querySelector('div').querySelectorAll('div')[1].id).toBe('b');
|
|
317
|
+
},
|
|
318
|
+
);
|
|
183
319
|
});
|
|
@@ -25,7 +25,7 @@ describe('SSR: switch statements', () => {
|
|
|
25
25
|
expect(body).toBe('<div>Case B</div>');
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
it('renders
|
|
28
|
+
it('renders a fall-through with an empty switch case', async () => {
|
|
29
29
|
component App() {
|
|
30
30
|
let value = 'b';
|
|
31
31
|
|
|
@@ -45,4 +45,47 @@ describe('SSR: switch statements', () => {
|
|
|
45
45
|
const { body } = await render(App);
|
|
46
46
|
expect(body).toBe('<div>Case B or C</div>');
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it('renders a fall-through with a case that has elements', async () => {
|
|
50
|
+
component App() {
|
|
51
|
+
let value = 'a';
|
|
52
|
+
|
|
53
|
+
switch (value) {
|
|
54
|
+
case 'a':
|
|
55
|
+
<div>{'Case A'}</div>
|
|
56
|
+
case 'b':
|
|
57
|
+
<div>{'Case B'}</div>
|
|
58
|
+
case 'c':
|
|
59
|
+
<div>{'Case C'}</div>
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
<div>{'Default Case'}</div>
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { body } = await render(App);
|
|
67
|
+
expect(body).toBe('<div>Case A</div><div>Case B</div><div>Case C</div>');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders a fall-through with a default case in the middle', async () => {
|
|
71
|
+
component App() {
|
|
72
|
+
let value = 'x';
|
|
73
|
+
|
|
74
|
+
switch (value) {
|
|
75
|
+
case 'a':
|
|
76
|
+
<div>{'Case A'}</div>
|
|
77
|
+
default:
|
|
78
|
+
<div>{'Default Case'}</div>
|
|
79
|
+
case 'b':
|
|
80
|
+
<div>{'Case B'}</div>
|
|
81
|
+
break;
|
|
82
|
+
case 'c':
|
|
83
|
+
<div>{'Case C'}</div>
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { body } = await render(App);
|
|
89
|
+
expect(body).toBe('<div>Default Case</div><div>Case B</div>');
|
|
90
|
+
});
|
|
48
91
|
});
|