Skip to content

Commit 6d57e30

Browse files
committed
feat(react-modifier): add support for children as a prop
1 parent a641d3e commit 6d57e30

File tree

5 files changed

+75
-12
lines changed

5 files changed

+75
-12
lines changed

react-migration-toolkit/src/components/react-bridge.gts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ type ExtraProps = Partial<{
1212
[ariaAttrs: `aria-${string}`]: string;
1313
}>;
1414

15-
type PropsOf<T> = T extends ComponentType<infer P>
16-
? Omit<P, 'children'> & ExtraProps
17-
: never;
15+
type PropsOf<T> = T extends ComponentType<infer P> ? P & ExtraProps : never;
1816

1917
interface ReactBridgeArgs<T extends keyof HTMLElementTagNameMap, R> {
2018
Element: ElementFromTagName<T>;

react-migration-toolkit/src/modifiers/react-modifier.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ import { getOwner } from '@ember/application';
33

44
import { isTesting, macroCondition } from '@embroider/macros';
55
import Modifier from 'ember-modifier';
6-
import { createElement, type ReactElement, type ComponentType } from 'react';
6+
import {
7+
act,
8+
type ComponentType,
9+
createElement,
10+
type PropsWithChildren,
11+
type ReactNode,
12+
} from 'react';
713
import { createRoot, type Root } from 'react-dom/client';
8-
import { act } from 'react';
914
import type ApplicationInstance from '@ember/application/instance';
1015
import { App } from '../react/app/app';
1116
import type { CustomProviderOptions } from '../../types';
@@ -27,7 +32,7 @@ function cleanup(instance: ReactModifier) {
2732

2833
type ReactModifierOptions = {
2934
reactComponent: ComponentType;
30-
props: object;
35+
props: PropsWithChildren;
3136
providerOptions: CustomProviderOptions | undefined;
3237
hasBlock: boolean;
3338
};
@@ -48,14 +53,19 @@ declare global {
4853

4954
export default class ReactModifier extends Modifier<ReactModifierSignature> {
5055
root: Root | null = null;
51-
children: ReactElement[] | null = null;
56+
children: ReactNode;
5257
isInitialRender = true;
5358
owner = getOwner(this) as ApplicationInstance;
5459

5560
modify(
5661
element: Element,
5762
_: null,
58-
{ reactComponent, props, providerOptions, hasBlock }: ReactModifierOptions,
63+
{
64+
reactComponent,
65+
props = {},
66+
providerOptions,
67+
hasBlock,
68+
}: ReactModifierOptions,
5969
) {
6070
if (!this.root) {
6171
this.root = createRoot(element);
@@ -71,20 +81,27 @@ export default class ReactModifier extends Modifier<ReactModifierSignature> {
7181
},
7282
);
7383
if (this.isInitialRender && filteredChildNodes.length > 0 && hasBlock) {
74-
const children = [
84+
this.children = [
7585
createElement<YieldWrapperProps>(YieldWrapper, {
7686
key: crypto.randomUUID(),
7787
nodes: Array.from(filteredChildNodes),
7888
}),
7989
];
80-
this.children = children;
8190
this.isInitialRender = false;
8291
}
8392

93+
const { children, ...restOfProps } = props;
94+
const childrenFromProps =
95+
typeof children === 'function' ? createElement(children) : children;
96+
8497
const wrappedComponent = createElement(
8598
App,
8699
{ owner: this.owner, providerOptions },
87-
createElement(reactComponent, props, this.children),
100+
createElement(
101+
reactComponent,
102+
restOfProps,
103+
childrenFromProps || this.children,
104+
),
88105
);
89106

90107
if (macroCondition(isTesting())) {

test-app/app/components/example.hbs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,14 @@
1111
{{! @glint-expect-error: the SafeString transformation is not properly typed }}
1212
@props={{hash text=this.safeText data-test-example="true"}}
1313
class="class-from-bridge"
14+
/>
15+
16+
<ReactBridge
17+
@reactComponent={{this.reactExample}}
18+
@props={{hash children=this.reactExample}}
19+
/>
20+
21+
<ReactBridge
22+
@reactComponent={{this.reactExample}}
23+
@props={{hash children="Children as Props"}}
1424
/>

test-app/app/react/example.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function Example({ text, children, ...props }: ExampleProps): ReactNode {
2020
<div
2121
data-test-has-owner={owner instanceof ApplicationInstance}
2222
data-test-theme={theme?.current}
23+
style={{ border: "1px solid gray", padding: 16, margin: 16 }}
2324
{...props}
2425
>
2526
<h1>Hi there 👋</h1>
@@ -45,7 +46,7 @@ export function Example({ text, children, ...props }: ExampleProps): ReactNode {
4546
<div data-test-children>
4647
<hr />
4748
<h3>Children values:</h3>
48-
{children}
49+
<div data-test-children-content>{children}</div>
4950
</div>
5051
)}
5152
</div>

test-app/tests/integration/components/react-bridge-test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,4 +272,41 @@ module('Integration | Component | react-bridge', function (hooks) {
272272
.hasAttribute('href', 'https://www.google.com')
273273
.hasText('world');
274274
});
275+
276+
module('when children prop is passed directly', function () {
277+
test('it correctly renders strings', async function (assert) {
278+
let text = `Test text as children`;
279+
280+
this.setProperties({
281+
reactExample: Example,
282+
props: {
283+
children: text,
284+
},
285+
});
286+
287+
await render(hbs`
288+
<ReactBridge
289+
@reactComponent={{this.reactExample}}
290+
@props={{hash children=this.props.children}}
291+
/>
292+
`);
293+
294+
assert.dom('[data-test-children-content]').hasText(text);
295+
});
296+
297+
test('it correctly renders ReactNode', async function (assert) {
298+
this.setProperties({
299+
reactExample: Example,
300+
});
301+
302+
await render(hbs`
303+
<ReactBridge
304+
@reactComponent={{this.reactExample}}
305+
@props={{hash children=this.reactExample}}
306+
/>
307+
`);
308+
309+
assert.dom('h1').exists({ count: 2 });
310+
});
311+
});
275312
});

0 commit comments

Comments
 (0)