diff --git a/apps/demo/src/app/app.component.html b/apps/demo/src/app/app.component.html
index 9764fe9c..ccf44aa5 100644
--- a/apps/demo/src/app/app.component.html
+++ b/apps/demo/src/app/app.component.html
@@ -9,6 +9,11 @@
diff --git a/apps/demo/src/app/app.component.ts b/apps/demo/src/app/app.component.ts
index a9d86a57..c3123193 100644
--- a/apps/demo/src/app/app.component.ts
+++ b/apps/demo/src/app/app.component.ts
@@ -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
diff --git a/apps/demo/src/app/app.module.ts b/apps/demo/src/app/app.module.ts
index 3bae67a2..75ff7b14 100644
--- a/apps/demo/src/app/app.module.ts
+++ b/apps/demo/src/app/app.module.ts
@@ -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
({
+ 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() {
diff --git a/apps/demo/src/app/counter/react-counter.tsx b/apps/demo/src/app/counter/react-counter.tsx
new file mode 100644
index 00000000..a2c0898a
--- /dev/null
+++ b/apps/demo/src/app/counter/react-counter.tsx
@@ -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 = ({ count = 0, onIncrement, children, ...rest } = {}) => {
+ return (
+
+ );
+};
+
+Counter.propTypes = {
+ count: PropTypes.number,
+ onIncrement: PropTypes.func,
+};
diff --git a/apps/demo/tsconfig.app.json b/apps/demo/tsconfig.app.json
index 46debef0..fade5b38 100644
--- a/apps/demo/tsconfig.app.json
+++ b/apps/demo/tsconfig.app.json
@@ -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"]
diff --git a/libs/core/src/lib/components/generic-wrap-component.ts b/libs/core/src/lib/components/generic-wrap-component.ts
new file mode 100644
index 00000000..912cccf0
--- /dev/null
+++ b/libs/core/src/lib/components/generic-wrap-component.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 {
+ /**
+ * The type of the component to wrap.
+ */
+ ReactComponent: React.ComponentType;
+
+ /**
+ * 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(WrappedComponent: React.ComponentType): 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(ReactComponent: React.ComponentType) {
+ 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(
+ options: Readonly>
+): Type> {
+ 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}
+ >
+
+ ${Tag}>
+ `,
+ })
+ class WrapperComponent extends ReactWrapperComponent {
+ @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();
+ });
+ }
+ }
+
+ inputProps.forEach(propName => __decorate([Input()], WrapperComponent.prototype, propName));
+ outputProps.forEach(propName => __decorate([Output()], WrapperComponent.prototype, propName));
+
+ return WrapperComponent;
+}
diff --git a/libs/core/src/lib/components/wrapper-component.ts b/libs/core/src/lib/components/wrapper-component.ts
index bc97cf1c..283371f3 100644
--- a/libs/core/src/lib/components/wrapper-component.ts
+++ b/libs/core/src/lib/components/wrapper-component.ts
@@ -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 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).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]>(([eventListener]) => [
- eventListener.type,
- (ev: React.SyntheticEvent) => eventListener.listener(ev && ev.nativeEvent),
- ])
+ unhandledEventListeners && Object.keys(unhandledEventListeners).length
+ ? fromPairs(
+ Object.values(unhandledEventListeners).map<[string, React.EventHandler]>(
+ ([eventListener]) => [
+ eventListener.type,
+ (ev: React.SyntheticEvent) => eventListener.listener(ev && ev.nativeEvent),
+ ]
+ )
)
: {};
- {
- }
this.reactNodeRef.nativeElement.setProperties({ ...props, ...eventHandlersProps });
}
diff --git a/libs/core/src/lib/declarations/known-keys.ts b/libs/core/src/lib/declarations/known-keys.ts
index 1b999789..b495321a 100644
--- a/libs/core/src/lib/declarations/known-keys.ts
+++ b/libs/core/src/lib/declarations/known-keys.ts
@@ -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.
diff --git a/libs/core/src/lib/declarations/many.ts b/libs/core/src/lib/declarations/many.ts
index bb631eab..00e68e21 100644
--- a/libs/core/src/lib/declarations/many.ts
+++ b/libs/core/src/lib/declarations/many.ts
@@ -1 +1,4 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
export type Many = T | T[];
diff --git a/libs/core/src/lib/utils/object/to-object.ts b/libs/core/src/lib/utils/object/from-pairs.ts
similarity index 70%
rename from libs/core/src/lib/utils/object/to-object.ts
rename to libs/core/src/lib/utils/object/from-pairs.ts
index ed1472eb..99dc6fbe 100644
--- a/libs/core/src/lib/utils/object/to-object.ts
+++ b/libs/core/src/lib/utils/object/from-pairs.ts
@@ -1,7 +1,7 @@
/**
* Transforms an array of [key, value] tuples to an object
*/
-export function toObject(pairs: T): object {
+export function fromPairs(pairs: T): object {
return pairs.reduce(
(acc, [key, value]) =>
Object.assign(acc, {
diff --git a/libs/fabric/src/lib/utils/omit.ts b/libs/core/src/lib/utils/object/omit.ts
similarity index 92%
rename from libs/fabric/src/lib/utils/omit.ts
rename to libs/core/src/lib/utils/object/omit.ts
index 02c8b004..d7a51812 100644
--- a/libs/fabric/src/lib/utils/omit.ts
+++ b/libs/core/src/lib/utils/object/omit.ts
@@ -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.
diff --git a/libs/core/src/public-api.ts b/libs/core/src/public-api.ts
index 63b74187..f23bc2cd 100644
--- a/libs/core/src/public-api.ts
+++ b/libs/core/src/public-api.ts
@@ -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,
diff --git a/libs/fabric/src/lib/components/button/base-button.component.ts b/libs/fabric/src/lib/components/button/base-button.component.ts
index e43cd76d..31d4af96 100644
--- a/libs/fabric/src/lib/components/button/base-button.component.ts
+++ b/libs/fabric/src/lib/components/button/base-button.component.ts
@@ -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
diff --git a/libs/fabric/src/lib/components/command-bar/command-bar.component.ts b/libs/fabric/src/lib/components/command-bar/command-bar.component.ts
index 3c5358a6..809b1fd6 100644
--- a/libs/fabric/src/lib/components/command-bar/command-bar.component.ts
+++ b/libs/fabric/src/lib/components/command-bar/command-bar.component.ts
@@ -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 {
diff --git a/libs/fabric/src/lib/components/details-list/details-list.component.ts b/libs/fabric/src/lib/components/details-list/details-list.component.ts
index 3ad42d6d..b3c9663f 100644
--- a/libs/fabric/src/lib/components/details-list/details-list.component.ts
+++ b/libs/fabric/src/lib/components/details-list/details-list.component.ts
@@ -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';
diff --git a/libs/fabric/src/lib/components/hover-card/hover-card.component.ts b/libs/fabric/src/lib/components/hover-card/hover-card.component.ts
index 04add79f..f5aff0fa 100644
--- a/libs/fabric/src/lib/components/hover-card/hover-card.component.ts
+++ b/libs/fabric/src/lib/components/hover-card/hover-card.component.ts
@@ -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',
diff --git a/libs/fabric/src/lib/components/pickers/base-picker/base-picker.component.ts b/libs/fabric/src/lib/components/pickers/base-picker/base-picker.component.ts
index e923e469..5bc7a5cb 100644
--- a/libs/fabric/src/lib/components/pickers/base-picker/base-picker.component.ts
+++ b/libs/fabric/src/lib/components/pickers/base-picker/base-picker.component.ts
@@ -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>
extends ReactWrapperComponent
@@ -19,7 +18,7 @@ export abstract class FabBasePickerComponent['resolveDelay'];
@Input() defaultSelectedItems?: IBasePickerProps['defaultSelectedItems'];
@Input() getTextFromItem?: IBasePickerProps['getTextFromItem'];
- @Input() className?: IBasePickerProps['className'];
+ @Input() className?: IBasePickerProps['className'];
@Input() pickerCalloutProps?: IBasePickerProps['pickerCalloutProps'];
@Input() searchingText?: IBasePickerProps['searchingText'];
@Input() disabled?: IBasePickerProps['disabled'];
diff --git a/libs/fabric/src/lib/components/search-box/search-box.component.ts b/libs/fabric/src/lib/components/search-box/search-box.component.ts
index fa1c1a87..12791073 100644
--- a/libs/fabric/src/lib/components/search-box/search-box.component.ts
+++ b/libs/fabric/src/lib/components/search-box/search-box.component.ts
@@ -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',
diff --git a/libs/fabric/src/lib/components/tooltip/tooltip-host.component.ts b/libs/fabric/src/lib/components/tooltip/tooltip-host.component.ts
index 99f5d264..92a3094d 100644
--- a/libs/fabric/src/lib/components/tooltip/tooltip-host.component.ts
+++ b/libs/fabric/src/lib/components/tooltip/tooltip-host.component.ts
@@ -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',