Skip to content

Commit da247f0

Browse files
author
Ben Grynhaus
committed
Render ng-templates using a ReactTemplate (React) component
Tracks changes via an `NgZone`
1 parent 271ce22 commit da247f0

File tree

5 files changed

+115
-22
lines changed

5 files changed

+115
-22
lines changed

libs/core/src/lib/components/wrapper-component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,12 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
192192
return undefined;
193193
}
194194

195+
if (!this._ngZone) {
196+
throw new Error('To create an input JSX renderer you must pass an NgZone to the constructor.');
197+
}
198+
195199
if (input instanceof TemplateRef) {
196-
const templateRenderer = createTemplateRenderer(input, additionalProps);
200+
const templateRenderer = createTemplateRenderer(input, this._ngZone, additionalProps);
197201
return (context: TContext) => templateRenderer.render(context);
198202
}
199203

libs/core/src/lib/renderer/react-content.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ export interface ReactContentProps {
2323
/**
2424
* Creates a new `ReactContent` element.
2525
* @param children The children to append to the `ReactContent` element.
26-
* @param additionalProps _Optional_. @see `ReactContentProps`
27-
* @returns
26+
* @param additionalProps _Optional_. @see `ReactContentProps`.
2827
*/
2928
export function createReactContentElement(children: ReadonlyArray<HTMLElement>, additionalProps?: ReactContentProps) {
3029
return React.createElement(ReactContent, {
@@ -36,12 +35,12 @@ export function createReactContentElement(children: ReadonlyArray<HTMLElement>,
3635
/**
3736
* @internal
3837
*/
39-
export interface InternalReactContentProps extends ReactContentProps {
38+
interface InternalReactContentProps extends ReactContentProps {
4039
readonly [CHILDREN_TO_APPEND_PROP]: ReadonlyArray<HTMLElement>;
4140
}
4241

4342
/**
44-
* Render any `HTMLElement`s (including Angular components) as a child of React components.
43+
* Render any `HTMLElement`s as a child of React components.
4544
* Supports two rendering modes:
4645
* 1. `legacy` - append `<react-content>` as the root, and nest the `children-to-append` underneath it.
4746
* 2. `new` (**default**) - append the `children-to-append` to the parent of this component, and hide the `<react-content>` element.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { EmbeddedViewRef, NgZone, TemplateRef } from '@angular/core';
5+
import * as React from 'react';
6+
import * as ReactDOM from 'react-dom';
7+
import { Subscription } from 'rxjs';
8+
9+
const DEBUG = false;
10+
11+
/**
12+
* @internal
13+
*/
14+
export interface ReactTemplateProps {
15+
/**
16+
* Experimental rendering mode.
17+
* Uses a similar approach to `router-outlet`, where the child elements are added to the parent, instead of this node, and this is hidden.
18+
* @default false
19+
*/
20+
legacyRenderMode?: boolean;
21+
}
22+
23+
/**
24+
* Creates a new `ReactTemplate` element.
25+
* @param templateRef The template to render.
26+
* @param context The context to inject the template.
27+
* @param ngZone A zone used for tracking changes in the template.
28+
* @param additionalProps _Optional_. @see `ReactTemplateProps`.
29+
*/
30+
export function createReactTemplateElement<TContext extends object | void>(
31+
templateRef: TemplateRef<TContext>,
32+
context: TContext,
33+
ngZone: NgZone,
34+
additionalProps?: ReactTemplateProps
35+
) {
36+
return React.createElement(ReactTemplate, { ngZone, templateRef, context, ...additionalProps });
37+
}
38+
39+
/**
40+
* @internal
41+
*/
42+
interface InternalReactTemplateProps<TContext extends object | void> extends ReactTemplateProps {
43+
ngZone: NgZone;
44+
templateRef: TemplateRef<TContext>;
45+
context: TContext;
46+
}
47+
48+
/**
49+
* Render an `ng-template` as a child of a React component.
50+
* Supports two rendering modes:
51+
* 1. `legacy` - append `<react-content>` as the root, and nest the `children-to-append` underneath it.
52+
* 2. `new` (**default**) - append the `children-to-append` to the parent of this component, and hide the `<react-content>` element.
53+
* (similar to how `router-outlet` behaves in Angular).
54+
*/
55+
export class ReactTemplate<TContext extends object | void> extends React.Component<
56+
InternalReactTemplateProps<TContext>
57+
> {
58+
private _embeddedViewRef: EmbeddedViewRef<TContext>;
59+
private _ngZoneSubscription: Subscription;
60+
61+
componentDidUpdate() {
62+
// Context has changes, trigger change detection after pushing the new context in
63+
Object.assign(this._embeddedViewRef.context, this.props.context);
64+
this._embeddedViewRef.detectChanges();
65+
}
66+
67+
componentDidMount() {
68+
const { context, ngZone, templateRef } = this.props;
69+
70+
this._embeddedViewRef = templateRef.createEmbeddedView(context);
71+
const element = ReactDOM.findDOMNode(this);
72+
if (DEBUG) {
73+
console.warn('ReactTemplate Component > componentDidMount > childrenToAppend:', {
74+
rootNodes: this._embeddedViewRef.rootNodes,
75+
});
76+
}
77+
78+
const hostElement = this.props.legacyRenderMode ? element : element.parentElement;
79+
80+
this._embeddedViewRef.rootNodes.forEach(child => hostElement.appendChild(child));
81+
82+
// Detect the first cycle's changes, and then subscribe for subsequent ones
83+
this._embeddedViewRef.detectChanges();
84+
this._ngZoneSubscription = ngZone.onUnstable.subscribe(() => {
85+
this._embeddedViewRef.detectChanges();
86+
});
87+
}
88+
89+
componentWillUnmount() {
90+
this._ngZoneSubscription.unsubscribe();
91+
92+
if (this._embeddedViewRef) {
93+
this._embeddedViewRef.destroy();
94+
}
95+
}
96+
97+
render() {
98+
return React.createElement('react-template', !this.props.legacyRenderMode && { style: { display: 'none' } });
99+
}
100+
}

libs/core/src/lib/renderer/renderprop-helpers.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { ComponentRef, EmbeddedViewRef, TemplateRef } from '@angular/core';
4+
import { ComponentRef, NgZone, TemplateRef } from '@angular/core';
55
import { createReactContentElement, ReactContentProps } from '../renderer/react-content';
6+
import { createReactTemplateElement } from './react-template';
67

78
export interface RenderPropContext<TContext extends object> {
89
readonly render: (context: TContext) => JSX.Element;
@@ -16,28 +17,16 @@ function renderReactContent(rootNodes: HTMLElement[], additionalProps?: ReactCon
1617
* Wrap a `TemplateRef` with a `JSX.Element`.
1718
*
1819
* @param templateRef The template to wrap
20+
* @param ngZone A zone used for tracking & triggering updates to the template
1921
* @param additionalProps optional additional props to pass to the `ReactContent` object that will render the content.
2022
*/
2123
export function createTemplateRenderer<TContext extends object>(
2224
templateRef: TemplateRef<TContext>,
25+
ngZone: NgZone,
2326
additionalProps?: ReactContentProps
2427
): RenderPropContext<TContext> {
25-
let viewRef: EmbeddedViewRef<TContext> | null = null;
26-
let renderedJsx: JSX.Element | null = null;
27-
2828
return {
29-
render: (context: TContext) => {
30-
if (!viewRef) {
31-
viewRef = templateRef.createEmbeddedView(context);
32-
renderedJsx = renderReactContent(viewRef.rootNodes, additionalProps);
33-
} else {
34-
// Mutate the template's context
35-
Object.assign(viewRef.context, context);
36-
}
37-
viewRef.detectChanges();
38-
39-
return renderedJsx;
40-
},
29+
render: (context: TContext) => createReactTemplateElement(templateRef, context, ngZone, additionalProps),
4130
};
4231
}
4332

libs/core/src/public-api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export * from './lib/components/wrapper-component';
66
export * from './lib/declarations/public-api';
77
export * from './lib/renderer/components/Disguise';
88
export { getPassProps, passProp, PassProp } from './lib/renderer/pass-prop-decorator';
9-
export { ReactContent } from './lib/renderer/react-content';
9+
export { createReactContentElement, ReactContent, ReactContentProps } from './lib/renderer/react-content';
10+
export * from './lib/renderer/react-template';
1011
export { registerElement } from './lib/renderer/registry';

0 commit comments

Comments
 (0)