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:
Родитель
b307c6fa34
Коммит
0bf7d9ab37
|
@ -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;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче