ripple 0.3.72 → 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.
Files changed (172) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/package.json +3 -3
  3. package/src/jsx-runtime.d.ts +4 -10
  4. package/src/runtime/dynamic-client.js +33 -0
  5. package/src/runtime/dynamic-server.js +80 -0
  6. package/src/runtime/index-client.js +5 -13
  7. package/src/runtime/index-server.js +2 -0
  8. package/src/runtime/internal/client/blocks.js +6 -27
  9. package/src/runtime/internal/client/composite.js +11 -6
  10. package/src/runtime/internal/client/for.js +80 -5
  11. package/src/runtime/internal/client/index.js +0 -2
  12. package/src/runtime/internal/client/render.js +5 -2
  13. package/src/runtime/internal/client/types.d.ts +0 -10
  14. package/src/runtime/internal/server/index.js +8 -1
  15. package/tests/client/__snapshots__/computed-properties.test.tsrx.snap +8 -0
  16. package/tests/client/__snapshots__/for.test.tsrx.snap +22 -0
  17. package/tests/client/__snapshots__/html.test.tsrx.snap +4 -0
  18. package/tests/client/array/array.copy-within.test.tsrx +19 -19
  19. package/tests/client/array/array.derived.test.tsrx +97 -109
  20. package/tests/client/array/array.iteration.test.tsrx +28 -28
  21. package/tests/client/array/array.mutations.test.tsrx +68 -68
  22. package/tests/client/array/array.static.test.tsrx +82 -92
  23. package/tests/client/array/array.to-methods.test.tsrx +15 -15
  24. package/tests/client/async-suspend.test.tsrx +180 -179
  25. package/tests/client/basic/__snapshots__/basic.attributes.test.tsrx.snap +2 -0
  26. package/tests/client/basic/__snapshots__/basic.rendering.test.tsrx.snap +4 -0
  27. package/tests/client/basic/basic.attributes.test.tsrx +273 -317
  28. package/tests/client/basic/basic.collections.test.tsrx +55 -61
  29. package/tests/client/basic/basic.components.test.tsrx +198 -220
  30. package/tests/client/basic/basic.errors.test.tsrx +70 -76
  31. package/tests/client/basic/basic.events.test.tsrx +80 -85
  32. package/tests/client/basic/basic.get-set.test.tsrx +54 -64
  33. package/tests/client/basic/basic.hmr.test.tsrx +15 -19
  34. package/tests/client/basic/basic.reactivity.test.tsrx +121 -135
  35. package/tests/client/basic/basic.rendering.test.tsrx +273 -178
  36. package/tests/client/basic/basic.styling.test.tsrx +16 -14
  37. package/tests/client/basic/basic.utilities.test.tsrx +8 -10
  38. package/tests/client/boundaries.test.tsrx +18 -18
  39. package/tests/client/compiler/compiler.assignments.test.tsrx +77 -76
  40. package/tests/client/compiler/compiler.attributes.test.tsrx +18 -14
  41. package/tests/client/compiler/compiler.basic.test.tsrx +357 -288
  42. package/tests/client/compiler/compiler.regex.test.tsrx +40 -44
  43. package/tests/client/compiler/compiler.tracked-access.test.tsrx +57 -38
  44. package/tests/client/compiler/compiler.try-in-function.test.tsrx +16 -16
  45. package/tests/client/compiler/compiler.typescript.test.tsrx +4 -3
  46. package/tests/client/composite/composite.dynamic-components.test.tsrx +62 -47
  47. package/tests/client/composite/composite.generics.test.tsrx +165 -167
  48. package/tests/client/composite/composite.props.test.tsrx +66 -74
  49. package/tests/client/composite/composite.reactivity.test.tsrx +132 -166
  50. package/tests/client/composite/composite.render.test.tsrx +92 -101
  51. package/tests/client/computed-properties.test.tsrx +14 -18
  52. package/tests/client/context.test.tsrx +14 -18
  53. package/tests/client/css/global-additional-cases.test.tsrx +493 -439
  54. package/tests/client/css/global-advanced-selectors.test.tsrx +169 -153
  55. package/tests/client/css/global-at-rules.test.tsrx +71 -66
  56. package/tests/client/css/global-basic.test.tsrx +105 -98
  57. package/tests/client/css/global-classes-ids.test.tsrx +128 -114
  58. package/tests/client/css/global-combinators.test.tsrx +83 -78
  59. package/tests/client/css/global-complex-nesting.test.tsrx +134 -120
  60. package/tests/client/css/global-edge-cases.test.tsrx +138 -120
  61. package/tests/client/css/global-keyframes.test.tsrx +108 -96
  62. package/tests/client/css/global-nested.test.tsrx +88 -78
  63. package/tests/client/css/global-pseudo.test.tsrx +104 -98
  64. package/tests/client/css/global-scoping.test.tsrx +145 -125
  65. package/tests/client/css/style-identifier.test.tsrx +65 -72
  66. package/tests/client/date.test.tsrx +83 -83
  67. package/tests/client/dynamic-elements.test.tsrx +318 -299
  68. package/tests/client/events.test.tsrx +252 -266
  69. package/tests/client/for.test.tsrx +120 -127
  70. package/tests/client/head.test.tsrx +74 -48
  71. package/tests/client/html.test.tsrx +37 -49
  72. package/tests/client/input-value.test.tsrx +1125 -1354
  73. package/tests/client/lazy-array.test.tsrx +10 -16
  74. package/tests/client/lazy-destructuring.test.tsrx +169 -221
  75. package/tests/client/map.test.tsrx +39 -41
  76. package/tests/client/media-query.test.tsrx +15 -19
  77. package/tests/client/object.test.tsrx +46 -56
  78. package/tests/client/portal.test.tsrx +31 -37
  79. package/tests/client/ref.test.tsrx +173 -193
  80. package/tests/client/return.test.tsrx +62 -37
  81. package/tests/client/set.test.tsrx +33 -33
  82. package/tests/client/svg.test.tsrx +197 -216
  83. package/tests/client/switch.test.tsrx +201 -191
  84. package/tests/client/track-async-hydration.test.tsrx +14 -18
  85. package/tests/client/tracked-index-access.test.tsrx +18 -28
  86. package/tests/client/try.test.tsrx +494 -619
  87. package/tests/client/tsx.test.tsrx +286 -292
  88. package/tests/client/typescript-generics.test.tsrx +121 -129
  89. package/tests/client/url/url.derived.test.tsrx +21 -25
  90. package/tests/client/url/url.parsing.test.tsrx +35 -35
  91. package/tests/client/url/url.partial-removal.test.tsrx +32 -32
  92. package/tests/client/url/url.reactivity.test.tsrx +68 -72
  93. package/tests/client/url/url.serialization.test.tsrx +8 -8
  94. package/tests/client/url-search-params/url-search-params.derived.test.tsrx +21 -27
  95. package/tests/client/url-search-params/url-search-params.initialization.test.tsrx +16 -16
  96. package/tests/client/url-search-params/url-search-params.iteration.test.tsrx +37 -37
  97. package/tests/client/url-search-params/url-search-params.mutation.test.tsrx +56 -60
  98. package/tests/client/url-search-params/url-search-params.retrieval.test.tsrx +32 -34
  99. package/tests/client/url-search-params/url-search-params.serialization.test.tsrx +9 -9
  100. package/tests/client/url-search-params/url-search-params.tracked-url.test.tsrx +10 -10
  101. package/tests/hydration/compiled/client/basic.js +390 -319
  102. package/tests/hydration/compiled/client/composite.js +52 -44
  103. package/tests/hydration/compiled/client/for.js +734 -604
  104. package/tests/hydration/compiled/client/head.js +183 -103
  105. package/tests/hydration/compiled/client/html.js +93 -86
  106. package/tests/hydration/compiled/client/if-children.js +95 -71
  107. package/tests/hydration/compiled/client/if.js +113 -89
  108. package/tests/hydration/compiled/client/mixed-control-flow.js +225 -209
  109. package/tests/hydration/compiled/client/nested-control-flow.js +94 -98
  110. package/tests/hydration/compiled/client/reactivity.js +26 -24
  111. package/tests/hydration/compiled/client/return.js +8 -42
  112. package/tests/hydration/compiled/client/switch.js +208 -173
  113. package/tests/hydration/compiled/client/track-async-serialization.js +176 -128
  114. package/tests/hydration/compiled/client/try.js +29 -21
  115. package/tests/hydration/compiled/server/basic.js +210 -221
  116. package/tests/hydration/compiled/server/composite.js +13 -14
  117. package/tests/hydration/compiled/server/for.js +427 -444
  118. package/tests/hydration/compiled/server/head.js +199 -189
  119. package/tests/hydration/compiled/server/html.js +33 -41
  120. package/tests/hydration/compiled/server/if-children.js +114 -117
  121. package/tests/hydration/compiled/server/if.js +77 -83
  122. package/tests/hydration/compiled/server/mixed-control-flow.js +145 -150
  123. package/tests/hydration/compiled/server/nested-control-flow.js +10 -0
  124. package/tests/hydration/compiled/server/reactivity.js +24 -22
  125. package/tests/hydration/compiled/server/return.js +6 -18
  126. package/tests/hydration/compiled/server/switch.js +179 -176
  127. package/tests/hydration/compiled/server/track-async-serialization.js +88 -70
  128. package/tests/hydration/compiled/server/try.js +31 -35
  129. package/tests/hydration/components/basic.tsrx +216 -258
  130. package/tests/hydration/components/composite.tsrx +32 -42
  131. package/tests/hydration/components/events.tsrx +81 -101
  132. package/tests/hydration/components/for.tsrx +270 -336
  133. package/tests/hydration/components/head.tsrx +43 -39
  134. package/tests/hydration/components/hmr.tsrx +16 -22
  135. package/tests/hydration/components/html-in-template.tsrx +15 -21
  136. package/tests/hydration/components/html.tsrx +442 -526
  137. package/tests/hydration/components/if-children.tsrx +107 -125
  138. package/tests/hydration/components/if.tsrx +68 -90
  139. package/tests/hydration/components/mixed-control-flow.tsrx +65 -72
  140. package/tests/hydration/components/nested-control-flow.tsrx +202 -216
  141. package/tests/hydration/components/portal.tsrx +33 -41
  142. package/tests/hydration/components/reactivity.tsrx +26 -34
  143. package/tests/hydration/components/return.tsrx +4 -6
  144. package/tests/hydration/components/switch.tsrx +73 -78
  145. package/tests/hydration/components/track-async-serialization.tsrx +83 -93
  146. package/tests/hydration/components/try.tsrx +37 -51
  147. package/tests/hydration/switch.test.js +8 -8
  148. package/tests/server/await.test.tsrx +3 -3
  149. package/tests/server/basic.attributes.test.tsrx +117 -162
  150. package/tests/server/basic.components.test.tsrx +164 -194
  151. package/tests/server/basic.test.tsrx +299 -199
  152. package/tests/server/compiler.test.tsrx +142 -72
  153. package/tests/server/composite.props.test.tsrx +54 -58
  154. package/tests/server/composite.test.tsrx +165 -167
  155. package/tests/server/context.test.tsrx +13 -17
  156. package/tests/server/dynamic-elements.test.tsrx +147 -148
  157. package/tests/server/for.test.tsrx +115 -84
  158. package/tests/server/head.test.tsrx +54 -31
  159. package/tests/server/html-nesting-validation.test.tsrx +16 -8
  160. package/tests/server/if.test.tsrx +49 -59
  161. package/tests/server/lazy-destructuring.test.tsrx +288 -366
  162. package/tests/server/return.test.tsrx +58 -36
  163. package/tests/server/streaming-ssr.test.tsrx +4 -4
  164. package/tests/server/style-identifier.test.tsrx +61 -69
  165. package/tests/server/switch.test.tsrx +89 -97
  166. package/tests/server/track-async-serialization.test.tsrx +85 -103
  167. package/tests/server/try.test.tsrx +275 -360
  168. package/tests/utils/ref-types.test.js +72 -0
  169. package/tests/utils/vite-plugin-config.test.js +41 -74
  170. package/types/index.d.ts +29 -4
  171. package/src/runtime/internal/client/compat.js +0 -40
  172. package/tests/utils/compiler-compat-config.test.js +0 -38
@@ -1,13 +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
- function App() {
7
- return <>
8
- let tag = track('div');
9
- <@tag>{'Hello World'}</@tag>
10
- </>;
6
+ function App() @{
7
+ let tag = track('div');
8
+ <Dynamic is={tag}>{'Hello World'}</Dynamic>
11
9
  }
12
10
  render(App);
13
11
 
@@ -16,14 +14,10 @@ describe('dynamic DOM elements', () => {
16
14
  expect(element.textContent).toBe('Hello World');
17
15
  });
18
16
 
19
- // The ts errors below are due to limitations in our current tsx generation for dynamic elements.
20
- // They can be ignored for now. But we'll fix them via jsx() vs <jsx>
21
17
  it('renders static dynamic element from a plain object with a tracked property', () => {
22
- function App() {
23
- return <>
24
- let obj = { tag: track('div') };
25
- <obj.tag.value>{'Hello World'}</obj.tag.value>
26
- </>;
18
+ function App() @{
19
+ let obj = { tag: track('div') };
20
+ <Dynamic is={obj.tag}>{'Hello World'}</Dynamic>
27
21
  }
28
22
  render(App);
29
23
 
@@ -33,12 +27,10 @@ describe('dynamic DOM elements', () => {
33
27
  });
34
28
 
35
29
  it('renders static dynamic element from a tracked object with a tracked property', () => {
36
- function App() {
37
- return <>
38
- let obj = track({ tag: track('div') });
39
- let tag = obj.value.tag;
40
- <@tag>{'Hello World'}</@tag>
41
- </>;
30
+ function App() @{
31
+ let obj = track({ tag: track('div') });
32
+ let tag = obj.value.tag;
33
+ <Dynamic is={tag}>{'Hello World'}</Dynamic>
42
34
  }
43
35
  render(App);
44
36
 
@@ -50,12 +42,10 @@ describe('dynamic DOM elements', () => {
50
42
  it(
51
43
  'renders static dynamic element from a tracked object with a computed tracked property',
52
44
  () => {
53
- function App() {
54
- return <>
55
- let obj = track({ tag: track('div') });
56
- let tag = obj.value['tag'];
57
- <@tag>{'Hello World'}</@tag>
58
- </>;
45
+ function App() @{
46
+ let obj = track({ tag: track('div') });
47
+ let tag = obj.value['tag'];
48
+ <Dynamic is={tag}>{'Hello World'}</Dynamic>
59
49
  }
60
50
  render(App);
61
51
 
@@ -66,18 +56,16 @@ describe('dynamic DOM elements', () => {
66
56
  );
67
57
 
68
58
  it('renders reactive dynamic element', () => {
69
- function App() {
70
- return <>
71
- let &[tag] = track('div');
59
+ function App() @{
60
+ let &[tag] = track('div');
61
+ <>
72
62
  <button
73
63
  onClick={() => {
74
64
  tag = 'span';
75
65
  }}
76
- >
77
- {'Change Tag'}
78
- </button>
79
- <@tag id="dynamic">{'Hello World'}</@tag>
80
- </>;
66
+ >{'Change Tag'}</button>
67
+ <Dynamic is={tag} id="dynamic">{'Hello World'}</Dynamic>
68
+ </>
81
69
  }
82
70
  render(App);
83
71
 
@@ -98,11 +86,9 @@ describe('dynamic DOM elements', () => {
98
86
  });
99
87
 
100
88
  it('renders self-closing dynamic element', () => {
101
- function App() {
102
- return <>
103
- let tag = track('input');
104
- <@tag type="text" value="test" />
105
- </>;
89
+ function App() @{
90
+ let tag = track('input');
91
+ <Dynamic is={tag} type="text" value="test" />
106
92
  }
107
93
  render(App);
108
94
 
@@ -113,12 +99,15 @@ describe('dynamic DOM elements', () => {
113
99
  });
114
100
 
115
101
  it('handles dynamic element with attributes', () => {
116
- function App() {
117
- return <>
118
- let tag = track('div');
119
- let &[className] = track('test-class');
120
- <@tag class={className} id="test" data-testid="dynamic-element">{'Content'}</@tag>
121
- </>;
102
+ function App() @{
103
+ let tag = track('div');
104
+ let &[className] = track('test-class');
105
+ <Dynamic
106
+ is={tag}
107
+ class={className}
108
+ id="test"
109
+ data-testid="dynamic-element"
110
+ >{'Content'}</Dynamic>
122
111
  }
123
112
  render(App);
124
113
 
@@ -130,14 +119,12 @@ describe('dynamic DOM elements', () => {
130
119
  });
131
120
 
132
121
  it('handles nested dynamic elements', () => {
133
- function App() {
134
- return <>
135
- let outerTag = track('div');
136
- let innerTag = track('span');
137
- <@outerTag class="outer">
138
- <@innerTag class="inner">{'Nested content'}</@innerTag>
139
- </@outerTag>
140
- </>;
122
+ function App() @{
123
+ let outerTag = track('div');
124
+ let innerTag = track('span');
125
+ <Dynamic is={outerTag} class="outer">
126
+ <Dynamic is={innerTag} class="inner">{'Nested content'}</Dynamic>
127
+ </Dynamic>
141
128
  }
142
129
  render(App);
143
130
 
@@ -151,14 +138,13 @@ describe('dynamic DOM elements', () => {
151
138
  });
152
139
 
153
140
  it('handles dynamic element with class object', () => {
154
- function App() {
155
- return <>
156
- let tag = track('div');
157
- let &[active] = track(true);
158
- <@tag class={{ active: active, 'dynamic-element': true }}>
159
- {'Element with class object'}
160
- </@tag>
161
- </>;
141
+ function App() @{
142
+ let tag = track('div');
143
+ let &[active] = track(true);
144
+ <Dynamic
145
+ is={tag}
146
+ class={{ active: active, 'dynamic-element': true }}
147
+ >{'Element with class object'}</Dynamic>
162
148
  }
163
149
  render(App);
164
150
 
@@ -169,19 +155,16 @@ describe('dynamic DOM elements', () => {
169
155
  });
170
156
 
171
157
  it('handles dynamic element with style object', () => {
172
- function App() {
173
- return <>
174
- let tag = track('span');
175
- <@tag
176
- style={{
177
- color: 'red',
178
- fontSize: '16px',
179
- fontWeight: 'bold',
180
- }}
181
- >
182
- {'Styled dynamic element'}
183
- </@tag>
184
- </>;
158
+ function App() @{
159
+ let tag = track('span');
160
+ <Dynamic
161
+ is={tag}
162
+ style={{
163
+ color: 'red',
164
+ fontSize: '16px',
165
+ fontWeight: 'bold',
166
+ }}
167
+ >{'Styled dynamic element'}</Dynamic>
185
168
  }
186
169
  render(App);
187
170
 
@@ -193,16 +176,18 @@ describe('dynamic DOM elements', () => {
193
176
  });
194
177
 
195
178
  it('handles dynamic element with spread attributes', () => {
196
- function App() {
197
- return <>
198
- let tag = track('section');
199
- const attrs = {
200
- id: 'spread-section',
201
- 'data-testid': 'spread-test',
202
- class: 'spread-class',
203
- };
204
- <@tag {...attrs} data-extra="additional">{'Element with spread attributes'}</@tag>
205
- </>;
179
+ function App() @{
180
+ let tag = track('section');
181
+ const attrs = {
182
+ id: 'spread-section',
183
+ 'data-testid': 'spread-test',
184
+ class: 'spread-class',
185
+ };
186
+ <Dynamic
187
+ is={tag}
188
+ {...attrs}
189
+ data-extra="additional"
190
+ >{'Element with spread attributes'}</Dynamic>
206
191
  }
207
192
  render(App);
208
193
 
@@ -217,18 +202,15 @@ describe('dynamic DOM elements', () => {
217
202
  it('handles dynamic element with ref', () => {
218
203
  let capturedElement: HTMLElement | null = null;
219
204
 
220
- function App() {
221
- return <>
222
- let tag = track('article');
223
- <@tag
224
- ref={(node: HTMLElement) => {
225
- capturedElement = node;
226
- }}
227
- id="ref-test"
228
- >
229
- {'Element with ref'}
230
- </@tag>
231
- </>;
205
+ function App() @{
206
+ let tag = track('article');
207
+ <Dynamic
208
+ is={tag}
209
+ ref={(node: HTMLElement) => {
210
+ capturedElement = node;
211
+ }}
212
+ id="ref-test"
213
+ >{'Element with ref'}</Dynamic>
232
214
  }
233
215
  render(App);
234
216
  flushSync();
@@ -244,27 +226,26 @@ describe('dynamic DOM elements', () => {
244
226
  let anonymousRefElement: HTMLInputElement | null = null;
245
227
  let namedRefElement: HTMLInputElement | null = null;
246
228
 
247
- function App() {
248
- return <>
249
- let tag = track('input');
250
- let input: HTMLInputElement | undefined;
251
- const state: { anonymous?: HTMLInputElement } = {};
252
- <@tag
253
- id="dynamic-ref-combo"
254
- type="text"
255
- ref={[
256
- input,
257
- state.anonymous,
258
- (node: HTMLInputElement | null) => {
259
- namedRefElement = node;
260
- },
261
- ]}
262
- />
263
- effect(() => {
264
- refAttrElement = input ?? null;
265
- anonymousRefElement = state.anonymous ?? null;
266
- });
267
- </>;
229
+ function App() @{
230
+ let tag = track('input');
231
+ let input: HTMLInputElement | undefined;
232
+ const state: { anonymous?: HTMLInputElement } = {};
233
+ effect(() => {
234
+ refAttrElement = input ?? null;
235
+ anonymousRefElement = state.anonymous ?? null;
236
+ });
237
+ <Dynamic
238
+ is={tag}
239
+ id="dynamic-ref-combo"
240
+ type="text"
241
+ ref={[
242
+ input,
243
+ state.anonymous,
244
+ (node: HTMLInputElement | null) => {
245
+ namedRefElement = node;
246
+ },
247
+ ]}
248
+ />
268
249
  }
269
250
 
270
251
  render(App);
@@ -284,59 +265,115 @@ describe('dynamic DOM elements', () => {
284
265
  let anonymousRefElement: HTMLInputElement | null = null;
285
266
  let namedRefElement: HTMLInputElement | null = null;
286
267
 
287
- function Child(props: PropsWithExtras<{}>) {
288
- return <><input id="dynamic-component-ref-combo" type="text" {...props} /></>;
268
+ function Child(props: PropsWithExtras<{}>) @{
269
+ <input id="dynamic-component-ref-combo" type="text" {...props} />
270
+ }
271
+
272
+ function App() @{
273
+ let dynamic = track(() => Child);
274
+ let input: HTMLInputElement | undefined;
275
+ const state: { anonymous?: HTMLInputElement } = {};
276
+ effect(() => {
277
+ refAttrElement = input ?? null;
278
+ anonymousRefElement = state.anonymous ?? null;
279
+ });
280
+ <Dynamic
281
+ is={dynamic}
282
+ ref={[
283
+ input,
284
+ state.anonymous,
285
+ (node: HTMLInputElement | null) => {
286
+ namedRefElement = node;
287
+ },
288
+ ]}
289
+ />
290
+ }
291
+
292
+ render(App);
293
+ flushSync();
294
+
295
+ const element = container.querySelector('#dynamic-component-ref-combo');
296
+ expect(element).toBeInstanceOf(HTMLInputElement);
297
+ expect(refAttrElement).toBe(element);
298
+ expect(anonymousRefElement).toBe(element);
299
+ expect(namedRefElement).toBe(element);
300
+ expect(element!.hasAttribute('ref')).toBe(false);
301
+ expect(element!.hasAttribute('input_ref')).toBe(false);
302
+ });
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} />
289
311
  }
290
312
 
291
- function App() {
292
- return <>
293
- let dynamic = track(() => Child);
294
- let input: HTMLInputElement | undefined;
295
- const state: { anonymous?: HTMLInputElement } = {};
296
- <@dynamic
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}
297
327
  ref={[
298
- input,
299
- state.anonymous,
328
+ (node: HTMLInputElement | null) => {
329
+ refAttrElement = node;
330
+ },
331
+ (node: HTMLInputElement | null) => {
332
+ anonymousRefElement = node;
333
+ },
300
334
  (node: HTMLInputElement | null) => {
301
335
  namedRefElement = node;
302
336
  },
303
337
  ]}
304
338
  />
305
- effect(() => {
306
- refAttrElement = input ?? null;
307
- anonymousRefElement = state.anonymous ?? null;
308
- });
309
- </>;
339
+ </>
310
340
  }
311
341
 
312
342
  render(App);
313
343
  flushSync();
314
344
 
315
- const element = container.querySelector('#dynamic-component-ref-combo');
316
- expect(element).toBeInstanceOf(HTMLInputElement);
317
- expect(refAttrElement).toBe(element);
318
- expect(anonymousRefElement).toBe(element);
319
- expect(namedRefElement).toBe(element);
320
- expect(element!.hasAttribute('ref')).toBe(false);
321
- expect(element!.hasAttribute('input_ref')).toBe(false);
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);
322
361
  });
323
362
 
324
363
  it('handles dynamic element with createRefKey in spread', () => {
325
- function App() {
326
- return <>
327
- let tag = track('header');
328
- function elementRef(node: HTMLElement) {
329
- // Set an attribute on the element to prove ref was called
330
- node.setAttribute('data-spread-ref-called', 'true');
331
- node.setAttribute('data-spread-ref-tag', node.tagName.toLowerCase());
332
- }
333
- const dynamicProps = {
334
- id: 'spread-ref-test',
335
- class: 'ref-element',
336
- [createRefKey()]: elementRef,
337
- };
338
- <@tag {...dynamicProps}>{'Element with spread ref'}</@tag>
339
- </>;
364
+ function App() @{
365
+ let tag = track('header');
366
+ function elementRef(node: HTMLElement) {
367
+ // Set an attribute on the element to prove ref was called
368
+ node.setAttribute('data-spread-ref-called', 'true');
369
+ node.setAttribute('data-spread-ref-tag', node.tagName.toLowerCase());
370
+ }
371
+ const dynamicProps = {
372
+ id: 'spread-ref-test',
373
+ class: 'ref-element',
374
+ [createRefKey()]: elementRef,
375
+ };
376
+ <Dynamic is={tag} {...dynamicProps}>{'Element with spread ref'}</Dynamic>
340
377
  }
341
378
  render(App);
342
379
  flushSync();
@@ -351,26 +388,25 @@ describe('dynamic DOM elements', () => {
351
388
  });
352
389
 
353
390
  it('has reactive attributes on dynamic elements', () => {
354
- function App() {
355
- return <>
356
- let tag = track('div');
357
- let &[count] = track(0);
391
+ function App() @{
392
+ let tag = track('div');
393
+ let &[count] = track(0);
394
+ <>
358
395
  <button
359
396
  onClick={() => {
360
397
  count++;
361
398
  }}
362
- >
363
- {'Increment'}
364
- </button>
365
- <@tag
399
+ >{'Increment'}</button>
400
+ <Dynamic
401
+ is={tag}
366
402
  id={count % 2 ? 'even' : 'odd'}
367
403
  class={count % 2 ? 'even-class' : 'odd-class'}
368
404
  data-count={count}
369
405
  >
370
406
  {'Count: '}
371
407
  {count}
372
- </@tag>
373
- </>;
408
+ </Dynamic>
409
+ </>
374
410
  }
375
411
 
376
412
  render(App);
@@ -406,16 +442,16 @@ describe('dynamic DOM elements', () => {
406
442
  });
407
443
 
408
444
  it('applies scoped CSS to dynamic elements', () => {
409
- function App() {
410
- return <>
411
- let tag = track('div');
412
- <@tag class="test-class">{'Dynamic element'}</@tag>
445
+ function App() @{
446
+ let tag = track('div');
447
+ <>
448
+ <Dynamic is={tag} class="test-class">{'Dynamic element'}</Dynamic>
413
449
  <style>
414
450
  .test-class {
415
451
  color: red;
416
452
  }
417
453
  </style>
418
- </>;
454
+ </>
419
455
  }
420
456
 
421
457
  render(App);
@@ -431,11 +467,12 @@ describe('dynamic DOM elements', () => {
431
467
  });
432
468
 
433
469
  it('applies scoped CSS to dynamic elements with reactive classes', () => {
434
- function App() {
435
- return <>
436
- let tag = track('button');
437
- let &[count] = track(0);
438
- <@tag
470
+ function App() @{
471
+ let tag = track('button');
472
+ let &[count] = track(0);
473
+ <>
474
+ <Dynamic
475
+ is={tag}
439
476
  class={count % 2 ? 'even' : 'odd'}
440
477
  id={count % 2 ? 'even' : 'odd'}
441
478
  onClick={() => {
@@ -444,7 +481,7 @@ describe('dynamic DOM elements', () => {
444
481
  >
445
482
  {'Count: '}
446
483
  {count}
447
- </@tag>
484
+ </Dynamic>
448
485
  <style>
449
486
  .even {
450
487
  background-color: green;
@@ -455,7 +492,7 @@ describe('dynamic DOM elements', () => {
455
492
  color: white;
456
493
  }
457
494
  </style>
458
- </>;
495
+ </>
459
496
  }
460
497
 
461
498
  render(App);
@@ -505,10 +542,10 @@ describe('dynamic DOM elements', () => {
505
542
  class: string;
506
543
  id: string;
507
544
  onClick: EventListener;
508
- }>) {
509
- return <>
510
- const tag = track('button');
511
- <@tag {...rest}>{rest.class}</@tag>
545
+ }>) @{
546
+ const tag = track('button');
547
+ <>
548
+ <Dynamic is={tag} {...rest}>{rest.class}</Dynamic>
512
549
  <style>
513
550
  .even {
514
551
  background-color: green;
@@ -517,20 +554,18 @@ describe('dynamic DOM elements', () => {
517
554
  background-color: red;
518
555
  }
519
556
  </style>
520
- </>;
557
+ </>
521
558
  }
522
559
 
523
- function App() {
524
- return <>
525
- let &[count] = track(0);
526
- <DynamicButton
527
- class={count % 2 ? 'even' : 'odd'}
528
- id={count % 2 ? 'even' : 'odd'}
529
- onClick={() => {
530
- count++;
531
- }}
532
- />
533
- </>;
560
+ function App() @{
561
+ let &[count] = track(0);
562
+ <DynamicButton
563
+ class={count % 2 ? 'even' : 'odd'}
564
+ id={count % 2 ? 'even' : 'odd'}
565
+ onClick={() => {
566
+ count++;
567
+ }}
568
+ />
534
569
  }
535
570
 
536
571
  render(App);
@@ -565,18 +600,18 @@ describe('dynamic DOM elements', () => {
565
600
  });
566
601
 
567
602
  it('adds scoping class to dynamic elements', () => {
568
- function App() {
569
- return <>
570
- let tag = track('div');
571
- <@tag class="scoped">
603
+ function App() @{
604
+ let tag = track('div');
605
+ <>
606
+ <Dynamic is={tag} class="scoped">
572
607
  <p>{'Scoped dynamic element'}</p>
573
- </@tag>
608
+ </Dynamic>
574
609
  <style>
575
610
  .scoped {
576
611
  color: blue;
577
612
  }
578
613
  </style>
579
- </>;
614
+ </>
580
615
  }
581
616
  render(App);
582
617
 
@@ -588,18 +623,18 @@ describe('dynamic DOM elements', () => {
588
623
  });
589
624
 
590
625
  it('adds scoping class to dynamic elements when selector targets by tag name', () => {
591
- function App() {
592
- return <>
593
- let tag = track('div');
594
- <@tag class="scoped">
626
+ function App() @{
627
+ let tag = track('div');
628
+ <>
629
+ <Dynamic is={tag} class="scoped">
595
630
  <p>{'Scoped dynamic element'}</p>
596
- </@tag>
631
+ </Dynamic>
597
632
  <style>
598
633
  div {
599
634
  color: blue;
600
635
  }
601
636
  </style>
602
- </>;
637
+ </>
603
638
  }
604
639
  render(App);
605
640
 
@@ -611,8 +646,8 @@ describe('dynamic DOM elements', () => {
611
646
  });
612
647
 
613
648
  it('doesn\'t add scoping class to components inside dynamic element', () => {
614
- function Child() {
615
- return <>
649
+ function Child() @{
650
+ <>
616
651
  <div class="child">
617
652
  <p>{'I am a child component'}</p>
618
653
  </div>
@@ -621,22 +656,22 @@ describe('dynamic DOM elements', () => {
621
656
  color: blue;
622
657
  }
623
658
  </style>
624
- </>;
659
+ </>
625
660
  }
626
661
 
627
- function App() {
628
- return <>
629
- let tag = track('div');
630
- <@tag class="scoped">
662
+ function App() @{
663
+ let tag = track('div');
664
+ <>
665
+ <Dynamic is={tag} class="scoped">
631
666
  <p>{'Scoped dynamic element'}</p>
632
667
  <Child />
633
- </@tag>
668
+ </Dynamic>
634
669
  <style>
635
670
  div {
636
671
  color: blue;
637
672
  }
638
673
  </style>
639
- </>;
674
+ </>
640
675
  }
641
676
  render(App);
642
677
 
@@ -657,8 +692,8 @@ describe('dynamic DOM elements', () => {
657
692
  });
658
693
 
659
694
  it('doesn\'t add scoping class to dynamically rendered component', () => {
660
- function Child() {
661
- return <>
695
+ function Child() @{
696
+ <>
662
697
  <div class="child">
663
698
  <p>{'I am a child component'}</p>
664
699
  </div>
@@ -667,19 +702,19 @@ describe('dynamic DOM elements', () => {
667
702
  color: green;
668
703
  }
669
704
  </style>
670
- </>;
705
+ </>
671
706
  }
672
707
 
673
- function App() {
674
- return <>
675
- let tag = track(() => Child);
676
- <@tag />
708
+ function App() @{
709
+ let tag = track(() => Child);
710
+ <>
711
+ <Dynamic is={tag} />
677
712
  <style>
678
713
  .child {
679
714
  color: red;
680
715
  }
681
716
  </style>
682
- </>;
717
+ </>
683
718
  }
684
719
  render(App);
685
720
 
@@ -697,29 +732,23 @@ describe('dynamic DOM elements', () => {
697
732
  let capturedElement: HTMLElement | null = null;
698
733
  let refCallCount = 0;
699
734
 
700
- function Button(props: any) {
701
- return <>
702
- const el = track('button');
703
- <@el {...props} />
704
- </>;
735
+ function Button(props: any) @{
736
+ const el = track('button');
737
+ <Dynamic is={el} {...props} />
705
738
  }
706
739
 
707
- function App() {
708
- return <>
709
- let &[active] = track(false);
710
- <Button
711
- data-active={String(active)}
712
- onClick={() => {
713
- active = !active;
714
- }}
715
- ref={(el: HTMLElement) => {
716
- capturedElement = el;
717
- refCallCount++;
718
- }}
719
- >
720
- {'content'}
721
- </Button>
722
- </>;
740
+ function App() @{
741
+ let &[active] = track(false);
742
+ <Button
743
+ data-active={String(active)}
744
+ onClick={() => {
745
+ active = !active;
746
+ }}
747
+ ref={(el: HTMLElement) => {
748
+ capturedElement = el;
749
+ refCallCount++;
750
+ }}
751
+ >{'content'}</Button>
723
752
  }
724
753
 
725
754
  render(App);
@@ -743,34 +772,30 @@ describe('dynamic DOM elements', () => {
743
772
  it('handles ref on dynamic element with spread props containing reactive values', () => {
744
773
  let capturedElement: HTMLElement | null = null;
745
774
 
746
- function Button(props: any) {
747
- return <>
748
- const el = track('button');
749
- <@el {...props} />
750
- </>;
751
- }
752
-
753
- function App() {
754
- return <>
755
- let &[active] = track(false);
756
- let &[buttonProps] = track(
757
- () => ({
758
- 'data-active': active,
759
- }),
760
- );
761
- <Button
762
- {...buttonProps}
763
- onClick={() => {
764
- active = !active;
765
- }}
766
- ref={(el: HTMLElement) => {
767
- capturedElement = el;
768
- }}
769
- >
770
- {'content: '}
771
- {active}
772
- </Button>
773
- </>;
775
+ function Button(props: any) @{
776
+ const el = track('button');
777
+ <Dynamic is={el} {...props} />
778
+ }
779
+
780
+ function App() @{
781
+ let &[active] = track(false);
782
+ let &[buttonProps] = track(
783
+ () => ({
784
+ 'data-active': active,
785
+ }),
786
+ );
787
+ <Button
788
+ {...buttonProps}
789
+ onClick={() => {
790
+ active = !active;
791
+ }}
792
+ ref={(el: HTMLElement) => {
793
+ capturedElement = el;
794
+ }}
795
+ >
796
+ {'content: '}
797
+ {active}
798
+ </Button>
774
799
  }
775
800
 
776
801
  render(App);
@@ -792,32 +817,26 @@ describe('dynamic DOM elements', () => {
792
817
  let refCallCount = 0;
793
818
  let capturedElement: HTMLElement | null = null;
794
819
 
795
- function Button(props: any) {
796
- return <>
797
- const el = track('button');
798
- <@el {...props} />
799
- </>;
820
+ function Button(props: any) @{
821
+ const el = track('button');
822
+ <Dynamic is={el} {...props} />
800
823
  }
801
824
 
802
- function App() {
803
- return <>
804
- let &[active] = track(false);
805
- <Button
806
- data-active={String(active)}
807
- onClick={() => {
808
- active = !active;
809
- }}
810
- ref={(el: HTMLElement) => {
811
- capturedElement = el;
812
- refCallCount++;
813
- return () => {
814
- cleanupCount++;
815
- };
816
- }}
817
- >
818
- {'content'}
819
- </Button>
820
- </>;
825
+ function App() @{
826
+ let &[active] = track(false);
827
+ <Button
828
+ data-active={String(active)}
829
+ onClick={() => {
830
+ active = !active;
831
+ }}
832
+ ref={(el: HTMLElement) => {
833
+ capturedElement = el;
834
+ refCallCount++;
835
+ return () => {
836
+ cleanupCount++;
837
+ };
838
+ }}
839
+ >{'content'}</Button>
821
840
  }
822
841
 
823
842
  render(App);
@@ -837,16 +856,16 @@ describe('dynamic DOM elements', () => {
837
856
  });
838
857
 
839
858
  it('should remove and add back a text node in a conditional statement with a tracked', () => {
840
- function App() {
841
- return <>
842
- let &[b] = track(true);
859
+ function App() @{
860
+ let &[b] = track(true);
861
+ <>
843
862
  <div>
844
- if (b) {
845
- {'Inside if'}
863
+ @if (b) {
864
+ <>Inside if</>
846
865
  }
847
866
  </div>
848
867
  <button onClick={() => (b = !b)}>{'Toggle b'}</button>
849
- </>;
868
+ </>
850
869
  }
851
870
 
852
871
  render(App);