Allow wrapping any React component with an Angular one on-the-fly. (#106)
This commit is contained in:
Родитель
4bb71a8395
Коммит
b756043226
|
@ -9,6 +9,11 @@
|
|||
</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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { AngularReactBrowserModule } from '@angular-react/core';
|
||||
import { AngularReactBrowserModule, wrapComponent } from '@angular-react/core';
|
||||
import {
|
||||
FabBreadcrumbModule,
|
||||
FabButtonModule,
|
||||
|
@ -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: [
|
||||
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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"]
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -12,6 +12,7 @@ import {
|
|||
Renderer2,
|
||||
SimpleChanges,
|
||||
AfterContentInit,
|
||||
ɵBaseDef,
|
||||
} from '@angular/core';
|
||||
import classnames from 'classnames';
|
||||
import toStyle from 'css-to-style';
|
||||
|
@ -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$/];
|
||||
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
export type Many<T> = T | T[];
|
||||
|
|
|
@ -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, {
|
|
@ -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.
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
@ -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',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
import { InputRendererOptions, JsxRenderFunc, Omit, ReactWrapperComponent } from '@angular-react/core';
|
||||
import { InputRendererOptions, JsxRenderFunc, Omit, ReactWrapperComponent, omit } from '@angular-react/core';
|
||||
import { ChangeDetectorRef, ElementRef, EventEmitter, Input, NgZone, OnInit, Output, Renderer2 } from '@angular/core';
|
||||
import { IPersonaProps } from 'office-ui-fabric-react/lib/Persona';
|
||||
import {
|
||||
|
@ -10,7 +10,6 @@ import {
|
|||
IBasePickerSuggestionsProps,
|
||||
IPickerItemProps,
|
||||
} from 'office-ui-fabric-react/lib/Pickers';
|
||||
import omit from '../../../utils/omit';
|
||||
|
||||
export abstract class FabBasePickerComponent<T, TProps extends IBasePickerProps<T>>
|
||||
extends ReactWrapperComponent<TProps>
|
||||
|
@ -19,7 +18,7 @@ export abstract class FabBasePickerComponent<T, TProps extends IBasePickerProps<
|
|||
@Input() resolveDelay?: IBasePickerProps<T>['resolveDelay'];
|
||||
@Input() defaultSelectedItems?: IBasePickerProps<T>['defaultSelectedItems'];
|
||||
@Input() getTextFromItem?: IBasePickerProps<T>['getTextFromItem'];
|
||||
@Input() className?: IBasePickerProps<T>['className'];
|
||||
@Input() className?: IBasePickerProps<T>['className'];
|
||||
@Input() pickerCalloutProps?: IBasePickerProps<T>['pickerCalloutProps'];
|
||||
@Input() searchingText?: IBasePickerProps<T>['searchingText'];
|
||||
@Input() disabled?: IBasePickerProps<T>['disabled'];
|
||||
|
|
|
@ -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,
|
||||
|
@ -16,7 +16,6 @@ import {
|
|||
} from '@angular/core';
|
||||
import { IButtonProps } from 'office-ui-fabric-react/lib/Button';
|
||||
import { ISearchBoxProps } from 'office-ui-fabric-react/lib/SearchBox';
|
||||
import omit from '../../utils/omit';
|
||||
|
||||
@Component({
|
||||
selector: 'fab-search-box',
|
||||
|
|
|
@ -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,
|
||||
|
@ -15,7 +15,6 @@ import {
|
|||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { ITooltipHostProps, ITooltipProps } from 'office-ui-fabric-react/lib/Tooltip';
|
||||
import { omit } from '../../utils/omit';
|
||||
|
||||
@Component({
|
||||
selector: 'fab-tooltip-host',
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"typeRoots": ["node_modules/@types"],
|
||||
"lib": ["es2017", "dom"],
|
||||
"baseUrl": ".",
|
||||
"jsx": "react",
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@angular-react/*": ["libs/*"],
|
||||
|
|
Загрузка…
Ссылка в новой задаче