ripple 0.2.98 → 0.2.100

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.98",
6
+ "version": "0.2.100",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -461,14 +461,20 @@ function RipplePlugin(config) {
461
461
 
462
462
  if (this.isContextual('index')) {
463
463
  this.next(); // consume 'index'
464
-
465
- if (this.type === tt.name) {
466
- node.index = this.parseIdent();
467
- } else {
464
+ node.index = this.parseExpression();
465
+ if (node.index.type !== 'Identifier') {
468
466
  this.raise(this.start, 'Expected identifier after "index" keyword');
469
467
  }
470
- } else {
471
- this.raise(this.start, 'Expected "index" keyword after semicolon in for-of loop');
468
+ this.eat(tt.semi);
469
+ }
470
+
471
+ if (this.isContextual('key')) {
472
+ this.next(); // consume 'key'
473
+ node.key = this.parseExpression();
474
+ }
475
+
476
+ if (this.isContextual('index')) {
477
+ this.raise(this.start, '"index" must come before "key" in for-of loop');
472
478
  }
473
479
  } else if (!isForIn) {
474
480
  // Set index to null for standard for-of loops
@@ -858,7 +864,7 @@ function RipplePlugin(config) {
858
864
  const element = this.startNode();
859
865
  element.start = position.index;
860
866
  element.loc.start = position;
861
- element.metadata = {};
867
+ element.metadata = {};
862
868
  element.type = 'Element';
863
869
  this.#path.push(element);
864
870
  element.children = [];
@@ -466,6 +466,14 @@ const visitors = {
466
466
  if (attr.name.type === 'Identifier') {
467
467
  attribute_names.add(attr.name);
468
468
 
469
+ if (attr.name.name === 'key') {
470
+ error(
471
+ 'The `key` attribute is not a thing in Ripple, and cannot be used on DOM elements. If you are using a for loop, then use the `for (let item of items; key item.id)` syntax.',
472
+ state.analysis.module.filename,
473
+ attr,
474
+ );
475
+ }
476
+
469
477
  if (is_event_attribute(attr.name.name)) {
470
478
  const event_name = attr.name.name.slice(2).toLowerCase();
471
479
  const handler = visit(attr.value, state);
@@ -11,6 +11,7 @@ import {
11
11
  TEMPLATE_FRAGMENT,
12
12
  TEMPLATE_SVG_NAMESPACE,
13
13
  TEMPLATE_MATHML_NAMESPACE,
14
+ IS_KEYED,
14
15
  } from '../../../../constants.js';
15
16
  import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
16
17
  import {
@@ -1020,11 +1021,15 @@ const visitors = {
1020
1021
  }
1021
1022
  const is_controlled = node.is_controlled;
1022
1023
  const index = node.index;
1024
+ const key = node.key;
1023
1025
  let flags = is_controlled ? IS_CONTROLLED : 0;
1024
1026
 
1025
- if (index !== null) {
1027
+ if (index != null) {
1026
1028
  flags |= IS_INDEXED;
1027
1029
  }
1030
+ if (key != null) {
1031
+ flags |= IS_KEYED;
1032
+ }
1028
1033
 
1029
1034
  // do only if not controller
1030
1035
  if (!is_controlled) {
@@ -1034,6 +1039,7 @@ const visitors = {
1034
1039
  const id = context.state.flush_node(is_controlled);
1035
1040
  const pattern = node.left.declarations[0].id;
1036
1041
  const body_scope = context.state.scopes.get(node.body);
1042
+ const is_keyed = key && (flags & IS_KEYED) !== 0;
1037
1043
 
1038
1044
  context.state.init.push(
1039
1045
  b.stmt(
@@ -1051,6 +1057,7 @@ const visitors = {
1051
1057
  ),
1052
1058
  ),
1053
1059
  b.literal(flags),
1060
+ is_keyed ? b.arrow(index ? [pattern, index] : [pattern], context.visit(key)) : undefined,
1054
1061
  ),
1055
1062
  ),
1056
1063
  );
package/src/constants.js CHANGED
@@ -2,6 +2,7 @@ export const TEMPLATE_FRAGMENT = 1;
2
2
  export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
3
3
  export const IS_CONTROLLED = 1 << 2;
4
4
  export const IS_INDEXED = 1 << 3;
5
- export const TEMPLATE_SVG_NAMESPACE = 1 << 3;
6
- export const TEMPLATE_MATHML_NAMESPACE = 1 << 4;
5
+ export const IS_KEYED = 1 << 4;
6
+ export const TEMPLATE_SVG_NAMESPACE = 1 << 5;
7
+ export const TEMPLATE_MATHML_NAMESPACE = 1 << 6;
7
8
 
@@ -60,8 +60,6 @@ export { TrackedMap } from './map.js';
60
60
 
61
61
  export { TrackedDate } from './date.js';
62
62
 
63
- export { keyed } from './internal/client/for.js';
64
-
65
63
  export { user_effect as effect } from './internal/client/blocks.js';
66
64
 
67
65
  export { Portal } from './internal/client/portal.js';
@@ -1,6 +1,6 @@
1
- /** @import { Block } from '#client' */
1
+ /** @import { Block, Tracked } from '#client' */
2
2
 
3
- import { IS_CONTROLLED, IS_INDEXED } from '../../../constants.js';
3
+ import { IS_CONTROLLED, IS_INDEXED, IS_KEYED } from '../../../constants.js';
4
4
  import { branch, destroy_block, destroy_block_children, render } from './blocks.js';
5
5
  import { FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
6
6
  import { create_text, next_sibling } from './operations.js';
@@ -69,14 +69,15 @@ function move(block, anchor) {
69
69
  /**
70
70
  * @template V
71
71
  * @param {V[] | Iterable<V>} collection
72
+ * @param {boolean} clone
72
73
  * @returns {V[]}
73
74
  */
74
- function collection_to_array(collection) {
75
+ function collection_to_array(collection, clone) {
75
76
  var array = is_array(collection) ? collection : collection == null ? [] : array_from(collection);
76
77
 
77
78
  // If we are working with a tracked array, then we need to get a copy of
78
79
  // the elements, as the array itself is proxied, and not useful in diffing
79
- if (TRACKED_ARRAY in array) {
80
+ if (clone || TRACKED_ARRAY in array) {
80
81
  array = array_from(array);
81
82
  }
82
83
 
@@ -85,15 +86,18 @@ function collection_to_array(collection) {
85
86
 
86
87
  /**
87
88
  * @template V
89
+ * @template K
88
90
  * @param {Element} node
89
91
  * @param {() => V[] | Iterable<V>} get_collection
90
92
  * @param {(anchor: Node, value: V, index?: any) => Block} render_fn
91
93
  * @param {number} flags
94
+ * @param {(item: V) => K} [get_key]
92
95
  * @returns {void}
93
96
  */
94
- export function for_block(node, get_collection, render_fn, flags) {
97
+ export function for_block(node, get_collection, render_fn, flags, get_key) {
95
98
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
96
99
  var is_indexed = (flags & IS_INDEXED) !== 0;
100
+ var is_keyed = (flags & IS_KEYED) !== 0;
97
101
  var anchor = /** @type {Element | Text} */ (node);
98
102
 
99
103
  if (is_controlled) {
@@ -103,7 +107,10 @@ export function for_block(node, get_collection, render_fn, flags) {
103
107
  render(() => {
104
108
  var block = /** @type {Block} */ (active_block);
105
109
  var collection = get_collection();
106
- var array = collection_to_array(collection);
110
+ var array = collection_to_array(collection, is_keyed);
111
+ if (is_keyed) {
112
+ array = keyed(block, array, /** @type {(item: V) => K} */ (get_key));
113
+ }
107
114
 
108
115
  untrack(() => {
109
116
  reconcile(anchor, block, array, render_fn, is_controlled, is_indexed);
@@ -321,9 +328,9 @@ function reconcile(anchor, block, b, render_fn, is_controlled, is_indexed) {
321
328
  if (j !== undefined) {
322
329
  if (fast_path_removal) {
323
330
  fast_path_removal = false;
324
- // while (i > a_start) {
325
- // destroy_block(a[a_start++]);
326
- // }
331
+ while (i > a_start) {
332
+ destroy_block(a[a_start++]);
333
+ }
327
334
  }
328
335
  sources[j - b_start] = i + 1;
329
336
  if (pos > j) {
@@ -396,7 +403,7 @@ function reconcile(anchor, block, b, render_fn, is_controlled, is_indexed) {
396
403
  let result;
397
404
  /** @type {Int32Array} */
398
405
  let p;
399
- let maxLen = 0;
406
+ let max_len = 0;
400
407
  // https://en.wikipedia.org/wiki/Longest_increasing_subsequence
401
408
  /**
402
409
  * @param {Int32Array} arr
@@ -412,8 +419,8 @@ function lis_algorithm(arr) {
412
419
  let c = 0;
413
420
  var len = arr.length;
414
421
 
415
- if (len > maxLen) {
416
- maxLen = len;
422
+ if (len > max_len) {
423
+ max_len = len;
417
424
  result = new Int32Array(len);
418
425
  p = new Int32Array(len);
419
426
  }
@@ -466,17 +473,12 @@ function lis_algorithm(arr) {
466
473
  /**
467
474
  * @template V
468
475
  * @template K
469
- * @param {V[] | Iterable<V>} collection
476
+ * @param {Block} block
477
+ * @param {V[]} b_array
470
478
  * @param {(item: V) => K} key_fn
471
479
  * @returns {V[]}
472
480
  */
473
- export function keyed(collection, key_fn) {
474
- var block = active_block;
475
- if (block === null || (block.f & FOR_BLOCK) === 0) {
476
- throw new Error('keyed() must be used inside a for block');
477
- }
478
-
479
- var b_array = collection_to_array(collection);
481
+ function keyed(block, b_array, key_fn) {
480
482
  var b_keys = b_array.map(key_fn);
481
483
 
482
484
  // We only need to do this in DEV
@@ -488,6 +490,7 @@ export function keyed(collection, key_fn) {
488
490
  var state = block.s;
489
491
 
490
492
  if (state === null) {
493
+ // Make a clone of it so we don't mutate the original thereafter
491
494
  return b_array;
492
495
  }
493
496
 
@@ -495,7 +498,7 @@ export function keyed(collection, key_fn) {
495
498
  var a_keys = a_array.map(key_fn);
496
499
  var a = new Map();
497
500
 
498
- for (let i = 0; i < a_keys.length; i++) {
501
+ for (var i = 0; i < a_keys.length; i++) {
499
502
  a.set(a_keys[i], i);
500
503
  }
501
504
 
@@ -503,8 +506,12 @@ export function keyed(collection, key_fn) {
503
506
  throw new Error('Duplicate keys are not allowed');
504
507
  }
505
508
 
506
- for (let i = 0; i < b_keys.length; i++) {
509
+ for (var i = 0; i < b_keys.length; i++) {
507
510
  var b_val = b_keys[i];
511
+ // if the index is the key, skip
512
+ if (b_val === i) {
513
+ continue;
514
+ }
508
515
  var index = a.get(b_val);
509
516
 
510
517
  if (index !== undefined) {
@@ -1,49 +1,98 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`for statements > correctly handle the index in a for...of loop 1`] = `
3
+ exports[`for statements > correctly handles intermediate statements in for block 1`] = `
4
4
  <div>
5
5
  <div>
6
6
  <div>
7
- 0 : a
7
+ <div>
8
+ 1
9
+ </div>
10
+ <div>
11
+ 1
12
+ </div>
8
13
  </div>
9
14
  <div>
10
- 1 : b
15
+ <div>
16
+ 2
17
+ </div>
18
+ <div>
19
+ 2
20
+ </div>
11
21
  </div>
12
22
  <div>
13
- 2 : c
23
+ <div>
24
+ 3
25
+ </div>
26
+ <div>
27
+ 3
28
+ </div>
14
29
  </div>
15
30
 
16
31
  </div>
17
32
  <button>
18
33
  Add Item
19
34
  </button>
20
- <button>
21
- Reverse
22
- </button>
23
35
 
24
36
  </div>
25
37
  `;
26
38
 
27
- exports[`for statements > correctly handle the index in a for...of loop 2`] = `
39
+ exports[`for statements > correctly handles intermediate statements in for block 2`] = `
28
40
  <div>
29
41
  <div>
30
42
  <div>
31
- 0 : a
43
+ <div>
44
+ 1
45
+ </div>
46
+ <div>
47
+ 1
48
+ </div>
32
49
  </div>
33
50
  <div>
34
- 1 : b
51
+ <div>
52
+ 2
53
+ </div>
54
+ <div>
55
+ 2
56
+ </div>
35
57
  </div>
36
58
  <div>
37
- 2 : c
59
+ <div>
60
+ 3
61
+ </div>
62
+ <div>
63
+ 3
64
+ </div>
38
65
  </div>
39
66
  <div>
40
- 3 : d
67
+ <div>
68
+ 4
69
+ </div>
70
+ <div>
71
+ 4
72
+ </div>
41
73
  </div>
42
74
 
43
75
  </div>
44
76
  <button>
45
77
  Add Item
46
78
  </button>
79
+
80
+ </div>
81
+ `;
82
+
83
+ exports[`for statements > correctly handles keyed for...of loops 1`] = `
84
+ <div>
85
+ <!---->
86
+ <div>
87
+ 0:Item 1
88
+ </div>
89
+ <div>
90
+ 1:Item 2
91
+ </div>
92
+ <div>
93
+ 2:Item 3
94
+ </div>
95
+ <!---->
47
96
  <button>
48
97
  Reverse
49
98
  </button>
@@ -51,20 +100,37 @@ exports[`for statements > correctly handle the index in a for...of loop 2`] = `
51
100
  </div>
52
101
  `;
53
102
 
54
- exports[`for statements > correctly handle the index in a for...of loop 3`] = `
103
+ exports[`for statements > correctly handles keyed for...of loops 2`] = `
104
+ <div>
105
+ <!---->
106
+ <div>
107
+ 0:Item 3
108
+ </div>
109
+ <div>
110
+ 1:Item 2
111
+ </div>
112
+ <div>
113
+ 2:Item 1
114
+ </div>
115
+ <!---->
116
+ <button>
117
+ Reverse
118
+ </button>
119
+
120
+ </div>
121
+ `;
122
+
123
+ exports[`for statements > correctly handles the index in a for...of loop 1`] = `
55
124
  <div>
56
125
  <div>
57
126
  <div>
58
- 0 : d
59
- </div>
60
- <div>
61
- 1 : c
127
+ 0 : a
62
128
  </div>
63
129
  <div>
64
- 2 : b
130
+ 1 : b
65
131
  </div>
66
132
  <div>
67
- 3 : a
133
+ 2 : c
68
134
  </div>
69
135
 
70
136
  </div>
@@ -78,82 +144,56 @@ exports[`for statements > correctly handle the index in a for...of loop 3`] = `
78
144
  </div>
79
145
  `;
80
146
 
81
- exports[`for statements > correctly handles intermediate statements in for block 1`] = `
147
+ exports[`for statements > correctly handles the index in a for...of loop 2`] = `
82
148
  <div>
83
149
  <div>
84
150
  <div>
85
- <div>
86
- 1
87
- </div>
88
- <div>
89
- 1
90
- </div>
151
+ 0 : a
91
152
  </div>
92
153
  <div>
93
- <div>
94
- 2
95
- </div>
96
- <div>
97
- 2
98
- </div>
154
+ 1 : b
99
155
  </div>
100
156
  <div>
101
- <div>
102
- 3
103
- </div>
104
- <div>
105
- 3
106
- </div>
157
+ 2 : c
158
+ </div>
159
+ <div>
160
+ 3 : d
107
161
  </div>
108
162
 
109
163
  </div>
110
164
  <button>
111
165
  Add Item
112
166
  </button>
167
+ <button>
168
+ Reverse
169
+ </button>
113
170
 
114
171
  </div>
115
172
  `;
116
173
 
117
- exports[`for statements > correctly handles intermediate statements in for block 2`] = `
174
+ exports[`for statements > correctly handles the index in a for...of loop 3`] = `
118
175
  <div>
119
176
  <div>
120
177
  <div>
121
- <div>
122
- 1
123
- </div>
124
- <div>
125
- 1
126
- </div>
178
+ 0 : d
127
179
  </div>
128
180
  <div>
129
- <div>
130
- 2
131
- </div>
132
- <div>
133
- 2
134
- </div>
181
+ 1 : c
135
182
  </div>
136
183
  <div>
137
- <div>
138
- 3
139
- </div>
140
- <div>
141
- 3
142
- </div>
184
+ 2 : b
143
185
  </div>
144
186
  <div>
145
- <div>
146
- 4
147
- </div>
148
- <div>
149
- 4
150
- </div>
187
+ 3 : a
151
188
  </div>
152
189
 
153
190
  </div>
154
191
  <button>
155
192
  Add Item
156
193
  </button>
194
+ <button>
195
+ Reverse
196
+ </button>
157
197
 
158
198
  </div>
159
199
  `;
@@ -341,50 +341,6 @@ describe('composite components', () => {
341
341
  component App() {
342
342
  // Ambiguous generics vs JSX / less-than parsing scenarios
343
343
 
344
- // 1. Simple "new" with generic (should NOT become an <Element>)
345
- const a = new TrackedArray<number>();
346
-
347
- // 2. Multi-line generic with newline after '<'
348
- const b = new TrackedArray<
349
- string
350
- >();
351
-
352
- // // 3. Member expression + generic
353
- // class List<T> {
354
- // items: T[];
355
- // constructor() {
356
- // this.items = [];
357
- // }
358
- // }
359
- // class Containers {
360
- // List<T>() {
361
- // return new List<T>();
362
- // }
363
- // }
364
-
365
- // const c = new Containers.List<string>();
366
-
367
- // 4. Chained call with generic method
368
- const someSource = new Array<number>(1, 2, 3);
369
- const d = someSource.map<number>((x) => x * 2).filter<boolean>((x) => !!x);
370
-
371
- // 5. Nested generics
372
- const e = new Map<string, Promise<number>>();
373
-
374
- // // 6. Generic after a call expression result
375
- // function getBuilder<T>() {
376
- // return {
377
- // build<U>() {
378
- // return {
379
- // build<V>() {
380
- // return 42;
381
- // }
382
- // };
383
- // }
384
- // };
385
- // }
386
- // const f = getBuilder()<ResultType>().build<number>();
387
-
388
344
  // // 7. Generic following optional chaining
389
345
  // const maybe = {};
390
346
  // const g = maybe?.factory<number>()?.make<boolean>();
@@ -408,7 +364,7 @@ describe('composite components', () => {
408
364
 
409
365
  // 10. JSX / Element should still work
410
366
  <div class="still-works">
411
- <span>{a.length}</span>
367
+ <span>{'Test'}</span>
412
368
  </div>
413
369
 
414
370
  // // 11. Generic function call vs Element: Identifier followed by generic args
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, flushSync, TrackedArray } from 'ripple';
2
+ import { mount, flushSync, TrackedArray, track } from 'ripple';
3
3
 
4
4
  describe('for statements', () => {
5
5
  let container;
@@ -85,7 +85,7 @@ describe('for statements', () => {
85
85
  expect(container).toMatchSnapshot();
86
86
  });
87
87
 
88
- it('correctly handle the index in a for...of loop', () => {
88
+ it('correctly handles the index in a for...of loop', () => {
89
89
  component App() {
90
90
  const items = new TrackedArray('a', 'b', 'c');
91
91
 
@@ -115,4 +115,33 @@ describe('for statements', () => {
115
115
 
116
116
  expect(container).toMatchSnapshot();
117
117
  });
118
+
119
+ it('correctly handles keyed for...of loops', () => {
120
+ component App() {
121
+ let items = track([
122
+ { id: 1, text: 'Item 1' },
123
+ { id: 2, text: 'Item 2' },
124
+ { id: 3, text: 'Item 3' },
125
+ ]);
126
+
127
+ for (let item of @items; index i; key item.id) {
128
+ <div>{i + ':' + item.text}</div>
129
+ }
130
+
131
+ <button onClick={() => {
132
+ @items = @items.toReversed();
133
+ }}>{'Reverse'}</button>
134
+ }
135
+
136
+ render(App);
137
+
138
+ expect(container).toMatchSnapshot();
139
+
140
+ const button = container.querySelector('button');
141
+
142
+ button.click();
143
+ flushSync();
144
+
145
+ expect(container).toMatchSnapshot();
146
+ });
118
147
  });
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount } from 'ripple';
3
+
4
+ describe('generic patterns', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ document.body.removeChild(container);
20
+ container = null;
21
+ });
22
+
23
+ it('tests simple generic function', () => {
24
+ component App() {
25
+ const e = new Map<string, Promise<number>>();
26
+ const a = new Array<number>();
27
+
28
+ const b = new Array<
29
+ string
30
+ >();
31
+
32
+ <div class="still-works">
33
+ <span>{'Test'}</span>
34
+ </div>
35
+
36
+ const someSource = new Array<number>(1, 2, 3);
37
+ const d = someSource.map<number>((x) => x * 2).filter<boolean>((x) => !!x);
38
+ }
39
+
40
+ render(App);
41
+ });
42
+
43
+ it('tests member expression)', () => {
44
+ component App() {
45
+ class List<T> {
46
+ items: T[];
47
+ constructor() {
48
+ this.items = [];
49
+ }
50
+ }
51
+
52
+ <div class="still-works">
53
+ <span>{'Test'}</span>
54
+ </div>
55
+
56
+ class Containers {
57
+ static List<T>() {
58
+ return new List<T>();
59
+ }
60
+ }
61
+
62
+ const c = Containers.List<string>();
63
+ }
64
+
65
+ render(App);
66
+ });
67
+
68
+ /**
69
+ * Complex generics tests
70
+ * These currently break the parser
71
+ * We can't just use skip and then the tests are still parsed and compiled
72
+ */
73
+ /*
74
+ describe('complex generics', () => {
75
+ it('tests after a call expression result', () => {
76
+ component App() {
77
+ function getBuilder() {
78
+ return <T>() => ({
79
+ build<U>(): { build<V>(): V; data: T; key: U } {
80
+ return {
81
+ build<V>(): V {
82
+ return 42 as V;
83
+ },
84
+ data: undefined as T,
85
+ key: undefined as U
86
+ };
87
+ }
88
+ });
89
+ }
90
+
91
+ <div class="still-works">
92
+ <span>{'Test'}</span>
93
+ </div>
94
+
95
+ type ResultType = string;
96
+
97
+ const f = getBuilder()<ResultType>().build<number>();
98
+ }
99
+ });
100
+ });
101
+ */
102
+ });