Skip to content

Commit 35fe590

Browse files
author
Ben Grynhaus
committed
Change createTemplateRenderer to accept an NgZone to allow tracking changes
1 parent 59ca73a commit 35fe590

File tree

4 files changed

+111
-17
lines changed

4 files changed

+111
-17
lines changed

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ElementRef,
1010
Injector,
1111
Input,
12+
NgZone,
1213
OnChanges,
1314
Renderer2,
1415
SimpleChanges,
@@ -42,6 +43,15 @@ export type InputRendererOptions<TContext extends object> =
4243

4344
export type JsxRenderFunc<TContext> = (context: TContext) => JSX.Element;
4445

46+
export interface WrapperComponentOptions {
47+
readonly setHostDisplay?: boolean;
48+
readonly ngZone?: NgZone;
49+
}
50+
51+
const defaultWrapperComponentOptions: WrapperComponentOptions = {
52+
setHostDisplay: false,
53+
};
54+
4555
/**
4656
* Base class for Angular @Components wrapping React Components.
4757
* Simplifies some of the handling around passing down props and setting CSS on the host component.
@@ -51,6 +61,9 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
5161
private _contentClass: string;
5262
private _contentStyle: string;
5363

64+
private ngZone: NgZone;
65+
private setHostDisplay: boolean;
66+
5467
protected abstract reactNodeRef: ElementRef<HTMLElement>;
5568

5669
/**
@@ -100,8 +113,11 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
100113
public readonly elementRef: ElementRef,
101114
private readonly changeDetectorRef: ChangeDetectorRef,
102115
private readonly renderer: Renderer2,
103-
private readonly setHostDisplay: boolean = false
104-
) {}
116+
{ setHostDisplay, ngZone }: WrapperComponentOptions = defaultWrapperComponentOptions
117+
) {
118+
this.ngZone = ngZone;
119+
this.setHostDisplay = setHostDisplay;
120+
}
105121

106122
ngAfterViewInit() {
107123
this._passAttributesAsProps();
@@ -154,8 +170,12 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
154170
return undefined;
155171
}
156172

173+
if (!this.ngZone) {
174+
throw new Error('To create an input JSX renderer you must pass an NgZone to the constructor.');
175+
}
176+
157177
if (input instanceof TemplateRef) {
158-
const templateRenderer = createTemplateRenderer(input, additionalProps);
178+
const templateRenderer = createTemplateRenderer(input, this.ngZone, additionalProps);
159179
return (context: TContext) => templateRenderer.render(context);
160180
}
161181

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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<TContext extends object | void> {
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+
ngZone: NgZone;
23+
templateRef: TemplateRef<TContext>;
24+
context: TContext;
25+
}
26+
27+
/**
28+
* Render an `ng-template` as a child of a React component.
29+
* Supports two rendering modes:
30+
* 1. `legacy` - append `<react-content>` as the root, and nest the `children-to-append` underneath it.
31+
* 2. `new` (**default**) - append the `children-to-append` to the parent of this component, and hide the `<react-content>` element.
32+
* (similar to how `router-outlet` behaves in Angular).
33+
*/
34+
export class ReactTemplate<TContext extends object | void> extends React.Component<ReactTemplateProps<TContext>> {
35+
private _embeddedViewRef: EmbeddedViewRef<TContext>;
36+
private _ngZoneSubscription: Subscription;
37+
38+
componentDidUpdate() {
39+
// Context has changes, trigger change detection after pushing the new context in
40+
Object.assign(this._embeddedViewRef.context, this.props.context);
41+
this._embeddedViewRef.detectChanges();
42+
}
43+
44+
componentDidMount() {
45+
const { context, ngZone, templateRef } = this.props;
46+
47+
this._embeddedViewRef = templateRef.createEmbeddedView(context);
48+
const element = ReactDOM.findDOMNode(this);
49+
if (DEBUG) {
50+
console.warn('ReactTemplate Component > componentDidMount > childrenToAppend:', {
51+
rootNodes: this._embeddedViewRef.rootNodes,
52+
});
53+
}
54+
55+
const hostElement = this.props.legacyRenderMode ? element : element.parentElement;
56+
57+
this._embeddedViewRef.rootNodes.forEach(child => hostElement.appendChild(child));
58+
59+
// Detect the first cycle's changes, and then subscribe for subsequent ones
60+
this._embeddedViewRef.detectChanges();
61+
this._ngZoneSubscription = ngZone.onUnstable.subscribe(() => {
62+
this._embeddedViewRef.detectChanges();
63+
});
64+
}
65+
66+
componentWillUnmount() {
67+
this._ngZoneSubscription.unsubscribe();
68+
69+
if (this._embeddedViewRef) {
70+
this._embeddedViewRef.destroy();
71+
}
72+
}
73+
74+
render() {
75+
return React.createElement('react-template', !this.props.legacyRenderMode && { style: { display: 'none' } });
76+
}
77+
}

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

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
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 * as React from 'react';
66
import { CHILDREN_TO_APPEND_PROP, ReactContent, ReactContentProps } from '../renderer/react-content';
7+
import { ReactTemplate } from './react-template';
78

89
export interface RenderPropContext<TContext extends object> {
910
readonly render: (context: TContext) => JSX.Element;
@@ -20,27 +21,22 @@ function renderReactContent(rootNodes: HTMLElement[], additionalProps?: ReactCon
2021
* Wrap a `TemplateRef` with a `JSX.Element`.
2122
*
2223
* @param templateRef The template to wrap
24+
* @param ngZone The NgZone - used to tracking & triggering updates to the template
2325
* @param additionalProps optional additional props to pass to the `ReactContent` object that will render the content.
2426
*/
2527
export function createTemplateRenderer<TContext extends object>(
2628
templateRef: TemplateRef<TContext>,
29+
ngZone: NgZone,
2730
additionalProps?: ReactContentProps
2831
): RenderPropContext<TContext> {
29-
let viewRef: EmbeddedViewRef<TContext> | null = null;
30-
let renderedJsx: JSX.Element | null = null;
31-
3232
return {
3333
render: (context: TContext) => {
34-
if (!viewRef) {
35-
viewRef = templateRef.createEmbeddedView(context);
36-
renderedJsx = renderReactContent(viewRef.rootNodes, additionalProps);
37-
} else {
38-
// Mutate the template's context
39-
Object.assign(viewRef.context, context);
40-
}
41-
viewRef.detectChanges();
42-
43-
return renderedJsx;
34+
return React.createElement(ReactTemplate, {
35+
ngZone,
36+
templateRef,
37+
context,
38+
...additionalProps,
39+
});
4440
},
4541
};
4642
}

libs/core/src/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export * from './lib/declarations/public-api';
77
export * from './lib/renderer/components/Disguise';
88
export { getPassProps, passProp, PassProp } from './lib/renderer/pass-prop-decorator';
99
export { ReactContent } from './lib/renderer/react-content';
10+
export { ReactTemplate } from './lib/renderer/react-template';
1011
export { registerElement } from './lib/renderer/registry';

0 commit comments

Comments
 (0)