Skip to content

Commit 0bf7d9a

Browse files
authored
Add declarative syntax to menu items in fab-*-buttons (#61)
* allow contextual menu item directive to have a custom render, or a custom icon render the custom icon render doesn't work due to an office-ui-fabric-react bug * remove unnecessary casting to `any` * allow `fab-*-button`s to render menu items in a declarative syntax, similar to how command bar items allow it
1 parent b307c6f commit 0bf7d9a

File tree

5 files changed

+191
-36
lines changed

5 files changed

+191
-36
lines changed

libs/fabric/src/lib/components/button/base-button.component.ts

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,31 @@
22
// Licensed under the MIT License.
33

44
import { InputRendererOptions, JsxRenderFunc, ReactWrapperComponent } from '@angular-react/core';
5-
import { ChangeDetectorRef, ElementRef, EventEmitter, Input, NgZone, OnInit, Output, Renderer2 } from '@angular/core';
5+
import {
6+
ChangeDetectorRef,
7+
ElementRef,
8+
EventEmitter,
9+
Input,
10+
NgZone,
11+
OnInit,
12+
Output,
13+
Renderer2,
14+
ContentChildren,
15+
QueryList,
16+
AfterContentInit,
17+
OnDestroy,
18+
} from '@angular/core';
619
import { IButtonProps } from 'office-ui-fabric-react/lib/Button';
20+
import { ContextualMenuItemDirective, IContextualMenuItemOptions } from '../contextual-menu/public-api';
21+
import { ChangeableItemsHelper } from '../core/shared/changeable-helper';
22+
import { IContextualMenuItem } from 'office-ui-fabric-react';
23+
import { Subscription } from 'rxjs';
24+
import { CommandBarItemChangedPayload } from '../command-bar/directives/command-bar-item.directives';
25+
import { mergeItemChanges } from '../core/declarative/item-changed';
26+
import { omit } from '../../utils/omit';
727

8-
export abstract class FabBaseButtonComponent extends ReactWrapperComponent<IButtonProps> implements OnInit {
28+
export abstract class FabBaseButtonComponent extends ReactWrapperComponent<IButtonProps>
29+
implements OnInit, AfterContentInit, OnDestroy {
930
@Input() componentRef?: IButtonProps['componentRef'];
1031
@Input() href?: IButtonProps['href'];
1132
@Input() primary?: IButtonProps['primary'];
@@ -47,13 +68,18 @@ export abstract class FabBaseButtonComponent extends ReactWrapperComponent<IButt
4768
@Output() readonly onMenuClick = new EventEmitter<{ ev?: MouseEvent | KeyboardEvent; button?: IButtonProps }>();
4869
@Output() readonly onAfterMenuDismiss = new EventEmitter<void>();
4970

71+
@ContentChildren(ContextualMenuItemDirective) readonly menuItemsDirectives?: QueryList<ContextualMenuItemDirective>;
72+
5073
onRenderIcon: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
5174
onRenderText: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
5275
onRenderDescription: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
5376
onRenderAriaDescription: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
5477
onRenderChildren: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
5578
onRenderMenuIcon: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
5679

80+
private _changeableItemsHelper: ChangeableItemsHelper<IContextualMenuItem>;
81+
private _subscriptions: Subscription[] = [];
82+
5783
constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, renderer: Renderer2, ngZone: NgZone) {
5884
super(elementRef, changeDetectorRef, renderer, { ngZone, setHostDisplay: true });
5985

@@ -70,6 +96,50 @@ export abstract class FabBaseButtonComponent extends ReactWrapperComponent<IButt
7096
this.onRenderMenuIcon = this.createRenderPropHandler(this.renderMenuIcon);
7197
}
7298

99+
ngAfterContentInit() {
100+
if (this.menuItemsDirectives && this.menuItemsDirectives.length > 0) {
101+
const setItems = (directiveItems: ReadonlyArray<ContextualMenuItemDirective>) => {
102+
const items = directiveItems.map(directive =>
103+
this._transformContextualMenuItemOptionsToProps(this._directiveToContextualMenuItem(directive))
104+
);
105+
if (!this.menuProps) {
106+
this.menuProps = { items: items };
107+
} else {
108+
this.menuProps.items = items;
109+
}
110+
111+
this.markForCheck();
112+
};
113+
114+
this._changeableItemsHelper = new ChangeableItemsHelper(this.menuItemsDirectives);
115+
this._subscriptions.push(
116+
this._changeableItemsHelper.onItemsChanged.subscribe((newItems: QueryList<ContextualMenuItemDirective>) => {
117+
setItems(newItems.toArray());
118+
}),
119+
this._changeableItemsHelper.onChildItemChanged.subscribe(({ key, changes }: CommandBarItemChangedPayload) => {
120+
const newItems = this.menuItemsDirectives.map(item =>
121+
item.key === key ? mergeItemChanges(item, changes) : item
122+
);
123+
setItems(newItems);
124+
125+
this.markForCheck();
126+
})
127+
);
128+
129+
setItems(this.menuItemsDirectives.toArray());
130+
}
131+
}
132+
133+
ngOnDestroy() {
134+
if (this._changeableItemsHelper) {
135+
this._changeableItemsHelper.destroy();
136+
}
137+
138+
if (this._subscriptions) {
139+
this._subscriptions.forEach(subscription => subscription.unsubscribe());
140+
}
141+
}
142+
73143
onMenuClickHandler(ev?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, button?: IButtonProps) {
74144
this.onMenuClick.emit({
75145
ev: ev && ev.nativeEvent,
@@ -80,4 +150,46 @@ export abstract class FabBaseButtonComponent extends ReactWrapperComponent<IButt
80150
onClickHandler(ev?: React.MouseEvent) {
81151
this.onClick.emit(ev.nativeEvent);
82152
}
153+
154+
private _directiveToContextualMenuItem(directive: ContextualMenuItemDirective): IContextualMenuItemOptions {
155+
return {
156+
...omit(
157+
directive,
158+
'menuItemsDirectives',
159+
'renderDirective',
160+
'renderIconDirective',
161+
'click',
162+
'onItemChanged',
163+
'onItemsChanged',
164+
'onChildItemChanged',
165+
'ngOnInit',
166+
'ngOnChanges',
167+
'ngOnDestroy',
168+
'ngAfterContentInit'
169+
),
170+
onClick: (ev, item) => {
171+
directive.click.emit({ ev: ev && ev.nativeEvent, item: item });
172+
},
173+
};
174+
}
175+
176+
private _transformContextualMenuItemOptionsToProps(itemOptions: IContextualMenuItemOptions): IContextualMenuItem {
177+
const sharedProperties = omit(itemOptions, 'renderIcon', 'render');
178+
179+
// Legacy render mode is used for the icon because otherwise the icon is to the right of the text (instead of the usual left)
180+
const iconRenderer = this.createInputJsxRenderer(itemOptions.renderIcon, { legacyRenderMode: true });
181+
const renderer = this.createInputJsxRenderer(itemOptions.render);
182+
183+
return Object.assign(
184+
{},
185+
sharedProperties,
186+
iconRenderer && {
187+
onRenderIcon: (item: IContextualMenuItem) => iconRenderer({ contextualMenuItem: item }),
188+
},
189+
renderer &&
190+
({
191+
onRender: (item, dismissMenu) => renderer({ item, dismissMenu }),
192+
} as Pick<IContextualMenuItem, 'onRender'>)
193+
) as IContextualMenuItem;
194+
}
83195
}

libs/fabric/src/lib/components/command-bar/command-bar.component.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,9 @@ export class FabCommandBarComponent extends ReactWrapperComponent<ICommandBarPro
198198
return Object.assign(
199199
{},
200200
sharedProperties,
201-
iconRenderer &&
202-
({
203-
onRenderIcon: (item: IContextualMenuItem) => iconRenderer({ contextualMenuItem: item }),
204-
} as any) /* NOTE: Fix for wrong typings of `onRenderIcon` in office-ui-fabric-react */,
201+
iconRenderer && {
202+
onRenderIcon: (item: IContextualMenuItem) => iconRenderer({ contextualMenuItem: item }),
203+
},
205204
renderer &&
206205
({ onRender: (item, dismissMenu) => renderer({ item, dismissMenu }) } as Pick<ICommandBarItemProps, 'onRender'>)
207206
) as ICommandBarItemProps;
Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { AfterContentInit, ContentChild, Directive, Input, TemplateRef } from '@angular/core';
4+
import { ContentChild, Directive, Input, TemplateRef } from '@angular/core';
55
import { ContextualMenuItemDirective } from '../../contextual-menu/directives/contextual-menu-item.directive';
66
import { ItemChangedPayload } from '../../core/declarative/item-changed.payload';
77
import {
@@ -29,30 +29,12 @@ export class CommandBarItemRenderIconDirective {
2929
}
3030

3131
@Directive({ selector: 'fab-command-bar-item' })
32-
export class CommandBarItemDirective extends ContextualMenuItemDirective
33-
implements ICommandBarItemOptions, AfterContentInit {
34-
@ContentChild(CommandBarItemRenderDirective) readonly renderDirective: CommandBarItemRenderDirective;
35-
@ContentChild(CommandBarItemRenderIconDirective) readonly renderIconDirective: CommandBarItemRenderIconDirective;
36-
32+
export class CommandBarItemDirective extends ContextualMenuItemDirective implements ICommandBarItemOptions {
3733
// ICommandBarItemOptions implementation
3834
@Input() iconOnly?: ICommandBarItemOptions['iconOnly'];
3935
@Input() tooltipHostProps?: ICommandBarItemOptions['tooltipHostProps'];
4036
@Input() buttonStyles?: ICommandBarItemOptions['buttonStyles'];
4137
@Input() cacheKey?: ICommandBarItemOptions['cacheKey'];
4238
@Input() renderedInOverflow?: ICommandBarItemOptions['renderedInOverflow'];
4339
@Input() commandBarButtonAs?: ICommandBarItemOptions['commandBarButtonAs'];
44-
@Input() render: ICommandBarItemOptions['render'];
45-
@Input() renderIcon: ICommandBarItemOptions['renderIcon'];
46-
47-
ngAfterContentInit() {
48-
super.ngAfterContentInit();
49-
50-
if (this.renderDirective && this.renderDirective.templateRef) {
51-
this.render = this.renderDirective.templateRef;
52-
}
53-
54-
if (this.renderIconDirective && this.renderIconDirective.templateRef) {
55-
this.renderIcon = this.renderIconDirective.templateRef;
56-
}
57-
}
5840
}

libs/fabric/src/lib/components/contextual-menu/contextual-menu.module.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@
33

44
import { CommonModule } from '@angular/common';
55
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
6-
import { ContextualMenuItemDirective } from './directives/contextual-menu-item.directive';
6+
import {
7+
ContextualMenuItemDirective,
8+
ContextualMenuItemRenderDirective,
9+
ContextualMenuItemRenderIconDirective,
10+
} from './directives/contextual-menu-item.directive';
711

8-
const components = [ContextualMenuItemDirective];
12+
const components = [
13+
ContextualMenuItemDirective,
14+
ContextualMenuItemRenderDirective,
15+
ContextualMenuItemRenderIconDirective,
16+
];
917

1018
@NgModule({
1119
imports: [CommonModule],

libs/fabric/src/lib/components/contextual-menu/directives/contextual-menu-item.directive.ts

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,38 @@ import {
1010
OnDestroy,
1111
Output,
1212
QueryList,
13+
ContentChild,
14+
TemplateRef,
1315
} from '@angular/core';
1416
import { IContextualMenuItem } from 'office-ui-fabric-react';
17+
import { KnownKeys, InputRendererOptions } from '@angular-react/core';
1518

1619
import { OnChanges } from '../../../declarations/angular/typed-changes';
1720
import { ItemChangedPayload } from '../../core/declarative/item-changed.payload';
1821
import { ChangeableItemsHelper, IChangeableItemsContainer } from '../../core/shared/changeable-helper';
1922
import { ChangeableItemDirective } from '../../core/shared/changeable-item.directive';
2023

24+
export type ContextualMenuItemChangedPayload = ItemChangedPayload<
25+
IContextualMenuItemOptions['key'],
26+
IContextualMenuItemOptions
27+
>;
28+
29+
/**
30+
* Wrapper directive to allow rendering a custom item to a ContextualMenuItem.
31+
*/
32+
@Directive({ selector: 'fab-command-bar-item > render' })
33+
export class ContextualMenuItemRenderDirective {
34+
@ContentChild(TemplateRef) readonly templateRef: TemplateRef<IContextualMenuItemOptionsRenderContext>;
35+
}
36+
37+
/**
38+
* Wrapper directive to allow rendering a custom icon to a ContextualMenuItem.
39+
*/
40+
@Directive({ selector: 'fab-command-bar-item > render-icon' })
41+
export class ContextualMenuItemRenderIconDirective {
42+
@ContentChild(TemplateRef) readonly templateRef: TemplateRef<IContextualMenuItemOptionsRenderIconContext>;
43+
}
44+
2145
@Directive({ selector: 'contextual-menu-item' })
2246
export class ContextualMenuItemDirective extends ChangeableItemDirective<IContextualMenuItem>
2347
implements
@@ -27,13 +51,15 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective<IContex
2751
OnChanges<ContextualMenuItemDirective>,
2852
OnDestroy {
2953
@ContentChildren(ContextualMenuItemDirective) readonly menuItemsDirectives: QueryList<ContextualMenuItemDirective>;
54+
@ContentChild(ContextualMenuItemRenderDirective) readonly renderDirective: ContextualMenuItemRenderDirective;
55+
@ContentChild(ContextualMenuItemRenderIconDirective)
56+
readonly renderIconDirective: ContextualMenuItemRenderIconDirective;
3057

3158
@Input() componentRef?: IContextualMenuItem['componentRef'];
3259
@Input() text?: IContextualMenuItem['text'];
3360
@Input() secondaryText?: IContextualMenuItem['secondaryText'];
3461
@Input() itemType?: IContextualMenuItem['itemType'];
3562
@Input() iconProps?: IContextualMenuItem['iconProps'];
36-
@Input() onRenderIcon?: IContextualMenuItem['onRenderIcon'];
3763
@Input() submenuIconProps?: IContextualMenuItem['submenuIconProps'];
3864
@Input() disabled?: IContextualMenuItem['disabled'];
3965
@Input() primaryDisabled?: IContextualMenuItem['primaryDisabled'];
@@ -54,29 +80,39 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective<IContex
5480
@Input() style?: IContextualMenuItem['style'];
5581
@Input() ariaLabel?: IContextualMenuItem['ariaLabel'];
5682
@Input() title?: IContextualMenuItem['title'];
57-
@Input() onRender?: IContextualMenuItem['onRender'];
5883
@Input() onMouseDown?: IContextualMenuItem['onMouseDown'];
5984
@Input() role?: IContextualMenuItem['role'];
6085
@Input() customOnRenderListLength?: IContextualMenuItem['customOnRenderListLength'];
6186
@Input() keytipProps?: IContextualMenuItem['keytipProps'];
6287
@Input() inactive?: IContextualMenuItem['inactive'];
6388
@Input() name?: IContextualMenuItem['name'];
89+
@Input() render: IContextualMenuItemOptions['render'];
90+
@Input() renderIcon: IContextualMenuItemOptions['renderIcon'];
6491

6592
@Output() readonly click = new EventEmitter<{ ev?: MouseEvent | KeyboardEvent; item?: IContextualMenuItem }>();
6693

6794
@Output()
6895
get onChildItemChanged(): EventEmitter<ItemChangedPayload<string, IContextualMenuItem>> {
69-
return this.changeableItemsHelper && this.changeableItemsHelper.onChildItemChanged;
96+
return this._changeableItemsHelper && this._changeableItemsHelper.onChildItemChanged;
7097
}
71-
@Input()
98+
99+
@Output()
72100
get onItemsChanged(): EventEmitter<QueryList<ChangeableItemDirective<IContextualMenuItem>>> {
73-
return this.changeableItemsHelper && this.changeableItemsHelper.onItemsChanged;
101+
return this._changeableItemsHelper && this._changeableItemsHelper.onItemsChanged;
74102
}
75103

76-
private changeableItemsHelper: ChangeableItemsHelper<IContextualMenuItem>;
104+
private _changeableItemsHelper: ChangeableItemsHelper<IContextualMenuItem>;
77105

78106
ngAfterContentInit() {
79-
this.changeableItemsHelper = new ChangeableItemsHelper(this.menuItemsDirectives, this, nonSelfDirective => {
107+
if (this.renderDirective && this.renderDirective.templateRef) {
108+
this.render = this.renderDirective.templateRef;
109+
}
110+
111+
if (this.renderIconDirective && this.renderIconDirective.templateRef) {
112+
this.renderIcon = this.renderIconDirective.templateRef;
113+
}
114+
115+
this._changeableItemsHelper = new ChangeableItemsHelper(this.menuItemsDirectives, this, nonSelfDirective => {
80116
const items = nonSelfDirective.map(directive => this._directiveToContextualMenuItem(directive as any));
81117
if (!this.subMenuProps) {
82118
this.subMenuProps = { items: items };
@@ -87,7 +123,7 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective<IContex
87123
}
88124

89125
ngOnDestroy() {
90-
this.changeableItemsHelper.destroy();
126+
this._changeableItemsHelper.destroy();
91127
}
92128

93129
private _directiveToContextualMenuItem(directive: ContextualMenuItemDirective): IContextualMenuItem {
@@ -99,3 +135,21 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective<IContex
99135
};
100136
}
101137
}
138+
139+
// Not using `Omit` here since it confused the TypeScript compiler and it just showed the properties listed here (`renderIcon`, `render` and `data`).
140+
// The type here is just `Omit` without the generics though.
141+
export interface IContextualMenuItemOptions<TData = any>
142+
extends Pick<IContextualMenuItem, Exclude<KnownKeys<IContextualMenuItem>, 'onRender' | 'onRenderIcon'>> {
143+
readonly renderIcon?: InputRendererOptions<IContextualMenuItemOptionsRenderIconContext>;
144+
readonly render?: InputRendererOptions<IContextualMenuItemOptionsRenderContext>;
145+
readonly data?: TData;
146+
}
147+
148+
export interface IContextualMenuItemOptionsRenderContext {
149+
item: any;
150+
dismissMenu: (ev?: any, dismissAll?: boolean) => void;
151+
}
152+
153+
export interface IContextualMenuItemOptionsRenderIconContext {
154+
contextualMenuItem: IContextualMenuItem;
155+
}

0 commit comments

Comments
 (0)