lib/ngx-responsivemenu/components/responsive-menu.component.ts
Responsive menu component, all items which are passed should be from type ResponsiveMenuItem or ResponsiveMenuToggle. All other items will never rendered into dom
AfterViewInit
AfterContentInit
OnDestroy
<ngx-responsivemenu>
<button type="button" ngxResponsiveMenuItem ...>Btn</button>
</ngx-responsivemenu>
selector | ngx-responsivemenu |
styleUrls | ./responsive-menu.component.scss |
templateUrl | responsive-menu.component.html |
Properties |
|
Methods |
|
Inputs |
Outputs |
Accessors |
Public
constructor(overflowCtrl: OverflowControl, renderer: Renderer2, hostEl: ElementRef, changeDetector: ChangeDetectorRef)
|
|||||||||||||||
Creates an instance of ResponsiveMenuComponent.
Parameters :
|
alignToggle | |
Type : BtnAlign
|
|
Default value : BtnAlign.RIGHT
|
|
set position of toggle btn, possible values are left or right |
classBtnPane | |
Type : string
|
|
add a class for the button pane as example for bootstrap btn-group |
classOverflow | |
Type : string
|
|
add class to overflow container, only for default overflow if option customOverflow is passed this class will not added to custom overflow, simply use [ngClass]="'className'" if you want to add a custom class |
customOverflow | |
Default value : false
|
|
if true content will not rendered longer in default container for overflow content and should rendered in a custom overflow container. |
forceOverflow | |
Type : boolean
|
|
if true toggle button will allways be visible even if content fits into button pane |
showMax | |
Default value : -1
|
|
set maximal amount of items which could rendered into button pane all other items will automatically add to overflow container |
rendered | |
Type : EventEmitter<void>
|
|
emits if responsive menu has been completed rendering process |
Public update | ||||||
update(width?: number)
|
||||||
update view, this will remove all contents and rerender buttons
Parameters :
Returns :
void
|
forceOverflow | ||||||
setforceOverflow(forced: boolean)
|
||||||
if true toggle button will allways be visible even if content fits into button pane
Parameters :
Returns :
void
|
classOverflow | ||||||
setclassOverflow(name: string)
|
||||||
add class to overflow container, only for default overflow if option customOverflow is passed this class will not added to custom overflow, simply use [ngClass]="'className'" if you want to add a custom class
Parameters :
Example :
Returns :
void
|
defaultToggleBtn | ||||
setdefaultToggleBtn(btn)
|
||||
static: false wait until change detection loop has been finished in this case button el will not rendered to dom if a custom button is given but we have to wait until change detection finished before we get it
Parameters :
Returns :
void
|
customToggleButton | ||||
setcustomToggleButton(btn)
|
||||
check if custom button is defined so we dont need to render default more button
Parameters :
Returns :
void
|
import {
AfterContentInit,
AfterViewInit,
Component,
ContentChildren,
ElementRef,
Input,
OnDestroy,
QueryList,
Renderer2,
ViewChild,
ContentChild,
ChangeDetectorRef,
Output,
EventEmitter,
} from "@angular/core";
import { MenuItemDirective } from "../directives/menu-item.directive";
import { takeUntil } from "rxjs/operators";
import { Subject } from "rxjs";
import { OverflowControl } from "../provider/overflow.control";
import { MenuToggleDirective } from "../directives/menu-toggle.directive";
/**
* possible toggle button alignments
*/
export enum BtnAlign {
LEFT = "left",
RIGHT = "right"
}
/**
* @ignore
*/
interface CssClasses {
[key: string]: boolean;
}
/**
* Responsive menu component, all items which are passed should be from type
* ResponsiveMenuItem or ResponsiveMenuToggle. All other items will never rendered
* into dom
*
* @example
* <ngx-responsivemenu>
* <button type="button" ngxResponsiveMenuItem ...>Btn</button>
* </ngx-responsivemenu>
*/
@Component({
selector: "ngx-responsivemenu",
templateUrl: "responsive-menu.component.html",
styleUrls: ["./responsive-menu.component.scss"]
})
export class ResponsiveMenuComponent implements AfterViewInit, AfterContentInit, OnDestroy {
/**
* if true default toggle button will not rendered anymore, will be set if a custom item
* has been added to content from type MenuToggleDirective
*
* @ignore
* @example
* <ngx-responsivemenu>
* ...
* <button type="button" ngxResponsiveMenuToggle>Button</div>
* </ngx-responsivemenu>
*/
public isCustomButton = false;
/**
* overflow css classes
*
* @ignore
*/
public overflowClasses: CssClasses = { overflow: true };
/**
* Get querylist for all content items from type MenuItemDirective.
* Will also subscribe to querylist to get notified something changes so we can
* rerender menu
*/
@ContentChildren(MenuItemDirective)
public menuItems: QueryList<MenuItemDirective>;
/**
* set maximal amount of items which could rendered into button pane
* all other items will automatically add to overflow container
*/
@Input()
public showMax = -1;
/**
* if true content will not rendered longer in default container for overflow
* content and should rendered in a custom overflow container.
*
* @example
*
* <ngx-responsive-menu [customOverflow]="true">
* <button type="button" ngxResponsiveMenuItem class="btn btn-sm btn-secondary" *ngFor="let item of items">{{label}}</button>
* </ngx-responsive-menu>
*
* <div class="superAwesomeOverflow">
* <!-- overflow will rendered here now -->
* <ngx-responsivemenu-overflow></ngx-responsivemenu-overflow>
* </div>
*/
@Input()
public customOverflow = false;
/**
* if true toggle button will allways be visible even if content
* fits into button pane
*/
@Input()
public set forceOverflow(forced: boolean) {
this.isForcedOverflow = forced;
this.overflowCtrl.forceOverflow = forced;
}
/**
* add a class for the button pane as example for bootstrap btn-group
*
* @example
* <ngx-responsive-menu [classBtnPane]="'btn-group'">
* <button type="button" ngxResponsiveMenuItem class="btn btn-sm btn-secondary" *ngFor="let item of items">{{label}}</button>
* </ngx-responsive-menu>
*/
@Input()
public classBtnPane: string;
/**
* add class to overflow container, only for default overflow if option customOverflow is passed
* this class will not added to custom overflow, simply use [ngClass]="'className'" if you want to add
* a custom class
*
* @example
* <ngx-responsive-menu [classOverflow]="'overflow-container'">
* ...
* </ngx-responsive-menu>
*/
@Input()
public set classOverflow(name: string) {
this.overflowClasses[name] = true;
}
/**
* set position of toggle btn, possible values are left or right
*/
@Input()
public alignToggle: BtnAlign = BtnAlign.RIGHT;
/**
* emits if responsive menu has been completed rendering process
*/
@Output()
rendered: EventEmitter<void> = new EventEmitter();
/**
* static: false wait until change detection loop has been finished in this case
* button el will not rendered to dom if a custom button is given but we have to wait
* until change detection finished before we get it
*/
@ViewChild(MenuToggleDirective, {read: MenuToggleDirective, static: false})
protected set defaultToggleBtn( btn: MenuToggleDirective ) {
if ( !this.toggleBtn ) {
this.toggleBtn = btn;
}
}
/**
* check if custom button is defined so we dont need to render default more button
*/
@ContentChild(MenuToggleDirective, {read: MenuToggleDirective, static: true})
protected set customToggleButton( btn: MenuToggleDirective ) {
this.isCustomButton = Boolean( btn );
if (btn) {
this.toggleBtn = btn;
}
}
/**
* button pane where items will be rendered if they fits into
*/
@ViewChild( "buttonPane", { read: ElementRef, static: true } )
private buttonPane: ElementRef;
/**
* temporary button pane where buttons will be rendered on render process
* to avoid visualization errors
*/
@ViewChild( "tmpButtonPane", { read: ElementRef, static: true } )
private tmpButtonPane: ElementRef;
/**
* if true toggle button will allways included to button pane and be visible
*/
private isForcedOverflow = false;
/**
* emits true if component gets destroyed
*/
private isDestroyed$: Subject<boolean> = new Subject();
/**
* toggle button to show / close overflow
*/
private toggleBtn: MenuToggleDirective;
/**
* possible overflow items, which fits into button bar but not with more button
* but since the next button could be the last button and be smaller then overflow button
* which could results into that all buttons fits into the bar we only have to mark this button
* for an overflow button
*/
private possibleOverflowItems: MenuItemDirective[] = [];
/**
* all overflow items which exists
*/
private overflowItems: MenuItemDirective[] = [];
/** max width of button bar */
private maxWidth: number;
/** reserved width which we will need to show more button */
private reservedWidth: number;
/**
* Creates an instance of ResponsiveMenuComponent.
*/
public constructor(
private overflowCtrl: OverflowControl,
private renderer: Renderer2,
private hostEl: ElementRef,
private changeDetector: ChangeDetectorRef
) {
}
/**
* component gets destroyed
*/
public ngOnDestroy() {
this.isDestroyed$.next( true );
}
/**
* after content has initialized register to QueryList
* to get notified about changes
*/
public ngAfterContentInit() {
this.menuItems.changes
.pipe(takeUntil(this.isDestroyed$))
.subscribe(() => this.update());
}
/**
* after view has been initialized and custom button exists
* append custom toggle button to button pane
*/
public ngAfterViewInit() {
if (this.isCustomButton) {
this.renderer.appendChild( this.buttonPane.nativeElement, this.toggleBtn.nativeElement );
}
this.render();
}
/**
* update view, this will remove all
* contents and rerender buttons
*/
public update(width?: number) {
this.render(width);
this.overflowCtrl.update();
}
/**
* remove old items from view so we ensure we have a clean tree
*/
private clearView() {
this.menuItems.forEach((menuItem: MenuItemDirective) => {
menuItem.remove();
});
this.changeDetector.detectChanges();
}
/**
* render buttons to button menubar, if they not fits anymore or
* max show count is reached put them directly to the overflow container
*/
private render(width?: number) {
this.initRenderProcess();
this.maxWidth = width || this.calcHostWidth();
const items = this.prepareMenuItems();
let isOverflow = false;
for ( let index = 0, count = 0, ln = items.length; index < ln; index++ , count++ ) {
const item = items[index];
isOverflow = isOverflow || this.showMax > -1 && count >= this.showMax;
if ( !isOverflow ) {
item.addTo(this.tmpButtonPane.nativeElement);
if (this.validateSize(item)) {
continue;
}
isOverflow = true;
}
this.overflowItems.push(item);
}
this.finalizeRenderProcess();
}
/**
* initialize render process
* clean up all views, get dimensions from elements
*/
private initRenderProcess() {
this.clearView();
this.overflowItems = [];
this.possibleOverflowItems = [];
this.overflowCtrl.data.items = [];
this.toggleBtn.display = true;
this.reservedWidth = this.toggleBtn.width;
this.toggleBtn.display = false;
}
/**
* finialize render process, enable more button if an overflow exists
*/
private finalizeRenderProcess() {
const overflowData = this.finalizeMenuItems();
this.overflowCtrl.data.items = overflowData;
this.toggleBtn.display = this.isForcedOverflow || overflowData.length > 0;
this.possibleOverflowItems = [];
this.overflowItems = [];
this.changeDetector.detectChanges();
this.rendered.emit();
}
/**
* prepare menu items, filter out items which should be hidden by default
* and put them to overflow
*/
private prepareMenuItems(): MenuItemDirective[] {
return this.menuItems.reduce<MenuItemDirective[]>((itemCollection, menuItem) => {
menuItem.visible
? itemCollection.push(menuItem)
: this.overflowItems.push(menuItem);
return itemCollection;
}, []);
}
/**
* finalize menu buttons after render process finished
* buttons which are in overflow will removed from dom and pushed to overflow array
* all others will added to buttonPane
*/
private finalizeMenuItems(): MenuItemDirective[] {
const items = this.overflowItems.length
? this.possibleOverflowItems.concat(this.overflowItems)
: [];
return this.menuItems.toArray().reduce((overflowItems, item) => {
// remove all items so they are not rendered anymore
item.remove();
if (!items.length || items.indexOf(item) === -1) {
this.alignToggle === BtnAlign.LEFT
? item.addTo(this.buttonPane.nativeElement)
: item.addTo(this.buttonPane.nativeElement, this.toggleBtn.nativeElement);
return overflowItems;
}
/** push item to overflow */
overflowItems.push(item);
return overflowItems;
}, []);
}
/**
* validate rendered item fits into button container
*/
private validateSize( item: MenuItemDirective ): boolean {
const usedSize = parseInt(this.tmpButtonPane.nativeElement.offsetWidth, 10);
/** item fits together with more button */
if (usedSize + this.reservedWidth <= this.maxWidth) {
return true;
}
/**
* menu item dosent fit into pane with toggle button
* but since this could be the last item, or the next item
* which come is smaller as the toggle button we push this
* into possible overflow items, unless toggle button should shown
* allways
*/
if ( usedSize <= this.maxWidth && !this.isForcedOverflow) {
this.possibleOverflowItems.push( item );
return true;
}
/**
* item dont fits anymore this is an overflow
*/
return false;
}
/**
* calculate inner width from host element
*/
private calcHostWidth(): number {
const hostNode: HTMLElement = this.hostEl.nativeElement;
const hostStyle = getComputedStyle(hostNode);
let width = Math.floor(hostNode.getBoundingClientRect().width);
/** @todo check better solution to not calculate this every time */
width += parseInt(hostStyle.getPropertyValue("border-left-width"), 10);
width += parseInt(hostStyle.getPropertyValue("border-right-width"), 10);
width += parseInt(hostStyle.getPropertyValue("padding-right"), 10);
width += parseInt(hostStyle.getPropertyValue("padding-left"), 10);
return width;
}
}
<!-- visible buttons -->
<div class="buttonWrapper">
<!-- should use tmp button pane to render buttons first -->
<div #tmpButtonPane class="buttonPane" [ngClass]="classBtnPane"></div>
<div #buttonPane class="buttonPane" [ngClass]="classBtnPane">
<button type="button" ngxResponsiveMenuToggle *ngIf="!isCustomButton">...</button>
</div>
</div>
<!-- default container where over items will be rendered in -->
<ngx-responsivemenu-overflow *ngIf="!customOverflow" [ngClass]="overflowClasses"></ngx-responsivemenu-overflow>
./responsive-menu.component.scss
:host {
display: flex;
flex-direction: column;
justify-content: center;
.buttonWrapper {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.buttonPane {
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
flex-shrink: 0;
}
::ng-deep {
.responsive-menu--item {
flex-shrink: 0 !important;
}
}
}