diff --git a/apps/demo/src/app/app.component.html b/apps/demo/src/app/app.component.html index bae81dda..9764fe9c 100644 --- a/apps/demo/src/app/app.component.html +++ b/apps/demo/src/app/app.component.html @@ -9,6 +9,8 @@

Getting up and running...

+ +
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Autem laboriosam id ad mollitia optio saepe qui aliquid diff --git a/apps/demo/src/app/app.component.ts b/apps/demo/src/app/app.component.ts index 28934762..a9d86a57 100644 --- a/apps/demo/src/app/app.component.ts +++ b/apps/demo/src/app/app.component.ts @@ -6,7 +6,9 @@ import { Selection, DropdownMenuItemType, IDropdownOption, + ICheckboxProps, } from 'office-ui-fabric-react'; +import { RenderPropOptions } from '@angular-react/core'; import { FabDropdownComponent } from '@angular-react/fabric'; const suffix = ' cm'; @@ -18,6 +20,13 @@ const suffix = ' cm'; encapsulation: ViewEncapsulation.None, }) export class AppComponent { + renderCheckboxLabel: RenderPropOptions = { + getProps: defaultProps => ({ + ...defaultProps, + label: defaultProps.label.toUpperCase(), + }), + }; + @ViewChild('customRange') customRangeTemplate: TemplateRef<{ item: any; dismissMenu: (ev?: any, dismissAll?: boolean) => void; diff --git a/libs/core/src/lib/components/render-props.ts b/libs/core/src/lib/components/render-props.ts new file mode 100644 index 00000000..70f2c1d8 --- /dev/null +++ b/libs/core/src/lib/components/render-props.ts @@ -0,0 +1,121 @@ +import { ComponentFactoryResolver, Type, Injector, TemplateRef, ComponentRef, NgZone } from '@angular/core'; +import { + RenderPropContext, + createTemplateRenderer, + createComponentRenderer, + createHtmlRenderer, + isRenderPropContext, +} from '../renderer/renderprop-helpers'; +import { ReactContentProps } from '../renderer/react-content'; + +export type JsxRenderFunc = (context: TContext) => JSX.Element; + +/** + * Render props options for creating & rendering a component. + */ +export interface RenderComponentOptions { + readonly componentType: Type; + readonly factoryResolver: ComponentFactoryResolver; + readonly injector: Injector; +} + +function isRenderComponentOptions(x: unknown): x is RenderComponentOptions { + if (typeof x !== 'object') { + return false; + } + + const maybeRenderComponentOptions = x as RenderComponentOptions; + return ( + maybeRenderComponentOptions.componentType != null && + maybeRenderComponentOptions.factoryResolver != null && + maybeRenderComponentOptions.injector != null + ); +} + +/** + * Allow intercepting and modifying the default props, which are then used by the default renderer. + */ +export interface RenderPropOptions { + readonly getProps: (defaultProps?: TContext) => TContext; +} + +function isRenderPropOptions(x: unknown): x is RenderPropOptions { + if (typeof x !== 'object') { + return false; + } + + const maybeRenderPropOptions = x as RenderPropOptions; + return maybeRenderPropOptions.getProps && typeof maybeRenderPropOptions.getProps === 'function'; +} + +/** + * Various options for passing renderers as render props. + */ +export type InputRendererOptions = + | TemplateRef + | ((context: TContext) => HTMLElement) + | ComponentRef + | RenderComponentOptions + | RenderPropContext + | RenderPropOptions; + +export function createInputJsxRenderer( + input: InputRendererOptions, + ngZone: NgZone, + additionalProps?: ReactContentProps +): JsxRenderFunc | undefined { + if (input instanceof TemplateRef) { + const templateRenderer = createTemplateRenderer(input, ngZone, additionalProps); + return (context: TContext) => templateRenderer.render(context); + } + + if (input instanceof ComponentRef) { + const componentRenderer = createComponentRenderer(input, additionalProps); + return (context: TContext) => componentRenderer.render(context); + } + + if (input instanceof Function) { + const htmlRenderer = createHtmlRenderer(input, additionalProps); + return (context: TContext) => htmlRenderer.render(context); + } + + if (isRenderComponentOptions(input)) { + const { componentType, factoryResolver, injector } = input; + const componentFactory = factoryResolver.resolveComponentFactory(componentType); + const componentRef = componentFactory.create(injector); + + // Call the function again with the created ComponentRef + return createInputJsxRenderer(componentRef, ngZone, additionalProps); + } +} + +export function createRenderPropHandler( + renderInputValue: InputRendererOptions, + ngZone: NgZone, + options?: { + jsxRenderer?: JsxRenderFunc; + additionalProps?: ReactContentProps; + } +): (props?: TProps, defaultRender?: JsxRenderFunc) => JSX.Element | null { + if (isRenderPropContext(renderInputValue)) { + return renderInputValue.render; + } + + if (isRenderPropOptions(renderInputValue)) { + return (props?: TProps, defaultRender?: JsxRenderFunc) => { + return typeof defaultRender === 'function' ? defaultRender(renderInputValue.getProps(props)) : null; + }; + } + + const renderer = + (options && options.jsxRenderer) || + createInputJsxRenderer(renderInputValue, ngZone, options && options.additionalProps); + + return (props?: TProps, defaultRender?: JsxRenderFunc) => { + if (!renderInputValue) { + return typeof defaultRender === 'function' ? defaultRender(props) : null; + } + + return renderer(props); + }; +} diff --git a/libs/core/src/lib/components/wrapper-component.ts b/libs/core/src/lib/components/wrapper-component.ts index 8e05de0d..bc97cf1c 100644 --- a/libs/core/src/lib/components/wrapper-component.ts +++ b/libs/core/src/lib/components/wrapper-component.ts @@ -5,50 +5,31 @@ import { AfterViewInit, ChangeDetectorRef, - ComponentFactoryResolver, - ComponentRef, ElementRef, - Injector, Input, NgZone, OnChanges, Renderer2, SimpleChanges, - TemplateRef, - Type, AfterContentInit, } from '@angular/core'; import classnames from 'classnames'; import toStyle from 'css-to-style'; import stylenames, { StyleObject } from 'stylenames'; + import { Many } from '../declarations/many'; import { ReactContentProps } from '../renderer/react-content'; import { isReactNode } from '../renderer/react-node'; import { isReactRendererData } from '../renderer/renderer'; -import { createComponentRenderer, createHtmlRenderer, createTemplateRenderer } from '../renderer/renderprop-helpers'; import { toObject } from '../utils/object/to-object'; import { afterRenderFinished } from '../utils/render/render-delay'; -import { unreachable } from '../utils/types/unreachable'; +import { InputRendererOptions, JsxRenderFunc, createInputJsxRenderer, createRenderPropHandler } from './render-props'; // 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) const ignoredAttributeMatchers = [/^_?ng-?.*/, /^style$/, /^class$/]; const ngClassRegExp = /^ng-/; -export interface RenderComponentOptions { - readonly componentType: Type; - readonly factoryResolver: ComponentFactoryResolver; - readonly injector: Injector; -} - -export type InputRendererOptions = - | TemplateRef - | ((context: TContext) => HTMLElement) - | ComponentRef - | RenderComponentOptions; - -export type JsxRenderFunc = (context: TContext) => JSX.Element; - export type ContentClassValue = string[] | Set | { [klass: string]: any }; export type ContentStyleValue = string | StyleObject; @@ -186,7 +167,7 @@ export abstract class ReactWrapperComponent implements AfterC /** * Create an JSX renderer for an `@Input` property. - * @param input The input property + * @param input The input property. * @param additionalProps optional additional props to pass to the `ReactContent` object that will render the content. */ protected createInputJsxRenderer( @@ -201,31 +182,7 @@ export abstract class ReactWrapperComponent implements AfterC throw new Error('To create an input JSX renderer you must pass an NgZone to the constructor.'); } - if (input instanceof TemplateRef) { - const templateRenderer = createTemplateRenderer(input, this._ngZone, additionalProps); - return (context: TContext) => templateRenderer.render(context); - } - - if (input instanceof ComponentRef) { - const componentRenderer = createComponentRenderer(input, additionalProps); - return (context: TContext) => componentRenderer.render(context); - } - - if (input instanceof Function) { - const htmlRenderer = createHtmlRenderer(input, additionalProps); - return (context: TContext) => htmlRenderer.render(context); - } - - if (typeof input === 'object') { - const { componentType, factoryResolver, injector } = input; - const componentFactory = factoryResolver.resolveComponentFactory(componentType); - const componentRef = componentFactory.create(injector); - - // Call the function again with the created ComponentRef - return this.createInputJsxRenderer(componentRef, additionalProps); - } - - unreachable(input); + return createInputJsxRenderer(input, this._ngZone, additionalProps); } /** @@ -234,24 +191,14 @@ export abstract class ReactWrapperComponent implements AfterC * @param jsxRenderer an optional renderer to use. * @param additionalProps optional additional props to pass to the `ReactContent` object that will render the content. */ - protected createRenderPropHandler( - renderInputValue: InputRendererOptions, + protected createRenderPropHandler( + renderInputValue: InputRendererOptions, options?: { - jsxRenderer?: JsxRenderFunc; + jsxRenderer?: JsxRenderFunc; additionalProps?: ReactContentProps; } - ): (props?: TProps, defaultRender?: JsxRenderFunc) => JSX.Element | null { - const renderer = - (options && options.jsxRenderer) || - this.createInputJsxRenderer(renderInputValue, options && options.additionalProps); - - return (props?: TProps, defaultRender?: JsxRenderFunc) => { - if (!renderInputValue) { - return typeof defaultRender === 'function' ? defaultRender(props) : null; - } - - return renderer(props); - }; + ): (props?: TRenderProps, defaultRender?: JsxRenderFunc) => JSX.Element | null { + return createRenderPropHandler(renderInputValue, this._ngZone, options); } private _passAttributesAsProps() { @@ -300,7 +247,7 @@ export abstract class ReactWrapperComponent implements AfterC } private _setHostDisplay() { - const nativeElement: HTMLElement = this.elementRef.nativeElement; + const nativeElement = this.elementRef.nativeElement; // We want to wait until child elements are rendered afterRenderFinished(() => { diff --git a/libs/core/src/lib/renderer/renderprop-helpers.ts b/libs/core/src/lib/renderer/renderprop-helpers.ts index ac069387..f2bab2bc 100644 --- a/libs/core/src/lib/renderer/renderprop-helpers.ts +++ b/libs/core/src/lib/renderer/renderprop-helpers.ts @@ -9,6 +9,15 @@ export interface RenderPropContext { readonly render: (context: TContext) => JSX.Element; } +export function isRenderPropContext(x: unknown): x is RenderPropContext { + if (typeof x !== 'object') { + return false; + } + + const maybeRenderPropContext = x as RenderPropContext; + return maybeRenderPropContext.render && typeof maybeRenderPropContext.render === 'function'; +} + function renderReactContent(rootNodes: HTMLElement[], additionalProps?: ReactContentProps): JSX.Element { return createReactContentElement(rootNodes, additionalProps); } diff --git a/libs/core/src/lib/utils/render/render-delay.ts b/libs/core/src/lib/utils/render/render-delay.ts index 69481e4c..e77936f0 100644 --- a/libs/core/src/lib/utils/render/render-delay.ts +++ b/libs/core/src/lib/utils/render/render-delay.ts @@ -1,6 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/** + * Delays the execution of a function to be after the next render. + * + * @param callback The function to execute + */ export const afterRenderFinished = (callback: Function) => { setTimeout(callback, 0); }; diff --git a/libs/core/src/public-api.ts b/libs/core/src/public-api.ts index a5387bde..63b74187 100644 --- a/libs/core/src/public-api.ts +++ b/libs/core/src/public-api.ts @@ -9,3 +9,9 @@ export { getPassProps, passProp, PassProp } from './lib/renderer/pass-prop-decor export { createReactContentElement, ReactContent, ReactContentProps } from './lib/renderer/react-content'; export * from './lib/renderer/react-template'; export { registerElement } from './lib/renderer/registry'; +export { + JsxRenderFunc, + RenderComponentOptions, + InputRendererOptions, + RenderPropOptions, +} from './lib/components/render-props';