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
5 changes: 5 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,11 @@ <h2>Getting up and running...</h2>
</ol>
</div>

<h5>Generic React component wrapper</h5>
<my-counter [count]="count" (onIncrement)="reactCustomOnIncrement($event)">
<div style="text-transform: uppercase;color:red">test</div>
</my-counter>

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

<div style="width:500px">
Expand Down
6 changes: 6 additions & 0 deletions apps/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ export class AppComponent {
this.onDecrement = this.onDecrement.bind(this);
}

count = 3;

reactCustomOnIncrement(newCount: number) {
this.count = newCount;
}

customItemCount = 1;

// FIXME: Allow declarative syntax too
Expand Down
14 changes: 11 additions & 3 deletions apps/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AngularReactBrowserModule } from '@angular-react/core';
import { AngularReactBrowserModule, wrapComponent } from '@angular-react/core';
import {
FabBreadcrumbModule,
FabButtonModule,
Expand Down Expand Up @@ -35,11 +35,18 @@ import {
FabSpinButtonModule,
FabTextFieldModule,
} from '@angular-react/fabric';
import { NgModule } from '@angular/core';
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { NxModule } from '@nrwl/nx';
import { initializeIcons } from 'office-ui-fabric-react/lib/Icons';
import { AppComponent } from './app.component';
import { CounterComponent } from './counter/counter.component';
import { CounterProps, Counter } from './counter/react-counter';

const MyCounterComponent = wrapComponent<CounterProps>({
ReactComponent: Counter,
selector: 'my-counter',
// propNames: ['count', 'onIncrement'], // needed if propTypes are not defined on `ReactComponent`
});

@NgModule({
imports: [
Expand Down Expand Up @@ -80,8 +87,9 @@ import { CounterComponent } from './counter/counter.component';
FabSpinButtonModule,
FabTextFieldModule,
],
declarations: [AppComponent, CounterComponent],
declarations: [AppComponent, CounterComponent, MyCounterComponent],
bootstrap: [AppComponent],
schemas: [NO_ERRORS_SCHEMA],
})
export class AppModule {
constructor() {
Expand Down
27 changes: 27 additions & 0 deletions apps/demo/src/app/counter/react-counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';

export interface CounterProps {
count?: number;
onIncrement?: (count: number) => void;
}

export const Counter: React.FC<CounterProps> = ({ count = 0, onIncrement, children, ...rest } = {}) => {
return (
<button {...rest} onClick={() => onIncrement(count + 1)}>
<div>
<h5>children:</h5>
{children}
</div>
<div>
<h5>count:</h5>
{count}
</div>
</button>
);
};

Counter.propTypes = {
count: PropTypes.number,
onIncrement: PropTypes.func,
};
3 changes: 2 additions & 1 deletion apps/demo/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc/apps/demo",
"module": "es2015"
"module": "es2015",
"jsx": "react"
},
"include": ["**/*.ts"],
"exclude": ["**/*.spec.ts", "src/test.ts"]
Expand Down
131 changes: 131 additions & 0 deletions libs/core/src/lib/components/generic-wrap-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as React from 'react';
import {
Component,
ElementRef,
ViewChild,
ChangeDetectionStrategy,
Input,
ChangeDetectorRef,
Renderer2,
NgZone,
Output,
EventEmitter,
Type,
} from '@angular/core';

declare const __decorate: typeof import('tslib').__decorate;

import { ReactWrapperComponent } from './wrapper-component';
import { registerElement } from '../renderer/registry';

export interface WrapComponentOptions<TProps extends object> {
/**
* The type of the component to wrap.
*/
ReactComponent: React.ComponentType<TProps>;

/**
* The selector to use.
*/
selector: string;

/**
* The prop names to pass to the `reactComponent`, if any.
* Note that any prop starting with `on` will be converted to an `Output`, and other to `Input`s.
*
* @note If `reactComponent` has `propTypes`, this can be omitted.
*/
propNames?: string[];

/**
* @see `WrapperComponentOptions#setHostDisplay`.
*/
setHostDisplay?: boolean;

/**
* An _optional_ callback for specified wether a prop should be considered an `Output`.
* @default propName => propName.startsWith('on')
*/
isOutputProp?: (propName: string) => boolean;
}

/**
* Gets the display name of a component.
* @param WrappedComponent The type of the wrapper component
*/
function getDisplayName<TProps extends object>(WrappedComponent: React.ComponentType<TProps>): string {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

/**
* Checks if the propName is an output one.
* Currently uses a simple check - anything that starts with `on` is considered an output prop.
*/
function defaultIsOutputProp(propName: string): boolean {
return propName.startsWith('on');
}

function getPropNames<TProps extends object>(ReactComponent: React.ComponentType<TProps>) {
if (!ReactComponent.propTypes) {
return null;
}

return Object.keys(ReactComponent.propTypes);
}

/**
* Wrap a React component with an Angular one.
*
* @template TProps The type of props of the underlying React element.
* @param options Options for wrapping the component.
* @returns A class of a wrapper Angular component.
*/
export function wrapComponent<TProps extends object>(
options: Readonly<WrapComponentOptions<TProps>>
): Type<ReactWrapperComponent<TProps>> {
const Tag = getDisplayName(options.ReactComponent);
registerElement(Tag, () => options.ReactComponent);

const propNames = options.propNames || getPropNames(options.ReactComponent);
const isOutputProp = options.isOutputProp || defaultIsOutputProp;

const inputProps = propNames.filter(propName => !isOutputProp(propName));
const outputProps = propNames.filter(isOutputProp);

const inputPropsBindings = inputProps.map(propName => `[${propName}]="${propName}"`);
const outputPropsBindings = outputProps.map(propName => `(${propName})="${propName}.emit($event)"`);
const propsBindings = [...inputPropsBindings, ...outputPropsBindings].join('\n');

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
styles: ['react-renderer'],
selector: options.selector,
template: `
<${Tag}
#reactNode
${propsBindings}
>
<ReactContent><ng-content></ng-content></ReactContent>
</${Tag}>
`,
})
class WrapperComponent extends ReactWrapperComponent<TProps> {
@ViewChild('reactNode') protected reactNodeRef: ElementRef;

constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, renderer: Renderer2, ngZone: NgZone) {
super(elementRef, changeDetectorRef, renderer, { ngZone, setHostDisplay: options.setHostDisplay });

outputProps.forEach(propName => {
this[propName] = new EventEmitter<any>();
});
}
}

inputProps.forEach(propName => __decorate([Input()], WrapperComponent.prototype, propName));
outputProps.forEach(propName => __decorate([Output()], WrapperComponent.prototype, propName));

return WrapperComponent;
}
26 changes: 17 additions & 9 deletions libs/core/src/lib/components/wrapper-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Renderer2,
SimpleChanges,
AfterContentInit,
ɵBaseDef,
} from '@angular/core';
import classnames from 'classnames';
import toStyle from 'css-to-style';
Expand All @@ -21,9 +22,10 @@ import { Many } from '../declarations/many';
import { ReactContentProps } from '../renderer/react-content';
import { isReactNode } from '../renderer/react-node';
import { isReactRendererData } from '../renderer/renderer';
import { toObject } from '../utils/object/to-object';
import { fromPairs } from '../utils/object/from-pairs';
import { afterRenderFinished } from '../utils/render/render-delay';
import { InputRendererOptions, JsxRenderFunc, createInputJsxRenderer, createRenderPropHandler } from './render-props';
import { omit } from '../utils/object/omit';

// 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$/];
Expand Down Expand Up @@ -231,17 +233,23 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterC
);

const eventListeners = this.elementRef.nativeElement.getEventListeners();
// Event listeners already being handled natively by the derived component
const handledEventListeners = Object.keys(
((this.constructor as any).ngBaseDef as ɵBaseDef<any>).outputs
) as (keyof typeof eventListeners)[];
const unhandledEventListeners = omit(eventListeners, ...handledEventListeners);

const eventHandlersProps =
eventListeners && Object.keys(eventListeners).length
? toObject(
Object.values(eventListeners).map<[string, React.EventHandler<React.SyntheticEvent>]>(([eventListener]) => [
eventListener.type,
(ev: React.SyntheticEvent) => eventListener.listener(ev && ev.nativeEvent),
])
unhandledEventListeners && Object.keys(unhandledEventListeners).length
? fromPairs(
Object.values(unhandledEventListeners).map<[string, React.EventHandler<React.SyntheticEvent>]>(
([eventListener]) => [
eventListener.type,
(ev: React.SyntheticEvent) => eventListener.listener(ev && ev.nativeEvent),
]
)
)
: {};
{
}

this.reactNodeRef.nativeElement.setProperties({ ...props, ...eventHandlersProps });
}
Expand Down
3 changes: 3 additions & 0 deletions libs/core/src/lib/declarations/known-keys.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

// prettier-ignore
/**
* Get the known keys (i.e. no index signature) of T.
Expand Down
3 changes: 3 additions & 0 deletions libs/core/src/lib/declarations/many.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

export type Many<T> = T | T[];
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Transforms an array of [key, value] tuples to an object
*/
export function toObject<T extends [string, any][]>(pairs: T): object {
export function fromPairs<T extends [PropertyKey, any][]>(pairs: T): object {
return pairs.reduce(
(acc, [key, value]) =>
Object.assign(acc, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Omit } from '@angular-react/core';
import { Omit } from '../../declarations/omit';

/**
* Omit a a set of properties from an object.
Expand Down
5 changes: 4 additions & 1 deletion libs/core/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@

export { AngularReactBrowserModule } from './lib/angular-react-browser.module';
export * from './lib/components/wrapper-component';
export * from './lib/components/generic-wrap-component';
export * from './lib/declarations/public-api';
export * from './lib/renderer/components/Disguise';
export { getPassProps, passProp, PassProp } from './lib/renderer/pass-prop-decorator';
export { createReactContentElement, ReactContent, ReactContentProps } from './lib/renderer/react-content';
export * from './lib/renderer/react-template';
export { registerElement } from './lib/renderer/registry';
export { registerElement, ComponentResolver } from './lib/renderer/registry';

export * from './lib/utils/object/omit';
export {
JsxRenderFunc,
RenderComponentOptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { InputRendererOptions, JsxRenderFunc, ReactWrapperComponent } from '@angular-react/core';
import { InputRendererOptions, JsxRenderFunc, ReactWrapperComponent, omit } from '@angular-react/core';
import {
ChangeDetectorRef,
ElementRef,
Expand All @@ -23,7 +23,6 @@ import { IContextualMenuItem } from 'office-ui-fabric-react';
import { Subscription } from 'rxjs';
import { CommandBarItemChangedPayload } from '../command-bar/directives/command-bar-item.directives';
import { mergeItemChanges } from '../core/declarative/item-changed';
import { omit } from '../../utils/omit';
import { getDataAttributes } from '../../utils/get-data-attributes';

export abstract class FabBaseButtonComponent extends ReactWrapperComponent<IButtonProps>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { InputRendererOptions, KnownKeys, ReactWrapperComponent } from '@angular-react/core';
import { InputRendererOptions, KnownKeys, ReactWrapperComponent, omit } from '@angular-react/core';
import {
AfterContentInit,
ChangeDetectionStrategy,
Expand All @@ -22,7 +22,6 @@ import { ICommandBarItemProps, ICommandBarProps } from 'office-ui-fabric-react/l
import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu';
import { Subscription } from 'rxjs';
import { OnChanges, TypedChanges } from '../../declarations/angular/typed-changes';
import omit from '../../utils/omit';
import { mergeItemChanges } from '../core/declarative/item-changed';
import { CommandBarItemChangedPayload, CommandBarItemDirective } from './directives/command-bar-item.directives';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
Renderer2,
ViewChild,
} from '@angular/core';
import { InputRendererOptions, JsxRenderFunc, ReactWrapperComponent } from '@angular-react/core';
import { InputRendererOptions, JsxRenderFunc, ReactWrapperComponent, omit } from '@angular-react/core';
import {
DetailsListBase,
IColumn,
Expand All @@ -32,7 +32,6 @@ import { IListProps } from 'office-ui-fabric-react/lib/List';
import { Subscription } from 'rxjs';

import { OnChanges, TypedChanges } from '../../declarations/angular/typed-changes';
import { omit } from '../../utils/omit';
import { mergeItemChanges } from '../core/declarative/item-changed';
import { ChangeableItemsDirective } from '../core/shared/changeable-items.directive';
import { IDetailsListColumnOptions } from './directives/details-list-column.directive';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { InputRendererOptions, Omit, ReactWrapperComponent } from '@angular-react/core';
import { InputRendererOptions, Omit, ReactWrapperComponent, omit } from '@angular-react/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Expand All @@ -15,7 +15,6 @@ import {
ViewChild,
} from '@angular/core';
import { IExpandingCardProps, IHoverCardProps, IPlainCardProps } from 'office-ui-fabric-react/lib/HoverCard';
import { omit } from '../../utils/omit';

@Component({
selector: 'fab-hover-card',
Expand Down
Loading