Skip to content

Commit d168704

Browse files
committed
allow fab-*-buttons to render menu items in a declarative syntax, similar to how command bar items allow it
1 parent 2f54281 commit d168704

File tree

2 files changed

+120
-8
lines changed

2 files changed

+120
-8
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/contextual-menu/directives/contextual-menu-item.directive.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
ContentChild,
1414
TemplateRef,
1515
} from '@angular/core';
16-
import { IContextualMenuItem, IContextualMenuItemProps } from 'office-ui-fabric-react';
16+
import { IContextualMenuItem } from 'office-ui-fabric-react';
1717
import { KnownKeys, InputRendererOptions } from '@angular-react/core';
1818

1919
import { OnChanges } from '../../../declarations/angular/typed-changes';
@@ -93,15 +93,15 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective<IContex
9393

9494
@Output()
9595
get onChildItemChanged(): EventEmitter<ItemChangedPayload<string, IContextualMenuItem>> {
96-
return this.changeableItemsHelper && this.changeableItemsHelper.onChildItemChanged;
96+
return this._changeableItemsHelper && this._changeableItemsHelper.onChildItemChanged;
9797
}
9898

9999
@Output()
100100
get onItemsChanged(): EventEmitter<QueryList<ChangeableItemDirective<IContextualMenuItem>>> {
101-
return this.changeableItemsHelper && this.changeableItemsHelper.onItemsChanged;
101+
return this._changeableItemsHelper && this._changeableItemsHelper.onItemsChanged;
102102
}
103103

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

106106
ngAfterContentInit() {
107107
if (this.renderDirective && this.renderDirective.templateRef) {
@@ -112,7 +112,7 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective<IContex
112112
this.renderIcon = this.renderIconDirective.templateRef;
113113
}
114114

115-
this.changeableItemsHelper = new ChangeableItemsHelper(this.menuItemsDirectives, this, nonSelfDirective => {
115+
this._changeableItemsHelper = new ChangeableItemsHelper(this.menuItemsDirectives, this, nonSelfDirective => {
116116
const items = nonSelfDirective.map(directive => this._directiveToContextualMenuItem(directive as any));
117117
if (!this.subMenuProps) {
118118
this.subMenuProps = { items: items };
@@ -123,7 +123,7 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective<IContex
123123
}
124124

125125
ngOnDestroy() {
126-
this.changeableItemsHelper.destroy();
126+
this._changeableItemsHelper.destroy();
127127
}
128128

129129
private _directiveToContextualMenuItem(directive: ContextualMenuItemDirective): IContextualMenuItem {

0 commit comments

Comments
 (0)