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
- import type { PropsWithExtras } from 'ripple';
2
- import { createRefKey, track } from 'ripple';
1
+ import type { DynamicProps, PropsWithExtras, TSRXElement } from 'ripple';
2
+ import { createRefKey, Dynamic, track } from 'ripple';
3
3
 
4
4
  describe('server dynamic DOM elements', () => {
5
5
  it('renders static dynamic element', async () => {
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
 
11
11
  const { body } = await render(App);
@@ -13,13 +13,11 @@ describe('server dynamic DOM elements', () => {
13
13
  expect(body).toBeHtml('<div>Hello World</div>');
14
14
  });
15
15
 
16
- // The ts errors below are due to limitations in our current tsx generation for dynamic elements.
17
- // They can be ignored for now. But we'll fix them via jsx() vs <jsx>
18
16
  it('renders static dynamic element from a plain object with a tracked property', async () => {
19
17
  function App() @{
20
18
  let obj = { tag: track('div') };
21
19
  let tag = obj.tag;
22
- <@tag>{'Hello World'}</@tag>
20
+ <Dynamic is={tag}>{'Hello World'}</Dynamic>
23
21
  }
24
22
 
25
23
  const { body } = await render(App);
@@ -27,11 +25,30 @@ describe('server dynamic DOM elements', () => {
27
25
  expect(body).toBeHtml('<div>Hello World</div>');
28
26
  });
29
27
 
28
+ it('does not pass is to dynamic component props', async () => {
29
+ function Child(props: Omit<DynamicProps<'div'>, 'is'> & {
30
+ label: string;
31
+ is?: Function;
32
+ }): TSRXElement<'div'> @{
33
+ <div>
34
+ {props.is === undefined && !('is' in props) ? 'hidden' : 'leaked'}
35
+ </div>
36
+ }
37
+
38
+ function App() @{
39
+ <Dynamic is={Child} label="child" class="yo" />
40
+ }
41
+
42
+ const { body } = await render(App);
43
+
44
+ expect(body).toBeHtml('<div>hidden</div>');
45
+ });
46
+
30
47
  it('renders static dynamic element from a tracked object with a tracked property', async () => {
31
48
  function App() @{
32
49
  let obj = track({ tag: track('div') });
33
50
  let tag = obj.value.tag;
34
- <@tag>{'Hello World'}</@tag>
51
+ <Dynamic is={tag}>{'Hello World'}</Dynamic>
35
52
  }
36
53
 
37
54
  const { body } = await render(App);
@@ -45,7 +62,7 @@ describe('server dynamic DOM elements', () => {
45
62
  function App() @{
46
63
  let obj = track({ tag: track('div') });
47
64
  let tag = obj.value['tag'];
48
- <@tag>{'Hello World'}</@tag>
65
+ <Dynamic is={tag}>{'Hello World'}</Dynamic>
49
66
  }
50
67
 
51
68
  const { body } = await render(App);
@@ -57,7 +74,7 @@ describe('server dynamic DOM elements', () => {
57
74
  it('renders self-closing dynamic element', async () => {
58
75
  function App() @{
59
76
  let tag = track('input');
60
- <@tag type="text" value="test" />
77
+ <Dynamic is={tag} type="text" value="test" />
61
78
  }
62
79
 
63
80
  const { body } = await render(App);
@@ -69,7 +86,12 @@ describe('server dynamic DOM elements', () => {
69
86
  function App() @{
70
87
  let tag = track('div');
71
88
  let &[className] = track('test-class');
72
- <@tag class={className} id="test" data-testid="dynamic-element">{'Content'}</@tag>
89
+ <Dynamic
90
+ is={tag}
91
+ class={className}
92
+ id="test"
93
+ data-testid="dynamic-element"
94
+ >{'Content'}</Dynamic>
73
95
  }
74
96
  const { body } = await render(App);
75
97
  const { document } = parseHtml(body);
@@ -86,9 +108,9 @@ describe('server dynamic DOM elements', () => {
86
108
  function App() @{
87
109
  let outerTag = track('div');
88
110
  let innerTag = track('span');
89
- <@outerTag class="outer">
90
- <@innerTag class="inner">{'Nested content'}</@innerTag>
91
- </@outerTag>
111
+ <Dynamic is={outerTag} class="outer">
112
+ <Dynamic is={innerTag} class="inner">{'Nested content'}</Dynamic>
113
+ </Dynamic>
92
114
  }
93
115
  const { body } = await render(App);
94
116
  const { document } = parseHtml(body);
@@ -106,7 +128,10 @@ describe('server dynamic DOM elements', () => {
106
128
  function App() @{
107
129
  let tag = track('div');
108
130
  let &[active] = track(true);
109
- <@tag class={{ active: active, 'dynamic-element': true }}>{'Element with class object'}</@tag>
131
+ <Dynamic
132
+ is={tag}
133
+ class={{ active: active, 'dynamic-element': true }}
134
+ >{'Element with class object'}</Dynamic>
110
135
  }
111
136
 
112
137
  const { body } = await render(App);
@@ -121,13 +146,14 @@ describe('server dynamic DOM elements', () => {
121
146
  it('handles dynamic element with style object', async () => {
122
147
  function App() @{
123
148
  let tag = track('span');
124
- <@tag
149
+ <Dynamic
150
+ is={tag}
125
151
  style={{
126
152
  color: 'red',
127
153
  fontSize: '16px',
128
154
  fontWeight: 'bold',
129
155
  }}
130
- >{'Styled dynamic element'}</@tag>
156
+ >{'Styled dynamic element'}</Dynamic>
131
157
  }
132
158
 
133
159
  const { body } = await render(App);
@@ -148,7 +174,11 @@ describe('server dynamic DOM elements', () => {
148
174
  'data-testid': 'spread-test',
149
175
  class: 'spread-class',
150
176
  };
151
- <@tag {...attrs} data-extra="additional">{'Element with spread attributes'}</@tag>
177
+ <Dynamic
178
+ is={tag}
179
+ {...attrs}
180
+ data-extra="additional"
181
+ >{'Element with spread attributes'}</Dynamic>
152
182
  }
153
183
  const { body } = await render(App);
154
184
  const { document } = parseHtml(body);
@@ -166,12 +196,13 @@ describe('server dynamic DOM elements', () => {
166
196
 
167
197
  function App() @{
168
198
  let tag = track('article');
169
- <@tag
199
+ <Dynamic
200
+ is={tag}
170
201
  ref={(node: HTMLElement) => {
171
202
  capturedElement = node;
172
203
  }}
173
204
  id="ref-test"
174
- >{'Element with ref'}</@tag>
205
+ >{'Element with ref'}</Dynamic>
175
206
  }
176
207
 
177
208
  const { body } = await render(App);
@@ -199,7 +230,7 @@ describe('server dynamic DOM elements', () => {
199
230
  class: 'ref-element',
200
231
  [createRefKey()]: elementRef,
201
232
  };
202
- <@tag {...dynamicProps}>{'Element with spread ref'}</@tag>
233
+ <Dynamic is={tag} {...dynamicProps}>{'Element with spread ref'}</Dynamic>
203
234
  }
204
235
 
205
236
  const { body } = await render(App);
@@ -218,7 +249,7 @@ describe('server dynamic DOM elements', () => {
218
249
  function App() @{
219
250
  let tag = track('div');
220
251
  <>
221
- <@tag class="test-class">{'Dynamic element'}</@tag>
252
+ <Dynamic is={tag} class="test-class">{'Dynamic element'}</Dynamic>
222
253
  <style>
223
254
  .test-class {
224
255
  color: red;
@@ -246,7 +277,7 @@ describe('server dynamic DOM elements', () => {
246
277
  }>) @{
247
278
  const tag = track('button');
248
279
  <>
249
- <@tag {...rest}>{rest.class}</@tag>
280
+ <Dynamic is={tag} {...rest}>{rest.class}</Dynamic>
250
281
  <style>
251
282
  .even {
252
283
  background-color: green;
@@ -283,9 +314,9 @@ describe('server dynamic DOM elements', () => {
283
314
  function App() @{
284
315
  let tag = track('div');
285
316
  <>
286
- <@tag class="scoped">
317
+ <Dynamic is={tag} class="scoped">
287
318
  <p>{'Scoped dynamic element'}</p>
288
- </@tag>
319
+ </Dynamic>
289
320
  <style>
290
321
  .scoped {
291
322
  color: blue;
@@ -312,9 +343,9 @@ describe('server dynamic DOM elements', () => {
312
343
  function App() @{
313
344
  let tag = track('div');
314
345
  <>
315
- <@tag class="scoped">
346
+ <Dynamic is={tag} class="scoped">
316
347
  <p>{'Scoped dynamic element'}</p>
317
- </@tag>
348
+ </Dynamic>
318
349
  <style>
319
350
  div {
320
351
  color: blue;
@@ -351,10 +382,10 @@ describe('server dynamic DOM elements', () => {
351
382
  function App() @{
352
383
  let tag = track('div');
353
384
  <>
354
- <@tag class="scoped">
385
+ <Dynamic is={tag} class="scoped">
355
386
  <p>{'Scoped dynamic element'}</p>
356
387
  <Child />
357
- </@tag>
388
+ </Dynamic>
358
389
  <style>
359
390
  div {
360
391
  color: blue;
@@ -399,7 +430,7 @@ describe('server dynamic DOM elements', () => {
399
430
  function App() @{
400
431
  let tag = track(() => Child);
401
432
  <>
402
- <@tag />
433
+ <Dynamic is={tag} />
403
434
  <style>
404
435
  .child {
405
436
  color: red;
@@ -90,6 +90,29 @@ describe('head elements', () => {
90
90
  expect(document.title).toBe('');
91
91
  });
92
92
 
93
+ it('renders external scripts with src attributes from a loop', async () => {
94
+ const Head = ({ scripts }: { scripts: { src: string }[] }) => @{
95
+ <head>
96
+ @for (const script of scripts) {
97
+ <script src={script.src} />
98
+ }
99
+ </head>
100
+ };
101
+
102
+ function App() @{
103
+ <Head scripts={[{ src: '/a.js' }, { src: '/b.js' }]} />
104
+ }
105
+
106
+ const { head, body } = await render(App);
107
+ const { document } = parseAsFullHtml(head, body);
108
+
109
+ const srcs = Array.from(document.querySelectorAll('head script[src]')).map(
110
+ (node) => node.getAttribute('src'),
111
+ ).sort();
112
+
113
+ expect(srcs).toEqual(['/a.js', '/b.js']);
114
+ });
115
+
93
116
  it('renders title with conditional content', async () => {
94
117
  function App() @{
95
118
  let &[showPrefix] = track(true);
@@ -1,4 +1,4 @@
1
- import { track } from 'ripple';
1
+ import { Dynamic, track } from 'ripple';
2
2
  import { compile } from '@tsrx/ripple';
3
3
 
4
4
  const external_styles = <style>
@@ -178,7 +178,7 @@ describe('style class maps (server)', () => {
178
178
 
179
179
  let dynamic = track(() => Child);
180
180
  <div class="wrapper">
181
- <@dynamic cls={styles.text} />
181
+ <Dynamic is={dynamic} cls={styles.text} />
182
182
  </div>
183
183
  }
184
184
 
@@ -269,9 +269,9 @@ describe('style class maps (server)', () => {
269
269
  function App() @{
270
270
  const DynamicWrapper = track(() => Wrapper);
271
271
  <>
272
- <@DynamicWrapper>
272
+ <Dynamic is={DynamicWrapper}>
273
273
  <div class="green">{'Slotted child'}</div>
274
- </@DynamicWrapper>
274
+ </Dynamic>
275
275
  <style>
276
276
  .green {
277
277
  color: green;
package/types/index.d.ts CHANGED
@@ -2,15 +2,16 @@ import type { ExtendedEventOptions } from '@tsrx/core/types';
2
2
  export type { RefValue } from '@tsrx/core/runtime/ref';
3
3
  export type { AddEventOptions, AddEventObject, ExtendedEventOptions } from '@tsrx/core/types';
4
4
 
5
- export type Component<T = Record<string, any>> = (props: T) => void;
5
+ export type Component<T = Record<string, any>> = (props: T) => void | TSRXElement;
6
6
 
7
7
  declare const TSRX_ELEMENT: unique symbol;
8
8
  declare const REF_KEY: unique symbol;
9
9
  export type RefKey = typeof REF_KEY;
10
10
 
11
- export type TSRXElement = {
11
+ export type TSRXElement<Tag = any> = {
12
12
  readonly render: Function;
13
13
  readonly [TSRX_ELEMENT]: true;
14
+ readonly __tag?: Tag;
14
15
  };
15
16
 
16
17
  /** Type for implicit children fragments rendered with `{children}`. */
@@ -26,6 +27,29 @@ export function tsrx_element(render: Function): TSRXElement;
26
27
 
27
28
  export function Fragment(props: FragmentProps): TSRXElement;
28
29
 
30
+ type DynamicIntrinsicElements = JSX.IntrinsicElements;
31
+ export type DynamicElementType = keyof DynamicIntrinsicElements | Component<any> | (string & {});
32
+ type UnwrapTracked<T> = T extends Tracked<infer V> ? V : T;
33
+ type DynamicTarget<T> = Exclude<UnwrapTracked<T>, null | undefined | false>;
34
+ type DynamicComponentProps<T> = [T] extends [never]
35
+ ? Props
36
+ : T extends Component<infer P>
37
+ ? Omit<P, 'is'>
38
+ : T extends keyof DynamicIntrinsicElements
39
+ ? DynamicIntrinsicElements[T]
40
+ : Props;
41
+ export type DynamicProps<T> = Expand<
42
+ {
43
+ is: T;
44
+ } & DynamicComponentProps<DynamicTarget<NoInfer<T>>>
45
+ >;
46
+
47
+ export function Dynamic<T>(
48
+ props: {
49
+ is: T;
50
+ } & DynamicComponentProps<DynamicTarget<NoInfer<T>>>,
51
+ ): TSRXElement;
52
+
29
53
  export function mount(
30
54
  component: Component,
31
55
  options: { target: HTMLElement; props?: Record<string, any>; rootBoundary?: RootBoundaryOptions },
@@ -174,8 +198,8 @@ interface TrackedBase<V> {
174
198
  '#v': V;
175
199
  value: V;
176
200
  }
177
- // Augment Tracked to be callable when V is a Component
178
- // This allows <@Something /> to work in JSX when Something is Tracked<Component>
201
+ // Augment Tracked to be callable when V is a Component.
202
+ // This allows tracked component values to continue flowing through JSX checks.
179
203
  interface TrackedCallable<V> {
180
204
  (props: V extends Component<infer P> ? P : never): V extends Component ? void : never;
181
205
  }