ripple 0.3.74 → 0.3.76

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.
@@ -1,11 +1,11 @@
1
1
  import type { PropsWithExtras } from 'ripple';
2
- import { createRefKey, effect, flushSync, track } from 'ripple';
2
+ import { createRefKey, Dynamic, effect, flushSync, track } from 'ripple';
3
3
 
4
4
  describe('dynamic DOM elements', () => {
5
5
  it('renders static dynamic element', () => {
6
6
  function App() @{
7
7
  let tag = track('div');
8
- <@tag>{'Hello World'}</@tag>
8
+ <Dynamic is={tag}>{'Hello World'}</Dynamic>
9
9
  }
10
10
  render(App);
11
11
 
@@ -14,12 +14,10 @@ describe('dynamic DOM elements', () => {
14
14
  expect(element.textContent).toBe('Hello World');
15
15
  });
16
16
 
17
- // The ts errors below are due to limitations in our current tsx generation for dynamic elements.
18
- // They can be ignored for now. But we'll fix them via jsx() vs <jsx>
19
17
  it('renders static dynamic element from a plain object with a tracked property', () => {
20
18
  function App() @{
21
19
  let obj = { tag: track('div') };
22
- <obj.tag.value>{'Hello World'}</obj.tag.value>
20
+ <Dynamic is={obj.tag}>{'Hello World'}</Dynamic>
23
21
  }
24
22
  render(App);
25
23
 
@@ -32,7 +30,7 @@ describe('dynamic DOM elements', () => {
32
30
  function App() @{
33
31
  let obj = track({ tag: track('div') });
34
32
  let tag = obj.value.tag;
35
- <@tag>{'Hello World'}</@tag>
33
+ <Dynamic is={tag}>{'Hello World'}</Dynamic>
36
34
  }
37
35
  render(App);
38
36
 
@@ -47,7 +45,7 @@ describe('dynamic DOM elements', () => {
47
45
  function App() @{
48
46
  let obj = track({ tag: track('div') });
49
47
  let tag = obj.value['tag'];
50
- <@tag>{'Hello World'}</@tag>
48
+ <Dynamic is={tag}>{'Hello World'}</Dynamic>
51
49
  }
52
50
  render(App);
53
51
 
@@ -66,7 +64,7 @@ describe('dynamic DOM elements', () => {
66
64
  tag = 'span';
67
65
  }}
68
66
  >{'Change Tag'}</button>
69
- <@tag id="dynamic">{'Hello World'}</@tag>
67
+ <Dynamic is={tag} id="dynamic">{'Hello World'}</Dynamic>
70
68
  </>
71
69
  }
72
70
  render(App);
@@ -90,7 +88,7 @@ describe('dynamic DOM elements', () => {
90
88
  it('renders self-closing dynamic element', () => {
91
89
  function App() @{
92
90
  let tag = track('input');
93
- <@tag type="text" value="test" />
91
+ <Dynamic is={tag} type="text" value="test" />
94
92
  }
95
93
  render(App);
96
94
 
@@ -104,7 +102,12 @@ describe('dynamic DOM elements', () => {
104
102
  function App() @{
105
103
  let tag = track('div');
106
104
  let &[className] = track('test-class');
107
- <@tag class={className} id="test" data-testid="dynamic-element">{'Content'}</@tag>
105
+ <Dynamic
106
+ is={tag}
107
+ class={className}
108
+ id="test"
109
+ data-testid="dynamic-element"
110
+ >{'Content'}</Dynamic>
108
111
  }
109
112
  render(App);
110
113
 
@@ -119,9 +122,9 @@ describe('dynamic DOM elements', () => {
119
122
  function App() @{
120
123
  let outerTag = track('div');
121
124
  let innerTag = track('span');
122
- <@outerTag class="outer">
123
- <@innerTag class="inner">{'Nested content'}</@innerTag>
124
- </@outerTag>
125
+ <Dynamic is={outerTag} class="outer">
126
+ <Dynamic is={innerTag} class="inner">{'Nested content'}</Dynamic>
127
+ </Dynamic>
125
128
  }
126
129
  render(App);
127
130
 
@@ -138,7 +141,10 @@ describe('dynamic DOM elements', () => {
138
141
  function App() @{
139
142
  let tag = track('div');
140
143
  let &[active] = track(true);
141
- <@tag class={{ active: active, 'dynamic-element': true }}>{'Element with class object'}</@tag>
144
+ <Dynamic
145
+ is={tag}
146
+ class={{ active: active, 'dynamic-element': true }}
147
+ >{'Element with class object'}</Dynamic>
142
148
  }
143
149
  render(App);
144
150
 
@@ -151,13 +157,14 @@ describe('dynamic DOM elements', () => {
151
157
  it('handles dynamic element with style object', () => {
152
158
  function App() @{
153
159
  let tag = track('span');
154
- <@tag
160
+ <Dynamic
161
+ is={tag}
155
162
  style={{
156
163
  color: 'red',
157
164
  fontSize: '16px',
158
165
  fontWeight: 'bold',
159
166
  }}
160
- >{'Styled dynamic element'}</@tag>
167
+ >{'Styled dynamic element'}</Dynamic>
161
168
  }
162
169
  render(App);
163
170
 
@@ -176,7 +183,11 @@ describe('dynamic DOM elements', () => {
176
183
  'data-testid': 'spread-test',
177
184
  class: 'spread-class',
178
185
  };
179
- <@tag {...attrs} data-extra="additional">{'Element with spread attributes'}</@tag>
186
+ <Dynamic
187
+ is={tag}
188
+ {...attrs}
189
+ data-extra="additional"
190
+ >{'Element with spread attributes'}</Dynamic>
180
191
  }
181
192
  render(App);
182
193
 
@@ -193,12 +204,13 @@ describe('dynamic DOM elements', () => {
193
204
 
194
205
  function App() @{
195
206
  let tag = track('article');
196
- <@tag
207
+ <Dynamic
208
+ is={tag}
197
209
  ref={(node: HTMLElement) => {
198
210
  capturedElement = node;
199
211
  }}
200
212
  id="ref-test"
201
- >{'Element with ref'}</@tag>
213
+ >{'Element with ref'}</Dynamic>
202
214
  }
203
215
  render(App);
204
216
  flushSync();
@@ -222,7 +234,8 @@ describe('dynamic DOM elements', () => {
222
234
  refAttrElement = input ?? null;
223
235
  anonymousRefElement = state.anonymous ?? null;
224
236
  });
225
- <@tag
237
+ <Dynamic
238
+ is={tag}
226
239
  id="dynamic-ref-combo"
227
240
  type="text"
228
241
  ref={[
@@ -264,7 +277,8 @@ describe('dynamic DOM elements', () => {
264
277
  refAttrElement = input ?? null;
265
278
  anonymousRefElement = state.anonymous ?? null;
266
279
  });
267
- <@dynamic
280
+ <Dynamic
281
+ is={dynamic}
268
282
  ref={[
269
283
  input,
270
284
  state.anonymous,
@@ -287,6 +301,65 @@ describe('dynamic DOM elements', () => {
287
301
  expect(element!.hasAttribute('input_ref')).toBe(false);
288
302
  });
289
303
 
304
+ it('updates forwarded refs when a dynamic component changes', () => {
305
+ let refAttrElement: HTMLInputElement | null = null;
306
+ let anonymousRefElement: HTMLInputElement | null = null;
307
+ let namedRefElement: HTMLInputElement | null = null;
308
+
309
+ function TextInput(props: PropsWithExtras<{}>) @{
310
+ <input id="dynamic-component-text" type="text" {...props} />
311
+ }
312
+
313
+ function SearchInput(props: PropsWithExtras<{}>) @{
314
+ <input id="dynamic-component-search" type="search" {...props} />
315
+ }
316
+
317
+ function App() @{
318
+ let &[dynamic] = track(() => TextInput);
319
+ <>
320
+ <button
321
+ onClick={() => {
322
+ dynamic = dynamic === TextInput ? SearchInput : TextInput;
323
+ }}
324
+ >{'Change Component'}</button>
325
+ <Dynamic
326
+ is={dynamic}
327
+ ref={[
328
+ (node: HTMLInputElement | null) => {
329
+ refAttrElement = node;
330
+ },
331
+ (node: HTMLInputElement | null) => {
332
+ anonymousRefElement = node;
333
+ },
334
+ (node: HTMLInputElement | null) => {
335
+ namedRefElement = node;
336
+ },
337
+ ]}
338
+ />
339
+ </>
340
+ }
341
+
342
+ render(App);
343
+ flushSync();
344
+
345
+ const button = container.querySelector('button')!;
346
+ const textInput = container.querySelector('#dynamic-component-text');
347
+ expect(textInput).toBeInstanceOf(HTMLInputElement);
348
+ expect(refAttrElement).toBe(textInput);
349
+ expect(anonymousRefElement).toBe(textInput);
350
+ expect(namedRefElement).toBe(textInput);
351
+
352
+ button.click();
353
+ flushSync();
354
+
355
+ const searchInput = container.querySelector('#dynamic-component-search');
356
+ expect(searchInput).toBeInstanceOf(HTMLInputElement);
357
+ expect(container.querySelector('#dynamic-component-text')).toBeNull();
358
+ expect(refAttrElement).toBe(searchInput);
359
+ expect(anonymousRefElement).toBe(searchInput);
360
+ expect(namedRefElement).toBe(searchInput);
361
+ });
362
+
290
363
  it('handles dynamic element with createRefKey in spread', () => {
291
364
  function App() @{
292
365
  let tag = track('header');
@@ -300,7 +373,7 @@ describe('dynamic DOM elements', () => {
300
373
  class: 'ref-element',
301
374
  [createRefKey()]: elementRef,
302
375
  };
303
- <@tag {...dynamicProps}>{'Element with spread ref'}</@tag>
376
+ <Dynamic is={tag} {...dynamicProps}>{'Element with spread ref'}</Dynamic>
304
377
  }
305
378
  render(App);
306
379
  flushSync();
@@ -324,14 +397,15 @@ describe('dynamic DOM elements', () => {
324
397
  count++;
325
398
  }}
326
399
  >{'Increment'}</button>
327
- <@tag
400
+ <Dynamic
401
+ is={tag}
328
402
  id={count % 2 ? 'even' : 'odd'}
329
403
  class={count % 2 ? 'even-class' : 'odd-class'}
330
404
  data-count={count}
331
405
  >
332
406
  {'Count: '}
333
407
  {count}
334
- </@tag>
408
+ </Dynamic>
335
409
  </>
336
410
  }
337
411
 
@@ -371,7 +445,7 @@ describe('dynamic DOM elements', () => {
371
445
  function App() @{
372
446
  let tag = track('div');
373
447
  <>
374
- <@tag class="test-class">{'Dynamic element'}</@tag>
448
+ <Dynamic is={tag} class="test-class">{'Dynamic element'}</Dynamic>
375
449
  <style>
376
450
  .test-class {
377
451
  color: red;
@@ -397,7 +471,8 @@ describe('dynamic DOM elements', () => {
397
471
  let tag = track('button');
398
472
  let &[count] = track(0);
399
473
  <>
400
- <@tag
474
+ <Dynamic
475
+ is={tag}
401
476
  class={count % 2 ? 'even' : 'odd'}
402
477
  id={count % 2 ? 'even' : 'odd'}
403
478
  onClick={() => {
@@ -406,7 +481,7 @@ describe('dynamic DOM elements', () => {
406
481
  >
407
482
  {'Count: '}
408
483
  {count}
409
- </@tag>
484
+ </Dynamic>
410
485
  <style>
411
486
  .even {
412
487
  background-color: green;
@@ -470,7 +545,7 @@ describe('dynamic DOM elements', () => {
470
545
  }>) @{
471
546
  const tag = track('button');
472
547
  <>
473
- <@tag {...rest}>{rest.class}</@tag>
548
+ <Dynamic is={tag} {...rest}>{rest.class}</Dynamic>
474
549
  <style>
475
550
  .even {
476
551
  background-color: green;
@@ -528,9 +603,9 @@ describe('dynamic DOM elements', () => {
528
603
  function App() @{
529
604
  let tag = track('div');
530
605
  <>
531
- <@tag class="scoped">
606
+ <Dynamic is={tag} class="scoped">
532
607
  <p>{'Scoped dynamic element'}</p>
533
- </@tag>
608
+ </Dynamic>
534
609
  <style>
535
610
  .scoped {
536
611
  color: blue;
@@ -551,9 +626,9 @@ describe('dynamic DOM elements', () => {
551
626
  function App() @{
552
627
  let tag = track('div');
553
628
  <>
554
- <@tag class="scoped">
629
+ <Dynamic is={tag} class="scoped">
555
630
  <p>{'Scoped dynamic element'}</p>
556
- </@tag>
631
+ </Dynamic>
557
632
  <style>
558
633
  div {
559
634
  color: blue;
@@ -587,10 +662,10 @@ describe('dynamic DOM elements', () => {
587
662
  function App() @{
588
663
  let tag = track('div');
589
664
  <>
590
- <@tag class="scoped">
665
+ <Dynamic is={tag} class="scoped">
591
666
  <p>{'Scoped dynamic element'}</p>
592
667
  <Child />
593
- </@tag>
668
+ </Dynamic>
594
669
  <style>
595
670
  div {
596
671
  color: blue;
@@ -633,7 +708,7 @@ describe('dynamic DOM elements', () => {
633
708
  function App() @{
634
709
  let tag = track(() => Child);
635
710
  <>
636
- <@tag />
711
+ <Dynamic is={tag} />
637
712
  <style>
638
713
  .child {
639
714
  color: red;
@@ -659,7 +734,7 @@ describe('dynamic DOM elements', () => {
659
734
 
660
735
  function Button(props: any) @{
661
736
  const el = track('button');
662
- <@el {...props} />
737
+ <Dynamic is={el} {...props} />
663
738
  }
664
739
 
665
740
  function App() @{
@@ -699,7 +774,7 @@ describe('dynamic DOM elements', () => {
699
774
 
700
775
  function Button(props: any) @{
701
776
  const el = track('button');
702
- <@el {...props} />
777
+ <Dynamic is={el} {...props} />
703
778
  }
704
779
 
705
780
  function App() @{
@@ -744,7 +819,7 @@ describe('dynamic DOM elements', () => {
744
819
 
745
820
  function Button(props: any) @{
746
821
  const el = track('button');
747
- <@el {...props} />
822
+ <Dynamic is={el} {...props} />
748
823
  }
749
824
 
750
825
  function App() @{
@@ -172,6 +172,40 @@ describe('head elements', () => {
172
172
  expect(document.title).toBe('');
173
173
  });
174
174
 
175
+ it('renders external scripts with src attributes from a loop', () => {
176
+ const added: HTMLScriptElement[] = [];
177
+ const scripts = [{ src: '/a.js' }, { src: '/b.js' }];
178
+
179
+ function App() @{
180
+ <head>
181
+ @for (const script of scripts) {
182
+ <script src={script.src} />
183
+ }
184
+ </head>
185
+ }
186
+
187
+ try {
188
+ render(App);
189
+
190
+ const head_scripts = Array.from(
191
+ document.head.querySelectorAll('script[src]'),
192
+ ) as HTMLScriptElement[];
193
+
194
+ for (const node of head_scripts) {
195
+ if (node.getAttribute('src') === '/a.js' || node.getAttribute('src') === '/b.js') {
196
+ added.push(node);
197
+ }
198
+ }
199
+
200
+ const srcs = added.map((node) => node.getAttribute('src')).sort();
201
+ expect(srcs).toEqual(['/a.js', '/b.js']);
202
+ } finally {
203
+ for (const node of added) {
204
+ node.remove();
205
+ }
206
+ }
207
+ });
208
+
175
209
  it('renders title with conditional content', () => {
176
210
  function App() @{
177
211
  let &[showPrefix] = track(true);
@@ -1,4 +1,4 @@
1
- import { track } from 'ripple';
1
+ import { Dynamic, track } from 'ripple';
2
2
  import type { Component, PropsWithChildren, PropsWithExtras } from 'ripple';
3
3
 
4
4
  describe('SVG namespace handling', () => {
@@ -296,7 +296,7 @@ describe('SVG namespace handling', () => {
296
296
  function App() @{
297
297
  let &[dynTag] = track('polygon');
298
298
  <SVG>
299
- <@dynTag points="0,0 30,0 15,10" />
299
+ <Dynamic is={dynTag} points="0,0 30,0 15,10" />
300
300
  </SVG>
301
301
  }
302
302
 
@@ -327,7 +327,7 @@ describe('SVG namespace handling', () => {
327
327
  function App() @{
328
328
  let &[Component] = track(() => Polygon);
329
329
  <SVG>
330
- <@Component points="0,0 30,0 15,10" />
330
+ <Dynamic is={Component} points="0,0 30,0 15,10" />
331
331
  </SVG>
332
332
  }
333
333
 
@@ -343,24 +343,25 @@ describe('SVG namespace handling', () => {
343
343
  it('should render SVG as a dynamic top element with any dynamic children elements', () => {
344
344
  function SVG({ children }: PropsWithChildren<{}>) @{
345
345
  let &[tag] = track('svg');
346
- <@tag
346
+ <Dynamic
347
+ is={tag}
347
348
  width={100}
348
349
  height={50}
349
350
  fill="red"
350
351
  viewBox="0 0 30 10"
351
352
  preserveAspectRatio="none"
352
- >{children}</@tag>
353
+ >{children}</Dynamic>
353
354
  }
354
355
 
355
356
  function Polygon({ points }: PropsWithExtras<{ points: string }>) @{
356
357
  let &[dynTag] = track('polygon');
357
- <@dynTag {points} />
358
+ <Dynamic is={dynTag} {points} />
358
359
  }
359
360
 
360
361
  function App() @{
361
362
  let &[Component] = track(() => Polygon);
362
363
  <SVG>
363
- <@Component points="0,0 30,0 15,10" />
364
+ <Dynamic is={Component} points="0,0 30,0 15,10" />
364
365
  </SVG>
365
366
  }
366
367
 
@@ -1,4 +1,4 @@
1
- import { track } from 'ripple';
1
+ import { Dynamic, track } from 'ripple';
2
2
  import type {
3
3
  Tracked,
4
4
  PropsWithChildren,
@@ -331,7 +331,7 @@ describe('basic server > components & composition', () => {
331
331
 
332
332
  function App() @{
333
333
  let Content = track(() => Noop);
334
- <@Content />
334
+ <Dynamic is={Content} />
335
335
  }
336
336
 
337
337
  const { body } = await render(App);
@@ -1,4 +1,4 @@
1
- import { Fragment, track } from 'ripple';
1
+ import { Dynamic, Fragment, track } from 'ripple';
2
2
  import type { Tracked, PropsNoChildren } from 'ripple';
3
3
 
4
4
  describe('basic client', () => {
@@ -402,8 +402,8 @@ second
402
402
 
403
403
  function Parent() @{
404
404
  const count = track(10);
405
- let Dynamic = track(() => Child);
406
- <@Dynamic {count} class={{ test: true }} />
405
+ let DynamicChild = track(() => Child);
406
+ <Dynamic is={DynamicChild} {count} class={{ test: true }} />
407
407
  }
408
408
 
409
409
  const { body } = await render(Parent);