Skip to content

Commit 13ff801

Browse files
authored
Allow specifying arbitrary event handlers to any ReactWrapperComponent (#39)
Using the [`geteventlisteners`](https://www.npmjs.com/package/geteventlisteners) package to allow capturing arbitrary event handlers specified as `@Output`s on any React-wrapper component. e.g.: ```html <fab-icon iconName="Add" (onClick)="handleIconClick($event)" (onMouseOver)="handleIconMouseOver($event)"></fab-icon> ``` ```typescript handleIconClick(ev: MouseEvent) { console.log('icon clicked!', ev); } handleIconMouseOver(ev: MouseEvent) { console.log('icon moused-over!', ev); } ``` Although extending global prototypes (i.e. not-yours) is bad practice, this is what Angular uses to capture events, and this seemed like the only way to get any arbitrary output from the element. The other option is to handle each specific event, which is a rather long list, and requires further maintenance, when the DOM, React or the component library adds events to listen to. This is similar in the idea as us passing any arbitrary attribute to the underlying React component.
1 parent 000fa42 commit 13ff801

File tree

10 files changed

+239
-126
lines changed

10 files changed

+239
-126
lines changed

apps/demo/src/app/app.component.html

Lines changed: 85 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,114 @@ <h1>Angular-React Demo</h1>
33
<h2>Getting up and running...</h2>
44

55
<ol>
6-
<li>Add
7-
<i>AngularReactBrowserModule</i> to
8-
<i>app.module.ts</i> in place of the default
9-
<i>BrowserModule</i>.</li>
10-
<li>Add
11-
<i>Fab[component]Module</i> or
12-
<i>Mat[component]Module</i> to
13-
<i>app.module.ts</i> imports.</li>
6+
<li>Add <i>AngularReactBrowserModule</i> to <i>app.module.ts</i> in place of the default <i>BrowserModule</i>.</li>
7+
<li>Add <i>Fab[component]Module</i> or <i>Mat[component]Module</i> to <i>app.module.ts</i> imports.</li>
148
<li>Add Fabric or Material components to your views.</li>
159
</ol>
1610
</div>
1711

12+
<fab-icon iconName="Add" (onClick)="onClickEventHandler($event)" (onMouseOver)="onMouseOverEventHandler($event)">
13+
</fab-icon>
14+
1815
<div>
1916
<fab-pivot>
20-
<fab-pivot-item headerText="Tab 1">
21-
<div>Tab 1's content</div>
22-
</fab-pivot-item>
23-
<fab-pivot-item headerText="Tab 2">
24-
<div>Tab 2's content</div>
25-
</fab-pivot-item>
26-
<fab-pivot-item headerText="Tab 3">
27-
<div>Tab 3's content</div>
28-
</fab-pivot-item>
17+
<fab-pivot-item headerText="Tab 1"> <div>Tab 1's content</div> </fab-pivot-item>
18+
<fab-pivot-item headerText="Tab 2"> <div>Tab 2's content</div> </fab-pivot-item>
19+
<fab-pivot-item headerText="Tab 3"> <div>Tab 3's content</div> </fab-pivot-item>
2920
</fab-pivot>
3021

3122
<fab-command-bar>
3223
<items>
33-
<fab-command-bar-item key="run" text="Run" [iconProps]="{ iconName: 'CaretRight' }" [disabled]="runDisabled"></fab-command-bar-item>
34-
<fab-command-bar-item key="new" text="New" [iconProps]="{ iconName: 'Add' }" (click)="onNewClicked()"></fab-command-bar-item>
35-
<fab-command-bar-item key="copy1" text="Copy1" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
36-
<fab-command-bar-item key="copy2" text="Copy2" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
37-
<fab-command-bar-item key="copy3" text="Copy3" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
38-
<fab-command-bar-item key="copy4" text="Copy4" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
39-
<fab-command-bar-item key="copy5" text="Copy5" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
40-
<fab-command-bar-item key="copy6" text="Copy6" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
41-
<fab-command-bar-item key="copy7" text="Copy7" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
42-
<fab-command-bar-item key="copy8" text="Copy8" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
43-
<fab-command-bar-item key="copy9" text="Copy9" [iconProps]="{ iconName: 'Copy' }" (click)="onCopyClicked()"></fab-command-bar-item>
24+
<fab-command-bar-item
25+
key="run"
26+
text="Run"
27+
[iconProps]="{ iconName: 'CaretRight' }"
28+
[disabled]="runDisabled"
29+
></fab-command-bar-item>
30+
<fab-command-bar-item
31+
key="new"
32+
text="New"
33+
[iconProps]="{ iconName: 'Add' }"
34+
(click)="onNewClicked()"
35+
></fab-command-bar-item>
36+
<fab-command-bar-item
37+
key="copy1"
38+
text="Copy1"
39+
[iconProps]="{ iconName: 'Copy' }"
40+
(click)="onCopyClicked()"
41+
></fab-command-bar-item>
42+
<fab-command-bar-item
43+
key="copy2"
44+
text="Copy2"
45+
[iconProps]="{ iconName: 'Copy' }"
46+
(click)="onCopyClicked()"
47+
></fab-command-bar-item>
48+
<fab-command-bar-item
49+
key="copy3"
50+
text="Copy3"
51+
[iconProps]="{ iconName: 'Copy' }"
52+
(click)="onCopyClicked()"
53+
></fab-command-bar-item>
54+
<fab-command-bar-item
55+
key="copy4"
56+
text="Copy4"
57+
[iconProps]="{ iconName: 'Copy' }"
58+
(click)="onCopyClicked()"
59+
></fab-command-bar-item>
60+
<fab-command-bar-item
61+
key="copy5"
62+
text="Copy5"
63+
[iconProps]="{ iconName: 'Copy' }"
64+
(click)="onCopyClicked()"
65+
></fab-command-bar-item>
66+
<fab-command-bar-item
67+
key="copy6"
68+
text="Copy6"
69+
[iconProps]="{ iconName: 'Copy' }"
70+
(click)="onCopyClicked()"
71+
></fab-command-bar-item>
72+
<fab-command-bar-item
73+
key="copy7"
74+
text="Copy7"
75+
[iconProps]="{ iconName: 'Copy' }"
76+
(click)="onCopyClicked()"
77+
></fab-command-bar-item>
78+
<fab-command-bar-item
79+
key="copy8"
80+
text="Copy8"
81+
[iconProps]="{ iconName: 'Copy' }"
82+
(click)="onCopyClicked()"
83+
></fab-command-bar-item>
84+
<fab-command-bar-item
85+
key="copy9"
86+
text="Copy9"
87+
[iconProps]="{ iconName: 'Copy' }"
88+
(click)="onCopyClicked()"
89+
></fab-command-bar-item>
4490
<fab-command-bar-item key="custom" text="custom text" (click)="onCopyClicked()">
4591
<render>
46-
<ng-template let-item="item">
47-
<counter></counter>
48-
</ng-template>
92+
<ng-template let-item="item"> <counter></counter> </ng-template>
4993
</render>
5094

51-
<!-- <render-icon>
95+
<!--
96+
<render-icon>
5297
<ng-template let-contextualMenuItemProps="contextualMenuItemProps">
5398
<div>custom icon</div>
5499
</ng-template>
55-
</render-icon> -->
100+
</render-icon>
101+
-->
56102
</fab-command-bar-item>
57103
<fab-command-bar-item *ngIf="runDisabled" key="sometimesVisible" text="woosh"></fab-command-bar-item>
58104
</items>
59105

60106
<far-items>
61107
<fab-command-bar-item key="help" text="Help" [iconProps]="{ iconName: 'Help' }"></fab-command-bar-item>
62-
<fab-command-bar-item key="full-screen" [iconOnly]="true" [iconProps]="{ iconName: fullScreenIcon }" (click)="toggleFullScreen()"></fab-command-bar-item>
108+
<fab-command-bar-item
109+
key="full-screen"
110+
[iconOnly]="true"
111+
[iconProps]="{ iconName: fullScreenIcon }"
112+
(click)="toggleFullScreen()"
113+
></fab-command-bar-item>
63114
</far-items>
64115
</fab-command-bar>
65116

apps/demo/src/app/app.component.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ export class AppComponent {
1111
@ViewChild('customRange')
1212
customRangeTemplate: TemplateRef<{ item: any; dismissMenu: (ev?: any, dismissAll?: boolean) => void }>;
1313

14+
onClickEventHandler(ev) {
15+
console.log('onClick', { ev });
16+
}
17+
18+
onMouseOverEventHandler(ev) {
19+
console.log('onMouseOver', { ev });
20+
}
21+
1422
marqueeEnabled: boolean;
1523
runDisabled: boolean;
1624
selection: ISelection;

libs/core/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"tslib",
99
"css-to-style",
1010
"classnames",
11-
"stylenames"
11+
"stylenames",
12+
"geteventlisteners"
1213
],
1314
"lib": {
1415
"languageLevel": [
@@ -60,11 +61,13 @@
6061
"dependencies": {
6162
"css-to-style": "^1.2.0",
6263
"classnames": "^2.2.6",
63-
"stylenames": "^1.1.6"
64+
"stylenames": "^1.1.6",
65+
"geteventlisteners": "^1.0.6"
6466
},
6567
"bundledDependencies": [
6668
"css-to-style",
6769
"classnames",
68-
"stylenames"
70+
"stylenames",
71+
"geteventlisteners"
6972
]
7073
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
interface EventListener<K extends keyof ElementEventMap> {
2+
type: K;
3+
listener: (ev: ElementEventMap[K]) => void;
4+
options?: boolean | EventListenerOptions;
5+
}
6+
7+
type EventListenerArray<K extends keyof ElementEventMap> = EventListener<K>[];
8+
9+
type EventListenersMap<K extends keyof ElementEventMap> = Record<K, EventListenerArray<K>>;
10+
11+
// declare global {
12+
interface Element {
13+
/**
14+
* Gets all the event listeners of the element.
15+
*/
16+
getEventListeners<K extends keyof ElementEventMap>(): EventListenersMap<K>;
17+
18+
/**
19+
* Gets all the event listeners of a type of the element.
20+
*/
21+
getEventListeners<K extends keyof ElementEventMap>(type?: K): EventListenerArray<K>;
22+
}
23+
// }

libs/core/src/lib/angular-react-browser.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { NgModule } from '@angular/core';
55
import { BrowserModule, ɵDomRendererFactory2 } from '@angular/platform-browser';
6-
6+
import 'geteventlisteners';
77
import { AngularReactRendererFactory } from './renderer/renderer';
88

99
@NgModule({

libs/core/src/lib/components/wrapper-component.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
3+
/// <reference path="../@types/geteventlisteners.d.ts" />
34

45
import {
56
AfterViewInit,
@@ -24,6 +25,7 @@ import { ReactContentProps } from '../renderer/react-content';
2425
import { isReactNode } from '../renderer/react-node';
2526
import { isReactRendererData } from '../renderer/renderer';
2627
import { createComponentRenderer, createHtmlRenderer, createTemplateRenderer } from '../renderer/renderprop-helpers';
28+
import { toObject } from '../utils/object/to-object';
2729
import { afterRenderFinished } from '../utils/render/render-delay';
2830
import { unreachable } from '../utils/types/unreachable';
2931

@@ -278,7 +280,20 @@ export abstract class ReactWrapperComponent<TProps extends {}> implements AfterV
278280
{}
279281
);
280282

281-
this.reactNodeRef.nativeElement.setProperties(props);
283+
const eventListeners = this.elementRef.nativeElement.getEventListeners();
284+
const eventHandlersProps =
285+
eventListeners && Object.keys(eventListeners).length
286+
? toObject(
287+
Object.values(eventListeners).map<[string, React.EventHandler<React.SyntheticEvent>]>(([eventListener]) => [
288+
eventListener.type,
289+
(ev: React.SyntheticEvent) => eventListener.listener(ev && ev.nativeEvent),
290+
])
291+
)
292+
: {};
293+
{
294+
}
295+
296+
this.reactNodeRef.nativeElement.setProperties({ ...props, ...eventHandlersProps });
282297
}
283298

284299
private _setHostDisplay() {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Transforms an array of [key, value] tuples to an object
3+
*/
4+
export function toObject<T extends [string, any][]>(pairs: T): object {
5+
return pairs.reduce(
6+
(acc, [key, value]) =>
7+
Object.assign(acc, {
8+
[key]: value,
9+
}),
10+
{}
11+
);
12+
}

libs/core/tsconfig.lib.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@
1212
"experimentalDecorators": true,
1313
"importHelpers": true,
1414
"types": [],
15-
"lib": [
16-
"dom",
17-
"es2015"
18-
]
15+
"typeRoots": ["./src/lib/@types"],
16+
"lib": ["dom", "es2015"]
1917
},
2018
"angularCompilerOptions": {
2119
"annotateForClosureCompiler": true,
@@ -26,8 +24,5 @@
2624
"flatModuleId": "AUTOGENERATED",
2725
"flatModuleOutFile": "AUTOGENERATED"
2826
},
29-
"exclude": [
30-
"src/test.ts",
31-
"**/*.spec.ts"
32-
]
27+
"exclude": ["src/test.ts", "**/*.spec.ts"]
3328
}

0 commit comments

Comments
 (0)