Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/demo/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ <h2>Getting up and running...</h2>
</ol>
</div>

<fab-checkbox label="foo" [renderLabel]="renderCheckboxLabel"></fab-checkbox>

<div style="width:500px">
<fab-message-bar>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Autem laboriosam id ad mollitia optio saepe qui aliquid
Expand Down
9 changes: 9 additions & 0 deletions apps/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,6 +20,13 @@ const suffix = ' cm';
encapsulation: ViewEncapsulation.None,
})
export class AppComponent {
renderCheckboxLabel: RenderPropOptions<ICheckboxProps> = {
getProps: defaultProps => ({
...defaultProps,
label: defaultProps.label.toUpperCase(),
}),
};

@ViewChild('customRange') customRangeTemplate: TemplateRef<{
item: any;
dismissMenu: (ev?: any, dismissAll?: boolean) => void;
Expand Down
121 changes: 121 additions & 0 deletions libs/core/src/lib/components/render-props.ts
Original file line number Diff line number Diff line change
@@ -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<TContext> = (context: TContext) => JSX.Element;

/**
* Render props options for creating & rendering a component.
*/
export interface RenderComponentOptions<TContext extends object> {
readonly componentType: Type<TContext>;
readonly factoryResolver: ComponentFactoryResolver;
readonly injector: Injector;
}

function isRenderComponentOptions<TContext extends object>(x: unknown): x is RenderComponentOptions<TContext> {
if (typeof x !== 'object') {
return false;
}

const maybeRenderComponentOptions = x as RenderComponentOptions<TContext>;
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<TContext extends object> {
readonly getProps: (defaultProps?: TContext) => TContext;
}

function isRenderPropOptions<TContext extends object>(x: unknown): x is RenderPropOptions<TContext> {
if (typeof x !== 'object') {
return false;
}

const maybeRenderPropOptions = x as RenderPropOptions<TContext>;
return maybeRenderPropOptions.getProps && typeof maybeRenderPropOptions.getProps === 'function';
}

/**
* Various options for passing renderers as render props.
*/
export type InputRendererOptions<TContext extends object> =
| TemplateRef<TContext>
| ((context: TContext) => HTMLElement)
| ComponentRef<TContext>
| RenderComponentOptions<TContext>
| RenderPropContext<TContext>
| RenderPropOptions<TContext>;

export function createInputJsxRenderer<TContext extends object>(
input: InputRendererOptions<TContext>,
ngZone: NgZone,
additionalProps?: ReactContentProps
): JsxRenderFunc<TContext> | 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<TContext>
return createInputJsxRenderer(componentRef, ngZone, additionalProps);
}
}

export function createRenderPropHandler<TProps extends object>(
renderInputValue: InputRendererOptions<TProps>,
ngZone: NgZone,
options?: {
jsxRenderer?: JsxRenderFunc<TProps>;
additionalProps?: ReactContentProps;
}
): (props?: TProps, defaultRender?: JsxRenderFunc<TProps>) => JSX.Element | null {
if (isRenderPropContext(renderInputValue)) {
return renderInputValue.render;
}

if (isRenderPropOptions(renderInputValue)) {
return (props?: TProps, defaultRender?: JsxRenderFunc<TProps>) => {
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<TProps>) => {
if (!renderInputValue) {
return typeof defaultRender === 'function' ? defaultRender(props) : null;
}

return renderer(props);
};
}
73 changes: 10 additions & 63 deletions libs/core/src/lib/components/wrapper-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TContext extends object> {
readonly componentType: Type<TContext>;
readonly factoryResolver: ComponentFactoryResolver;
readonly injector: Injector;
}

export type InputRendererOptions<TContext extends object> =
| TemplateRef<TContext>
| ((context: TContext) => HTMLElement)
| ComponentRef<TContext>
| RenderComponentOptions<TContext>;

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

export type ContentClassValue = string[] | Set<string> | { [klass: string]: any };
export type ContentStyleValue = string | StyleObject;

Expand Down Expand Up @@ -186,7 +167,7 @@ export abstract class ReactWrapperComponent<TProps extends {}> 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<TContext extends object>(
Expand All @@ -201,31 +182,7 @@ export abstract class ReactWrapperComponent<TProps extends {}> 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<TContext>
return this.createInputJsxRenderer(componentRef, additionalProps);
}

unreachable(input);
return createInputJsxRenderer(input, this._ngZone, additionalProps);
}

/**
Expand All @@ -234,24 +191,14 @@ export abstract class ReactWrapperComponent<TProps extends {}> 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<TProps extends object>(
renderInputValue: InputRendererOptions<TProps>,
protected createRenderPropHandler<TRenderProps extends object>(
renderInputValue: InputRendererOptions<TRenderProps>,
options?: {
jsxRenderer?: JsxRenderFunc<TProps>;
jsxRenderer?: JsxRenderFunc<TRenderProps>;
additionalProps?: ReactContentProps;
}
): (props?: TProps, defaultRender?: JsxRenderFunc<TProps>) => JSX.Element | null {
const renderer =
(options && options.jsxRenderer) ||
this.createInputJsxRenderer(renderInputValue, options && options.additionalProps);

return (props?: TProps, defaultRender?: JsxRenderFunc<TProps>) => {
if (!renderInputValue) {
return typeof defaultRender === 'function' ? defaultRender(props) : null;
}

return renderer(props);
};
): (props?: TRenderProps, defaultRender?: JsxRenderFunc<TRenderProps>) => JSX.Element | null {
return createRenderPropHandler(renderInputValue, this._ngZone, options);
}

private _passAttributesAsProps() {
Expand Down Expand Up @@ -300,7 +247,7 @@ export abstract class ReactWrapperComponent<TProps extends {}> 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(() => {
Expand Down
9 changes: 9 additions & 0 deletions libs/core/src/lib/renderer/renderprop-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ export interface RenderPropContext<TContext extends object> {
readonly render: (context: TContext) => JSX.Element;
}

export function isRenderPropContext<TContext extends object>(x: unknown): x is RenderPropContext<TContext> {
if (typeof x !== 'object') {
return false;
}

const maybeRenderPropContext = x as RenderPropContext<TContext>;
return maybeRenderPropContext.render && typeof maybeRenderPropContext.render === 'function';
}

function renderReactContent(rootNodes: HTMLElement[], additionalProps?: ReactContentProps): JSX.Element {
return createReactContentElement(rootNodes, additionalProps);
}
Expand Down
5 changes: 5 additions & 0 deletions libs/core/src/lib/utils/render/render-delay.ts
Original file line number Diff line number Diff line change
@@ -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);
};
6 changes: 6 additions & 0 deletions libs/core/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';