Skip to content

Commit 4bb71a8

Browse files
authored
Allow modifying props in custom render function. (#99)
Note that this is only a partial replacement for Fabric's pattern, since we still want to abstract away JSX from consumers, and Angular doesn't have first-class support for creating templates in TypeScript.
1 parent fe79f0f commit 4bb71a8

File tree

7 files changed

+162
-63
lines changed

7 files changed

+162
-63
lines changed

apps/demo/src/app/app.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ <h2>Getting up and running...</h2>
99
</ol>
1010
</div>
1111

12+
<fab-checkbox label="foo" [renderLabel]="renderCheckboxLabel"></fab-checkbox>
13+
1214
<div style="width:500px">
1315
<fab-message-bar>
1416
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Autem laboriosam id ad mollitia optio saepe qui aliquid

apps/demo/src/app/app.component.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
Selection,
77
DropdownMenuItemType,
88
IDropdownOption,
9+
ICheckboxProps,
910
} from 'office-ui-fabric-react';
11+
import { RenderPropOptions } from '@angular-react/core';
1012
import { FabDropdownComponent } from '@angular-react/fabric';
1113

1214
const suffix = ' cm';
@@ -18,6 +20,13 @@ const suffix = ' cm';
1820
encapsulation: ViewEncapsulation.None,
1921
})
2022
export class AppComponent {
23+
renderCheckboxLabel: RenderPropOptions<ICheckboxProps> = {
24+
getProps: defaultProps => ({
25+
...defaultProps,
26+
label: defaultProps.label.toUpperCase(),
27+
}),
28+
};
29+
2130
@ViewChild('customRange') customRangeTemplate: TemplateRef<{
2231
item: any;
2332
dismissMenu: (ev?: any, dismissAll?: boolean) => void;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { ComponentFactoryResolver, Type, Injector, TemplateRef, ComponentRef, NgZone } from '@angular/core';
2+
import {
3+
RenderPropContext,
4+
createTemplateRenderer,
5+
createComponentRenderer,
6+
createHtmlRenderer,
7+
isRenderPropContext,
8+
} from '../renderer/renderprop-helpers';
9+
import { ReactContentProps } from '../renderer/react-content';
10+
11+
export type JsxRenderFunc<TContext> = (context: TContext) => JSX.Element;
12+
13+
/**
14+
* Render props options for creating & rendering a component.
15+
*/
16+
export interface RenderComponentOptions<TContext extends object> {
17+
readonly componentType: Type<TContext>;
18+
readonly factoryResolver: ComponentFactoryResolver;
19+
readonly injector: Injector;
20+
}
21+
22+
function isRenderComponentOptions<TContext extends object>(x: unknown): x is RenderComponentOptions<TContext> {
23+
if (typeof x !== 'object') {
24+
return false;
25+
}
26+
27+
const maybeRenderComponentOptions = x as RenderComponentOptions<TContext>;
28+
return (
29+
maybeRenderComponentOptions.componentType != null &&
30+
maybeRenderComponentOptions.factoryResolver != null &&
31+
maybeRenderComponentOptions.injector != null
32+
);
33+
}
34+
35+
/**
36+
* Allow intercepting and modifying the default props, which are then used by the default renderer.
37+
*/
38+
export interface RenderPropOptions<TContext extends object> {
39+
readonly getProps: (defaultProps?: TContext) => TContext;
40+
}
41+
42+
function isRenderPropOptions<TContext extends object>(x: unknown): x is RenderPropOptions<TContext> {
43+
if (typeof x !== 'object') {
44+
return false;
45+
}
46+
47+
const maybeRenderPropOptions = x as RenderPropOptions<TContext>;
48+
return maybeRenderPropOptions.getProps && typeof maybeRenderPropOptions.getProps === 'function';
49+
}
50+
51+
/**
52+
* Various options for passing renderers as render props.
53+
*/
54+
export type InputRendererOptions<TContext extends object> =
55+
| TemplateRef<TContext>
56+
| ((context: TContext) => HTMLElement)
57+
| ComponentRef<TContext>
58+
| RenderComponentOptions<TContext>
59+
| RenderPropContext<TContext>
60+
| RenderPropOptions<TContext>;
61+
62+
export function createInputJsxRenderer<TContext extends object>(
63+
input: InputRendererOptions<TContext>,
64+
ngZone: NgZone,
65+
additionalProps?: ReactContentProps
66+
): JsxRenderFunc<TContext> | undefined {
67+
if (input instanceof TemplateRef) {
68+
const templateRenderer = createTemplateRenderer(input, ngZone, additionalProps);
69+
return (context: TContext) => templateRenderer.render(context);
70+
}
71+
72+
if (input instanceof ComponentRef) {
73+
const componentRenderer = createComponentRenderer(input, additionalProps);
74+
return (context: TContext) => componentRenderer.render(context);
75+
}
76+
77+
if (input instanceof Function) {
78+
const htmlRenderer = createHtmlRenderer(input, additionalProps);
79+
return (context: TContext) => htmlRenderer.render(context);
80+
}
81+
82+
if (isRenderComponentOptions(input)) {
83+
const { componentType, factoryResolver, injector } = input;
84+
const componentFactory = factoryResolver.resolveComponentFactory(componentType);
85+
const componentRef = componentFactory.create(injector);
86+
87+
// Call the function again with the created ComponentRef<TContext>
88+
return createInputJsxRenderer(componentRef, ngZone, additionalProps);
89+
}
90+
}
91+
92+
export function createRenderPropHandler<TProps extends object>(
93+
renderInputValue: InputRendererOptions<TProps>,
94+
ngZone: NgZone,
95+
options?: {
96+
jsxRenderer?: JsxRenderFunc<TProps>;
97+
additionalProps?: ReactContentProps;
98+
}
99+
): (props?: TProps, defaultRender?: JsxRenderFunc<TProps>) => JSX.Element | null {
100+
if (isRenderPropContext(renderInputValue)) {
101+
return renderInputValue.render;
102+
}
103+
104+
if (isRenderPropOptions(renderInputValue)) {
105+
return (props?: TProps, defaultRender?: JsxRenderFunc<TProps>) => {
106+
return typeof defaultRender === 'function' ? defaultRender(renderInputValue.getProps(props)) : null;
107+
};
108+
}
109+
110+
const renderer =
111+
(options && options.jsxRenderer) ||
112+
createInputJsxRenderer(renderInputValue, ngZone, options && options.additionalProps);
113+
114+
return (props?: TProps, defaultRender?: JsxRenderFunc<TProps>) => {
115+
if (!renderInputValue) {
116+
return typeof defaultRender === 'function' ? defaultRender(props) : null;
117+
}
118+
119+
return renderer(props);
120+
};
121+
}

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

Lines changed: 10 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,31 @@
55
import {
66
AfterViewInit,
77
ChangeDetectorRef,
8-
ComponentFactoryResolver,
9-
ComponentRef,
108
ElementRef,
11-
Injector,
129
Input,
1310
NgZone,
1411
OnChanges,
1512
Renderer2,
1613
SimpleChanges,
17-
TemplateRef,
18-
Type,
1914
AfterContentInit,
2015
} from '@angular/core';
2116
import classnames from 'classnames';
2217
import toStyle from 'css-to-style';
2318
import stylenames, { StyleObject } from 'stylenames';
19+
2420
import { Many } from '../declarations/many';
2521
import { ReactContentProps } from '../renderer/react-content';
2622
import { isReactNode } from '../renderer/react-node';
2723
import { isReactRendererData } from '../renderer/renderer';
28-
import { createComponentRenderer, createHtmlRenderer, createTemplateRenderer } from '../renderer/renderprop-helpers';
2924
import { toObject } from '../utils/object/to-object';
3025
import { afterRenderFinished } from '../utils/render/render-delay';
31-
import { unreachable } from '../utils/types/unreachable';
26+
import { InputRendererOptions, JsxRenderFunc, createInputJsxRenderer, createRenderPropHandler } from './render-props';
3227

3328
// Forbidden attributes are still ignored, since they may be set from the wrapper components themselves (forbidden is only applied for users of the wrapper components)
3429
const ignoredAttributeMatchers = [/^_?ng-?.*/, /^style$/, /^class$/];
3530

3631
const ngClassRegExp = /^ng-/;
3732

38-
export interface RenderComponentOptions<TContext extends object> {
39-
readonly componentType: Type<TContext>;
40-
readonly factoryResolver: ComponentFactoryResolver;
41-
readonly injector: Injector;
42-
}
43-
44-
export type InputRendererOptions<TContext extends object> =
45-
| TemplateRef<TContext>
46-
| ((context: TContext) => HTMLElement)
47-
| ComponentRef<TContext>
48-
| RenderComponentOptions<TContext>;
49-
50-
export type JsxRenderFunc<TContext> = (context: TContext) => JSX.Element;
51-
5233
export type ContentClassValue = string[] | Set<string> | { [klass: string]: any };
5334
export type ContentStyleValue = string | StyleObject;
5435

@@ -186,7 +167,7 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterC
186167

187168
/**
188169
* Create an JSX renderer for an `@Input` property.
189-
* @param input The input property
170+
* @param input The input property.
190171
* @param additionalProps optional additional props to pass to the `ReactContent` object that will render the content.
191172
*/
192173
protected createInputJsxRenderer<TContext extends object>(
@@ -201,31 +182,7 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterC
201182
throw new Error('To create an input JSX renderer you must pass an NgZone to the constructor.');
202183
}
203184

204-
if (input instanceof TemplateRef) {
205-
const templateRenderer = createTemplateRenderer(input, this._ngZone, additionalProps);
206-
return (context: TContext) => templateRenderer.render(context);
207-
}
208-
209-
if (input instanceof ComponentRef) {
210-
const componentRenderer = createComponentRenderer(input, additionalProps);
211-
return (context: TContext) => componentRenderer.render(context);
212-
}
213-
214-
if (input instanceof Function) {
215-
const htmlRenderer = createHtmlRenderer(input, additionalProps);
216-
return (context: TContext) => htmlRenderer.render(context);
217-
}
218-
219-
if (typeof input === 'object') {
220-
const { componentType, factoryResolver, injector } = input;
221-
const componentFactory = factoryResolver.resolveComponentFactory(componentType);
222-
const componentRef = componentFactory.create(injector);
223-
224-
// Call the function again with the created ComponentRef<TContext>
225-
return this.createInputJsxRenderer(componentRef, additionalProps);
226-
}
227-
228-
unreachable(input);
185+
return createInputJsxRenderer(input, this._ngZone, additionalProps);
229186
}
230187

231188
/**
@@ -234,24 +191,14 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterC
234191
* @param jsxRenderer an optional renderer to use.
235192
* @param additionalProps optional additional props to pass to the `ReactContent` object that will render the content.
236193
*/
237-
protected createRenderPropHandler<TProps extends object>(
238-
renderInputValue: InputRendererOptions<TProps>,
194+
protected createRenderPropHandler<TRenderProps extends object>(
195+
renderInputValue: InputRendererOptions<TRenderProps>,
239196
options?: {
240-
jsxRenderer?: JsxRenderFunc<TProps>;
197+
jsxRenderer?: JsxRenderFunc<TRenderProps>;
241198
additionalProps?: ReactContentProps;
242199
}
243-
): (props?: TProps, defaultRender?: JsxRenderFunc<TProps>) => JSX.Element | null {
244-
const renderer =
245-
(options && options.jsxRenderer) ||
246-
this.createInputJsxRenderer(renderInputValue, options && options.additionalProps);
247-
248-
return (props?: TProps, defaultRender?: JsxRenderFunc<TProps>) => {
249-
if (!renderInputValue) {
250-
return typeof defaultRender === 'function' ? defaultRender(props) : null;
251-
}
252-
253-
return renderer(props);
254-
};
200+
): (props?: TRenderProps, defaultRender?: JsxRenderFunc<TRenderProps>) => JSX.Element | null {
201+
return createRenderPropHandler(renderInputValue, this._ngZone, options);
255202
}
256203

257204
private _passAttributesAsProps() {
@@ -300,7 +247,7 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterC
300247
}
301248

302249
private _setHostDisplay() {
303-
const nativeElement: HTMLElement = this.elementRef.nativeElement;
250+
const nativeElement = this.elementRef.nativeElement;
304251

305252
// We want to wait until child elements are rendered
306253
afterRenderFinished(() => {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ export interface RenderPropContext<TContext extends object> {
99
readonly render: (context: TContext) => JSX.Element;
1010
}
1111

12+
export function isRenderPropContext<TContext extends object>(x: unknown): x is RenderPropContext<TContext> {
13+
if (typeof x !== 'object') {
14+
return false;
15+
}
16+
17+
const maybeRenderPropContext = x as RenderPropContext<TContext>;
18+
return maybeRenderPropContext.render && typeof maybeRenderPropContext.render === 'function';
19+
}
20+
1221
function renderReactContent(rootNodes: HTMLElement[], additionalProps?: ReactContentProps): JSX.Element {
1322
return createReactContentElement(rootNodes, additionalProps);
1423
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
/**
5+
* Delays the execution of a function to be after the next render.
6+
*
7+
* @param callback The function to execute
8+
*/
49
export const afterRenderFinished = (callback: Function) => {
510
setTimeout(callback, 0);
611
};

libs/core/src/public-api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@ export { getPassProps, passProp, PassProp } from './lib/renderer/pass-prop-decor
99
export { createReactContentElement, ReactContent, ReactContentProps } from './lib/renderer/react-content';
1010
export * from './lib/renderer/react-template';
1111
export { registerElement } from './lib/renderer/registry';
12+
export {
13+
JsxRenderFunc,
14+
RenderComponentOptions,
15+
InputRendererOptions,
16+
RenderPropOptions,
17+
} from './lib/components/render-props';

0 commit comments

Comments
 (0)