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
This commit is contained in:
Ben Grynhaus 2018-12-19 17:02:05 +02:00 коммит произвёл GitHub
Родитель b307c6fa34
Коммит 0bf7d9ab37
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 193 добавлений и 38 удалений

Просмотреть файл

@ -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<IButtonProps> implements OnInit {
export abstract class FabBaseButtonComponent extends ReactWrapperComponent<IButtonProps>
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<IButt
@Output() readonly onMenuClick = new EventEmitter<{ ev?: MouseEvent | KeyboardEvent; button?: IButtonProps }>();
@Output() readonly onAfterMenuDismiss = new EventEmitter<void>();
@ContentChildren(ContextualMenuItemDirective) readonly menuItemsDirectives?: QueryList<ContextualMenuItemDirective>;
onRenderIcon: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
onRenderText: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
onRenderDescription: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
@ -54,6 +77,9 @@ export abstract class FabBaseButtonComponent extends ReactWrapperComponent<IButt
onRenderChildren: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
onRenderMenuIcon: (props?: IButtonProps, defaultRender?: JsxRenderFunc<IButtonProps>) => JSX.Element;
private _changeableItemsHelper: ChangeableItemsHelper<IContextualMenuItem>;
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<IButt
this.onRenderMenuIcon = this.createRenderPropHandler(this.renderMenuIcon);
}
ngAfterContentInit() {
if (this.menuItemsDirectives && this.menuItemsDirectives.length > 0) {
const setItems = (directiveItems: ReadonlyArray<ContextualMenuItemDirective>) => {
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<ContextualMenuItemDirective>) => {
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<HTMLElement> | React.KeyboardEvent<HTMLElement>, button?: IButtonProps) {
this.onMenuClick.emit({
ev: ev && ev.nativeEvent,
@ -80,4 +150,46 @@ export abstract class FabBaseButtonComponent extends ReactWrapperComponent<IButt
onClickHandler(ev?: React.MouseEvent) {
this.onClick.emit(ev.nativeEvent);
}
private _directiveToContextualMenuItem(directive: ContextualMenuItemDirective): IContextualMenuItemOptions {
return {
...omit(
directive,
'menuItemsDirectives',
'renderDirective',
'renderIconDirective',
'click',
'onItemChanged',
'onItemsChanged',
'onChildItemChanged',
'ngOnInit',
'ngOnChanges',
'ngOnDestroy',
'ngAfterContentInit'
),
onClick: (ev, item) => {
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<IContextualMenuItem, 'onRender'>)
) as IContextualMenuItem;
}
}

Просмотреть файл

@ -198,10 +198,9 @@ export class FabCommandBarComponent extends ReactWrapperComponent<ICommandBarPro
return Object.assign(
{},
sharedProperties,
iconRenderer &&
({
onRenderIcon: (item: IContextualMenuItem) => 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<ICommandBarItemProps, 'onRender'>)
) as ICommandBarItemProps;

Просмотреть файл

@ -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;
}
}
}

Просмотреть файл

@ -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],

Просмотреть файл

@ -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<IContextualMenuItemOptionsRenderContext>;
}
/**
* 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<IContextualMenuItemOptionsRenderIconContext>;
}
@Directive({ selector: 'contextual-menu-item' })
export class ContextualMenuItemDirective extends ChangeableItemDirective<IContextualMenuItem>
implements
@ -27,13 +51,15 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective<IContex
OnChanges<ContextualMenuItemDirective>,
OnDestroy {
@ContentChildren(ContextualMenuItemDirective) readonly menuItemsDirectives: QueryList<ContextualMenuItemDirective>;
@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<IContex
@Input() style?: IContextualMenuItem['style'];
@Input() ariaLabel?: IContextualMenuItem['ariaLabel'];
@Input() title?: IContextualMenuItem['title'];
@Input() onRender?: IContextualMenuItem['onRender'];
@Input() onMouseDown?: IContextualMenuItem['onMouseDown'];
@Input() role?: IContextualMenuItem['role'];
@Input() customOnRenderListLength?: IContextualMenuItem['customOnRenderListLength'];
@Input() keytipProps?: IContextualMenuItem['keytipProps'];
@Input() inactive?: IContextualMenuItem['inactive'];
@Input() name?: IContextualMenuItem['name'];
@Input() render: IContextualMenuItemOptions['render'];
@Input() renderIcon: IContextualMenuItemOptions['renderIcon'];
@Output() readonly click = new EventEmitter<{ ev?: MouseEvent | KeyboardEvent; item?: IContextualMenuItem }>();
@Output()
get onChildItemChanged(): EventEmitter<ItemChangedPayload<string, IContextualMenuItem>> {
return this.changeableItemsHelper && this.changeableItemsHelper.onChildItemChanged;
}
@Input()
get onItemsChanged(): EventEmitter<QueryList<ChangeableItemDirective<IContextualMenuItem>>> {
return this.changeableItemsHelper && this.changeableItemsHelper.onItemsChanged;
return this._changeableItemsHelper && this._changeableItemsHelper.onChildItemChanged;
}
private changeableItemsHelper: ChangeableItemsHelper<IContextualMenuItem>;
@Output()
get onItemsChanged(): EventEmitter<QueryList<ChangeableItemDirective<IContextualMenuItem>>> {
return this._changeableItemsHelper && this._changeableItemsHelper.onItemsChanged;
}
private _changeableItemsHelper: ChangeableItemsHelper<IContextualMenuItem>;
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<IContex
}
ngOnDestroy() {
this.changeableItemsHelper.destroy();
this._changeableItemsHelper.destroy();
}
private _directiveToContextualMenuItem(directive: ContextualMenuItemDirective): IContextualMenuItem {
@ -99,3 +135,21 @@ export class ContextualMenuItemDirective extends ChangeableItemDirective<IContex
};
}
}
// Not using `Omit` here since it confused the TypeScript compiler and it just showed the properties listed here (`renderIcon`, `render` and `data`).
// The type here is just `Omit` without the generics though.
export interface IContextualMenuItemOptions<TData = any>
extends Pick<IContextualMenuItem, Exclude<KnownKeys<IContextualMenuItem>, 'onRender' | 'onRenderIcon'>> {
readonly renderIcon?: InputRendererOptions<IContextualMenuItemOptionsRenderIconContext>;
readonly render?: InputRendererOptions<IContextualMenuItemOptionsRenderContext>;
readonly data?: TData;
}
export interface IContextualMenuItemOptionsRenderContext {
item: any;
dismissMenu: (ev?: any, dismissAll?: boolean) => void;
}
export interface IContextualMenuItemOptionsRenderIconContext {
contextualMenuItem: IContextualMenuItem;
}