Skip to content
Merged
43 changes: 25 additions & 18 deletions apps/demo/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,31 @@ <h2>Getting up and running...</h2>
</div>

<div>
<fab-pivot>
<fab-pivot-item headerText="Tab 1">
<div>Tab 1's content</div>
</fab-pivot-item>
<fab-pivot-item headerText="Tab 2">
<div>Tab 2's content</div>
</fab-pivot-item>
<fab-pivot-item headerText="Tab 3">
<div>Tab 3's content</div>
</fab-pivot-item>
</fab-pivot>

<fab-command-bar>
<items>
<fab-command-bar-item key="run" text="Run" [iconProps]="{ iconName: 'CaretRight' }" [disabled]="runDisabled"></fab-command-bar-item>
<fab-command-bar-item key="new" text="New" [iconProps]="{ iconName: 'Add' }" (click)="onNewClicked()"></fab-command-bar-item>
<fab-command-bar-item key="save" text="Save" [iconProps]="{ iconName: 'Save' }" [subMenuProps]="saveSubMenuProps">
<contextual-menu-item *ngIf="runDisabled" key="sometimesVisible" text="woosh"></contextual-menu-item>
<contextual-menu-item key="save" text="Save"></contextual-menu-item>
<contextual-menu-item key="save-as" text="Save As" (click)="onSaveAsClicked()">
<contextual-menu-item key="save-as-1" text="Save As 1" (click)="onSaveAsFirstClicked()"></contextual-menu-item>
<contextual-menu-item key="save-as-2" text="Save As 2" (click)="onSaveAsSecondClicked()"></contextual-menu-item>
</contextual-menu-item>
</fab-command-bar-item>
<fab-command-bar-item key="copy" text="Copy" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
<fab-command-bar-item key="copy1" text="Copy1" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
<fab-command-bar-item key="copy2" text="Copy2" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
<fab-command-bar-item key="copy3" text="Copy3" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
<fab-command-bar-item key="copy4" text="Copy4" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
<fab-command-bar-item key="copy5" text="Copy5" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
<fab-command-bar-item key="copy6" text="Copy6" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
<fab-command-bar-item key="copy7" text="Copy7" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
<fab-command-bar-item key="copy8" text="Copy8" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
<fab-command-bar-item key="copy9" text="Copy9" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
<fab-command-bar-item key="custom" text="custom text" (click)="onCopyClicked()">
<render>
<ng-template let-item="item">
Expand All @@ -37,10 +49,10 @@ <h2>Getting up and running...</h2>
</render>

<!-- <render-icon>
<ng-template let-contextualMenuItemProps="contextualMenuItemProps">
<div>custom icon</div>
</ng-template>
</render-icon> -->
<ng-template let-contextualMenuItemProps="contextualMenuItemProps">
<div>custom icon</div>
</ng-template>
</render-icon> -->
</fab-command-bar-item>
<fab-command-bar-item *ngIf="runDisabled" key="sometimesVisible" text="woosh"></fab-command-bar-item>
</items>
Expand All @@ -50,9 +62,4 @@ <h2>Getting up and running...</h2>
<fab-command-bar-item key="full-screen" [iconOnly]="true" [iconProps]="{ iconName: fullScreenIcon }" (click)="toggleFullScreen()"></fab-command-bar-item>
</far-items>
</fab-command-bar>

<fab-default-button (onClick)="toggleRun()" text="Toggle run"></fab-default-button>

<fab-panel [isOpen]="isPanelOpen" (onDismiss)="isPanelOpen = false">
</fab-panel>
</div>
29 changes: 23 additions & 6 deletions libs/core/src/lib/components/wrapper-component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { AfterViewInit, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, Input, OnChanges, Renderer2, SimpleChanges, TemplateRef, Type } from '@angular/core';
import {
AfterViewInit,
ChangeDetectorRef,
ComponentFactoryResolver,
ComponentRef,
ElementRef,
Injector,
Input,
OnChanges,
Renderer2,
SimpleChanges,
TemplateRef,
Type,
} from '@angular/core';
import toStyle from 'css-to-style';
import { ReactContentProps } from '../renderer/react-content';
import { isReactNode } from '../renderer/react-node';
import { isReactRendererData } from '../renderer/renderer';
import { renderComponent, renderFunc, renderTemplate } from '../renderer/renderprop-helpers';
import { createComponentRenderer, createHtmlRenderer, createTemplateRenderer } from '../renderer/renderprop-helpers';
import { afterRenderFinished } from '../utils/render/render-delay';
import { unreachable } from '../utils/types/unreachable';

Expand Down Expand Up @@ -142,23 +155,27 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
}

if (input instanceof TemplateRef) {
return (context: TContext) => renderTemplate(input, context, additionalProps);
const templateRenderer = createTemplateRenderer(input, additionalProps);
return (context: TContext) => templateRenderer.render(context);
}

if (input instanceof ComponentRef) {
return (context: TContext) => renderComponent(input, context, additionalProps);
const componentRenderer = createComponentRenderer(input, additionalProps);
return (context: TContext) => componentRenderer.render(context);
}

if (input instanceof Function) {
return (context: TContext) => renderFunc(input, context, additionalProps);
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);

return (context: TContext) => renderComponent(componentRef, context, additionalProps);
// Call the function again with the created ComponentRef<TContext>
return this.createInputJsxRenderer(componentRef, additionalProps);
}

unreachable(input);
Expand Down
7 changes: 6 additions & 1 deletion libs/core/src/lib/renderer/react-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Omit } from '../declarations/omit';
import * as dom from '../utils/dom';

const DEBUG = false;
export const CHILDREN_TO_APPEND_PROP = 'children-to-append';
Expand Down Expand Up @@ -46,7 +47,11 @@ export class ReactContent extends React.PureComponent<AllReactContentProps> {
}

const hostElement = this.props.legacyRenderMode ? element : element.parentElement;
this.props[CHILDREN_TO_APPEND_PROP].forEach(child => hostElement.appendChild(child));

// Only add children not already in the DOM
this.props[CHILDREN_TO_APPEND_PROP].filter(child => !dom.isNodeInDOM(child)).forEach(child =>
hostElement.appendChild(child)
);
}
}

Expand Down
12 changes: 0 additions & 12 deletions libs/core/src/lib/renderer/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import { Injectable, Renderer2, RendererStyleFlags2, RendererType2 } from '@angular/core';
import { EventManager, ɵDomRendererFactory2, ɵDomSharedStylesHost } from '@angular/platform-browser';
import * as ReactDOM from 'react-dom';
import { StringMap } from '../declarations/StringMap';
import { isReactNode, ReactNode } from './react-node';

Expand Down Expand Up @@ -58,17 +57,6 @@ export class AngularReactRendererFactory extends ɵDomRendererFactory2 {
// earlier (as is done for DOM elements) because React element props
// are ReadOnly.

// Workaround for ReactNodes inside ReactContent being added to the root of the VDOM and not removed from the VDOM when unmounted from the DOM.
this.reactRootNodes.forEach(node => {
if (
!isReactNode(node.parent) &&
!document.body.contains(node.parent) &&
ReactDOM.unmountComponentAtNode(node.parent)
) {
this.reactRootNodes.delete(node);
}
});

if (this.isRenderPending) {
// Remove root nodes that are pending destroy after render.
this.reactRootNodes = new Set(Array.from(this.reactRootNodes).filter(node => !node.render().destroyPending));
Expand Down
68 changes: 46 additions & 22 deletions libs/core/src/lib/renderer/renderprop-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { ComponentRef, TemplateRef } from '@angular/core';
import { ComponentRef, EmbeddedViewRef, TemplateRef } from '@angular/core';
import * as React from 'react';
import { CHILDREN_TO_APPEND_PROP, ReactContent, ReactContentProps } from '../renderer/react-content';

export interface RenderPropContext<TContext extends object> {
readonly render: (context: TContext) => JSX.Element;
}

function renderReactContent(rootNodes: HTMLElement[], additionalProps?: ReactContentProps): JSX.Element {
return React.createElement(ReactContent, {
...additionalProps,
Expand All @@ -16,51 +20,71 @@ function renderReactContent(rootNodes: HTMLElement[], additionalProps?: ReactCon
* Wrap a `TemplateRef` with a `JSX.Element`.
*
* @param templateRef The template to wrap
* @param context The context to pass to the template
* @param additionalProps optional additional props to pass to the `ReactContent` object that will render the content.
*/
export function renderTemplate<TContext extends object>(
export function createTemplateRenderer<TContext extends object>(
templateRef: TemplateRef<TContext>,
context?: TContext,
additionalProps?: ReactContentProps
): JSX.Element {
const viewRef = templateRef.createEmbeddedView(context);
viewRef.detectChanges();
): RenderPropContext<TContext> {
let viewRef: EmbeddedViewRef<TContext> | null = null;
let renderedJsx: JSX.Element | null = null;

return {
render: (context: TContext) => {
if (!viewRef) {
viewRef = templateRef.createEmbeddedView(context);
renderedJsx = renderReactContent(viewRef.rootNodes, additionalProps);
} else {
// Mutate the template's context
Object.assign(viewRef.context, context);
}
viewRef.detectChanges();

return renderReactContent(viewRef.rootNodes, additionalProps);
return renderedJsx;
},
};
}

/**
* Wrap a function resolving to an `HTMLElement` with a `JSX.Element`.
*
* @param htmlRenderFunc The function to wrap
* @param context The context to pass to the function
* @param additionalProps optional additional props to pass to the `ReactContent` object that will render the content.
*/
export function renderFunc<TContext extends object>(
export function createHtmlRenderer<TContext extends object>(
htmlRenderFunc: (context: TContext) => HTMLElement,
context?: TContext,
additionalProps?: ReactContentProps
): JSX.Element {
const rootHtmlElement = htmlRenderFunc(context);

return renderReactContent([rootHtmlElement], additionalProps);
): RenderPropContext<TContext> {
return {
render: context => {
const rootHtmlElement = htmlRenderFunc(context);
return renderReactContent([rootHtmlElement], additionalProps);
},
};
}

/**
* Wrap a `ComponentRef` with a `JSX.Element`.
*
* @param htmlRenderFunc The component reference to wrap
* @param context The context to pass to the component as `@Input`
* @param additionalProps optional additional props to pass to the `ReactContent` object that will render the content.
*/
export function renderComponent<TContext extends object>(
export function createComponentRenderer<TContext extends object>(
componentRef: ComponentRef<TContext>,
context?: TContext,
additionalProps?: ReactContentProps
): JSX.Element {
Object.assign(componentRef.instance, context);
componentRef.changeDetectorRef.detectChanges();
): RenderPropContext<TContext> {
let renderedJsx: JSX.Element | null = null;

return {
render: context => {
if (!renderedJsx) {
renderedJsx = renderReactContent([componentRef.location.nativeElement], additionalProps);
}

Object.assign(componentRef.instance, context);
componentRef.changeDetectorRef.detectChanges();

return renderReactContent([componentRef.location.nativeElement], additionalProps);
return renderedJsx;
},
};
}
7 changes: 7 additions & 0 deletions libs/core/src/lib/utils/dom/dom-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Checks if a node is in the DOM.
*
* @param node The node to check
* @returns whether the node is in the DOM
*/
export const isNodeInDOM = (node: Node) => node.isConnected || document.body.contains(node);
1 change: 1 addition & 0 deletions libs/core/src/lib/utils/dom/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dom-utils';