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 df21cc6..7597873 100644 --- a/libs/fabric/src/lib/components/button/base-button.component.ts +++ b/libs/fabric/src/lib/components/button/base-button.component.ts @@ -2,10 +2,31 @@ // Licensed under the MIT License. import { InputRendererOptions, JsxRenderFunc, ReactWrapperComponent } from '@angular-react/core'; -import { ChangeDetectorRef, ElementRef, EventEmitter, Input, NgZone, OnInit, Output, Renderer2 } from '@angular/core'; +import { + ChangeDetectorRef, + ElementRef, + EventEmitter, + Input, + NgZone, + OnInit, + Output, + Renderer2, + ContentChildren, + QueryList, + AfterContentInit, + OnDestroy, +} from '@angular/core'; import { IButtonProps } from 'office-ui-fabric-react/lib/Button'; +import { ContextualMenuItemDirective, IContextualMenuItemOptions } from '../contextual-menu/public-api'; +import { ChangeableItemsHelper } from '../core/shared/changeable-helper'; +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'; -export abstract class FabBaseButtonComponent extends ReactWrapperComponent implements OnInit { +export abstract class FabBaseButtonComponent extends ReactWrapperComponent + implements OnInit, AfterContentInit, OnDestroy { @Input() componentRef?: IButtonProps['componentRef']; @Input() href?: IButtonProps['href']; @Input() primary?: IButtonProps['primary']; @@ -47,6 +68,8 @@ export abstract class FabBaseButtonComponent extends ReactWrapperComponent(); @Output() readonly onAfterMenuDismiss = new EventEmitter(); + @ContentChildren(ContextualMenuItemDirective) readonly menuItemsDirectives?: QueryList; + onRenderIcon: (props?: IButtonProps, defaultRender?: JsxRenderFunc) => JSX.Element; onRenderText: (props?: IButtonProps, defaultRender?: JsxRenderFunc) => JSX.Element; onRenderDescription: (props?: IButtonProps, defaultRender?: JsxRenderFunc) => JSX.Element; @@ -54,6 +77,9 @@ export abstract class FabBaseButtonComponent extends ReactWrapperComponent) => JSX.Element; onRenderMenuIcon: (props?: IButtonProps, defaultRender?: JsxRenderFunc) => JSX.Element; + private _changeableItemsHelper: ChangeableItemsHelper; + private _subscriptions: Subscription[] = []; + constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, renderer: Renderer2, ngZone: NgZone) { super(elementRef, changeDetectorRef, renderer, { ngZone, setHostDisplay: true }); @@ -70,6 +96,50 @@ export abstract class FabBaseButtonComponent extends ReactWrapperComponent 0) { + const setItems = (directiveItems: ReadonlyArray) => { + const items = directiveItems.map(directive => + this._transformContextualMenuItemOptionsToProps(this._directiveToContextualMenuItem(directive)) + ); + if (!this.menuProps) { + this.menuProps = { items: items }; + } else { + this.menuProps.items = items; + } + + this.markForCheck(); + }; + + this._changeableItemsHelper = new ChangeableItemsHelper(this.menuItemsDirectives); + this._subscriptions.push( + this._changeableItemsHelper.onItemsChanged.subscribe((newItems: QueryList) => { + setItems(newItems.toArray()); + }), + this._changeableItemsHelper.onChildItemChanged.subscribe(({ key, changes }: CommandBarItemChangedPayload) => { + const newItems = this.menuItemsDirectives.map(item => + item.key === key ? mergeItemChanges(item, changes) : item + ); + setItems(newItems); + + this.markForCheck(); + }) + ); + + setItems(this.menuItemsDirectives.toArray()); + } + } + + ngOnDestroy() { + if (this._changeableItemsHelper) { + this._changeableItemsHelper.destroy(); + } + + if (this._subscriptions) { + this._subscriptions.forEach(subscription => subscription.unsubscribe()); + } + } + onMenuClickHandler(ev?: React.MouseEvent | React.KeyboardEvent, button?: IButtonProps) { this.onMenuClick.emit({ ev: ev && ev.nativeEvent, @@ -80,4 +150,46 @@ export abstract class FabBaseButtonComponent extends ReactWrapperComponent { + directive.click.emit({ ev: ev && ev.nativeEvent, item: item }); + }, + }; + } + + private _transformContextualMenuItemOptionsToProps(itemOptions: IContextualMenuItemOptions): IContextualMenuItem { + const sharedProperties = omit(itemOptions, 'renderIcon', 'render'); + + // Legacy render mode is used for the icon because otherwise the icon is to the right of the text (instead of the usual left) + const iconRenderer = this.createInputJsxRenderer(itemOptions.renderIcon, { legacyRenderMode: true }); + const renderer = this.createInputJsxRenderer(itemOptions.render); + + return Object.assign( + {}, + sharedProperties, + iconRenderer && { + onRenderIcon: (item: IContextualMenuItem) => iconRenderer({ contextualMenuItem: item }), + }, + renderer && + ({ + onRender: (item, dismissMenu) => renderer({ item, dismissMenu }), + } as Pick) + ) as IContextualMenuItem; + } } 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 f2cb776..ff0fe80 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 @@ -198,10 +198,9 @@ export class FabCommandBarComponent extends ReactWrapperComponent iconRenderer({ contextualMenuItem: item }), - } as any) /* NOTE: Fix for wrong typings of `onRenderIcon` in office-ui-fabric-react */, + iconRenderer && { + onRenderIcon: (item: IContextualMenuItem) => iconRenderer({ contextualMenuItem: item }), + }, renderer && ({ onRender: (item, dismissMenu) => renderer({ item, dismissMenu }) } as Pick) ) as ICommandBarItemProps; diff --git a/libs/fabric/src/lib/components/command-bar/directives/command-bar-item.directives.ts b/libs/fabric/src/lib/components/command-bar/directives/command-bar-item.directives.ts index 3646f8a..6002ca8 100644 --- a/libs/fabric/src/lib/components/command-bar/directives/command-bar-item.directives.ts +++ b/libs/fabric/src/lib/components/command-bar/directives/command-bar-item.directives.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { AfterContentInit, ContentChild, Directive, Input, TemplateRef } from '@angular/core'; +import { ContentChild, Directive, Input, TemplateRef } from '@angular/core'; import { ContextualMenuItemDirective } from '../../contextual-menu/directives/contextual-menu-item.directive'; import { ItemChangedPayload } from '../../core/declarative/item-changed.payload'; import { @@ -29,11 +29,7 @@ export class CommandBarItemRenderIconDirective { } @Directive({ selector: 'fab-command-bar-item' }) -export class CommandBarItemDirective extends ContextualMenuItemDirective - implements ICommandBarItemOptions, AfterContentInit { - @ContentChild(CommandBarItemRenderDirective) readonly renderDirective: CommandBarItemRenderDirective; - @ContentChild(CommandBarItemRenderIconDirective) readonly renderIconDirective: CommandBarItemRenderIconDirective; - +export class CommandBarItemDirective extends ContextualMenuItemDirective implements ICommandBarItemOptions { // ICommandBarItemOptions implementation @Input() iconOnly?: ICommandBarItemOptions['iconOnly']; @Input() tooltipHostProps?: ICommandBarItemOptions['tooltipHostProps']; @@ -41,18 +37,4 @@ export class CommandBarItemDirective extends ContextualMenuItemDirective @Input() cacheKey?: ICommandBarItemOptions['cacheKey']; @Input() renderedInOverflow?: ICommandBarItemOptions['renderedInOverflow']; @Input() commandBarButtonAs?: ICommandBarItemOptions['commandBarButtonAs']; - @Input() render: ICommandBarItemOptions['render']; - @Input() renderIcon: ICommandBarItemOptions['renderIcon']; - - ngAfterContentInit() { - super.ngAfterContentInit(); - - if (this.renderDirective && this.renderDirective.templateRef) { - this.render = this.renderDirective.templateRef; - } - - if (this.renderIconDirective && this.renderIconDirective.templateRef) { - this.renderIcon = this.renderIconDirective.templateRef; - } - } } diff --git a/libs/fabric/src/lib/components/contextual-menu/contextual-menu.module.ts b/libs/fabric/src/lib/components/contextual-menu/contextual-menu.module.ts index ecdfb2a..f29e38a 100644 --- a/libs/fabric/src/lib/components/contextual-menu/contextual-menu.module.ts +++ b/libs/fabric/src/lib/components/contextual-menu/contextual-menu.module.ts @@ -3,9 +3,17 @@ import { CommonModule } from '@angular/common'; import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ContextualMenuItemDirective } from './directives/contextual-menu-item.directive'; +import { + ContextualMenuItemDirective, + ContextualMenuItemRenderDirective, + ContextualMenuItemRenderIconDirective, +} from './directives/contextual-menu-item.directive'; -const components = [ContextualMenuItemDirective]; +const components = [ + ContextualMenuItemDirective, + ContextualMenuItemRenderDirective, + ContextualMenuItemRenderIconDirective, +]; @NgModule({ imports: [CommonModule], diff --git a/libs/fabric/src/lib/components/contextual-menu/directives/contextual-menu-item.directive.ts b/libs/fabric/src/lib/components/contextual-menu/directives/contextual-menu-item.directive.ts index b53b2bd..da641e9 100644 --- a/libs/fabric/src/lib/components/contextual-menu/directives/contextual-menu-item.directive.ts +++ b/libs/fabric/src/lib/components/contextual-menu/directives/contextual-menu-item.directive.ts @@ -10,14 +10,38 @@ import { OnDestroy, Output, QueryList, + ContentChild, + TemplateRef, } from '@angular/core'; import { IContextualMenuItem } from 'office-ui-fabric-react'; +import { KnownKeys, InputRendererOptions } from '@angular-react/core'; import { OnChanges } from '../../../declarations/angular/typed-changes'; import { ItemChangedPayload } from '../../core/declarative/item-changed.payload'; import { ChangeableItemsHelper, IChangeableItemsContainer } from '../../core/shared/changeable-helper'; import { ChangeableItemDirective } from '../../core/shared/changeable-item.directive'; +export type ContextualMenuItemChangedPayload = ItemChangedPayload< + IContextualMenuItemOptions['key'], + IContextualMenuItemOptions +>; + +/** + * Wrapper directive to allow rendering a custom item to a ContextualMenuItem. + */ +@Directive({ selector: 'fab-command-bar-item > render' }) +export class ContextualMenuItemRenderDirective { + @ContentChild(TemplateRef) readonly templateRef: TemplateRef; +} + +/** + * Wrapper directive to allow rendering a custom icon to a ContextualMenuItem. + */ +@Directive({ selector: 'fab-command-bar-item > render-icon' }) +export class ContextualMenuItemRenderIconDirective { + @ContentChild(TemplateRef) readonly templateRef: TemplateRef; +} + @Directive({ selector: 'contextual-menu-item' }) export class ContextualMenuItemDirective extends ChangeableItemDirective implements @@ -27,13 +51,15 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective, OnDestroy { @ContentChildren(ContextualMenuItemDirective) readonly menuItemsDirectives: QueryList; + @ContentChild(ContextualMenuItemRenderDirective) readonly renderDirective: ContextualMenuItemRenderDirective; + @ContentChild(ContextualMenuItemRenderIconDirective) + readonly renderIconDirective: ContextualMenuItemRenderIconDirective; @Input() componentRef?: IContextualMenuItem['componentRef']; @Input() text?: IContextualMenuItem['text']; @Input() secondaryText?: IContextualMenuItem['secondaryText']; @Input() itemType?: IContextualMenuItem['itemType']; @Input() iconProps?: IContextualMenuItem['iconProps']; - @Input() onRenderIcon?: IContextualMenuItem['onRenderIcon']; @Input() submenuIconProps?: IContextualMenuItem['submenuIconProps']; @Input() disabled?: IContextualMenuItem['disabled']; @Input() primaryDisabled?: IContextualMenuItem['primaryDisabled']; @@ -54,29 +80,39 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective(); @Output() get onChildItemChanged(): EventEmitter> { - return this.changeableItemsHelper && this.changeableItemsHelper.onChildItemChanged; - } - @Input() - get onItemsChanged(): EventEmitter>> { - return this.changeableItemsHelper && this.changeableItemsHelper.onItemsChanged; + return this._changeableItemsHelper && this._changeableItemsHelper.onChildItemChanged; } - private changeableItemsHelper: ChangeableItemsHelper; + @Output() + get onItemsChanged(): EventEmitter>> { + return this._changeableItemsHelper && this._changeableItemsHelper.onItemsChanged; + } + + private _changeableItemsHelper: ChangeableItemsHelper; ngAfterContentInit() { - this.changeableItemsHelper = new ChangeableItemsHelper(this.menuItemsDirectives, this, nonSelfDirective => { + if (this.renderDirective && this.renderDirective.templateRef) { + this.render = this.renderDirective.templateRef; + } + + if (this.renderIconDirective && this.renderIconDirective.templateRef) { + this.renderIcon = this.renderIconDirective.templateRef; + } + + this._changeableItemsHelper = new ChangeableItemsHelper(this.menuItemsDirectives, this, nonSelfDirective => { const items = nonSelfDirective.map(directive => this._directiveToContextualMenuItem(directive as any)); if (!this.subMenuProps) { this.subMenuProps = { items: items }; @@ -87,7 +123,7 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective + extends Pick, 'onRender' | 'onRenderIcon'>> { + readonly renderIcon?: InputRendererOptions; + readonly render?: InputRendererOptions; + readonly data?: TData; +} + +export interface IContextualMenuItemOptionsRenderContext { + item: any; + dismissMenu: (ev?: any, dismissAll?: boolean) => void; +} + +export interface IContextualMenuItemOptionsRenderIconContext { + contextualMenuItem: IContextualMenuItem; +}