I am using Angular 8 with Angular Materials to build a multi-level menu. I can get the menu to work by using recursion for each level. I recursively call the same directive that displays each level of the menu.
This all works, and the menu is built as expected. However, the menu does not behave as expected. Examples I have seen, when you hover over an item, the nest item is opened, and if you move off an item, its child is closed.
For example, this is a simple version I made:
https://stackblitz.com/edit/dynamic-sidenav-multi-level-menu-tvim5b?file=app/app.component.html
Problem
My issue is when I build my menu, if I click on an item, the child opens. However, I can never get the child to close unless I click off the menu altogether. It is not behaving like the above example.
Question
How can I get my example to be have like the above, and close menu items (children) when the items loses focus?
Info
I have not put my specific example in StackBlitz because I don't own the code, and it needs backend services to support the implementation.
I think my issues are because I am building the menu items recursively , and the [matMenuTriggerFor] is referencing the menu in the next recursion.
Code
sidenav-list.component.html
<mat-nav-list>
<!-- Add the Home item -->
<a mat-list-item routerLink="/home" (click)="onSidenavClose()"><mat-icon>home</mat-icon><span class="nav-caption">Home</span></a>
<!-- Recurse over the app-sidenav-item -->
<app-sidenav-item *ngFor="let item of navItems" [item]="item" [depth]="depth+1" [sidenavClose]="sidenavClose"></app-sidenav-item>
</mat-nav-list>
sidenav-item.component.html <app-sidenav-item>
<div>
<button mat-button *ngIf="depth === 1" [matMenuTriggerFor]="menu"><mat-icon>play_arrow</mat-icon>{{item.name | titlecase}}</button>
<button mat-menu-item *ngIf="depth > 1" [matMenuTriggerFor]="menu">{{item.name}}</button>
<mat-menu #menu="matMenu">
<button *ngIf="item.actions.getItems" mat-menu-item (click)="onItemSelected(item, 0)"><mat-icon>list</mat-icon>Get Items</button>
<button *ngIf="item.actions.updateItem" mat-menu-item (click)="onItemSelected(item, 1)"><mat-icon>edit</mat-icon>Update Items</button>
<button *ngIf="item.actions.addItem" mat-menu-item (click)="onItemSelected(item, 2)"><mat-icon>add</mat-icon>Add Item</button>
<app-sidenav-item *ngFor="let child of item.children" [item]="child" [depth]="depth+1" [sidenavClose]="sidenavClose"></app-sidenav-item>
</mat-menu>
</div>
Screen Print
As you can see, I am able to open more than one item on separate nodes. I cannot get the previous one to close. Also, it only responds to clicks and not mouse hover.
Ok here is the thing, You have to preprocess some data 2 way, that means in your object you have to know if it has children to enable more hierarchy level and you need to know which parent it came from to filter it to build this
and your html should look like this. Since you know you can go 3 - 4 levels you generate template for those levels and play with data when it is there.
There is also another #input for MatMenu called matMenuTriggerData with which the parent will trigger data to child.
<button mat-button [matMenuTriggerFor]="level1" [matMenuTriggerData]="getData(null, 1)">Animal index</button>
<mat-menu #level1="matMenu">
<ng-template matMenuContent let-data="data">
<ng-template ngFor let-item [ngForOf]="data">
<button mat-menu-item *ngIf="item.children" [matMenuTriggerFor]="level2"
[matMenuTriggerData]="getData(item, 2)">{{item.label}}</button>
<button mat-menu-item *ngIf="!item.children">{{item.label}}</button>
</ng-template>
</ng-template>
</mat-menu>
<mat-menu #level2="matMenu">
<ng-template matMenuContent let-data="data">
<ng-template ngFor let-item [ngForOf]="data">
<button mat-menu-item *ngIf="item.children" [matMenuTriggerFor]="level3"
[matMenuTriggerData]="getData(item, 3)">{{item.label}}</button>
<button mat-menu-item *ngIf="!item.children">{{item.label}}</button>
</ng-template>
</ng-template>
</mat-menu>
<mat-menu #level3="matMenu">
<ng-template matMenuContent let-data="data">
<button mat-menu-item *ngFor="let item of data">{{item.label}}</button>
</ng-template>
</mat-menu>
Note the last level has no more triggers.
you can write a function for your filtered data
getData(selected, requested) {
return selected ? {
data:
this['level' + requested].filter(item => item.parent === selected.value)
} : { data: this.level1 };
}
Each item will contain value, label, parent and hasChildren in different levels, you can directly hook up with api make sure the object is passed has a data attribute , see functon getData
You can checkout this solution at https://stackblitz.com/edit/angular-yfslub
Hope you can modify to your needs.
Related
I have a list of child components inside a ngFor:
<ng-container *ngFor="let field of fields">
<button (click)="show = !show">Show</button>
<ng-container *ngIf="show">
<app-field [fieldInfo]="field"></app-field>
</ng-container>
</ng-container>
I want to only show this specific app-field component if show is true, but this is obviously not the way, because it sets show to true for all of the rendered components.
My problem is really that I cant show all of the app-fields because they turn my app into a slow mess when they are displayed (there will be hundreds of them), so I only want to display them when they are needed.
How can I toggle the rendering of each specific component on/off uppon click? Or is there som other solution I could look into?
show variable is global and updating value for show will reflect for all the app-field components, Thus You need to determine and specify a show/hide variable for each component individually.
To do that you should add a show member inside the field Object as follow:
<ng-container *ngFor="let field of fields">
<button (click)="field.show = !field.show">Show</button>
<ng-container *ngIf="field.show">
<app-field [fieldInfo]="field"></app-field>
</ng-container>
</ng-container>
Also found similar solution, check this answer
I needed to only have the component be attached to the DOM when set to be visible. Solved it with the answer in: Thierry Templier answer here
Inside parent component:
public showField: any = {};
Then in parent component template:
<ng-container *ngFor="let field of fields">
<button (click)="showField[childField.id] = !showField[childField.id]">Show</button>
<ng-container *ngIf="showField[childField.id]">
<app-field [fieldInfo]="field"></app-field>
</ng-container>
</ng-container>
This destroys the component when *ngIf is false, which is what I needed in this situation.
Thanks to #yazan for leading me to the answer.
I'm implementing a toolbar where each route (nav buttons) I hover on will display a popover, similar to bootstrap, containing the component I send (kind of like the navbar items here).
<div *ngFor="let route of toolbarRoutes">
<button mat-stroked-button class="toolbar-nav-button" (click)="routeTo(route.path)">
{{route.displayName}}
</button>
<app-hoverable *ngIf="route.component" [component]="route.component"></app-hoverable>
</div>
My problem is that I want to render/show app-hoverable only if the button (<button mat-stroked-button class="toolbar-nav-button" (click)="routeTo(route.path)">) is hovered, how can i do this?
Simply like this (I have stacked attributes for readability) :
<div *ngFor="let route of toolbarRoutes">
<button mat-stroked-button
class="toolbar-nav-button"
(click)="routeTo(route.path)"
(mouseenter)="route.showHoverable=true"
(mouseleave)="route.showHoverable=false">
{{route.displayName}}
</button>
<app-hoverable *ngIf="route.showHoverable" [component]="route.component"></app-hoverable>
</div>
I am dynamically creating a list of elements on a page in Angular 6. Each item on the list has a button on the right that, when clicked, opens a dropdown menu with a delete option. Because it's a dropdown, I need to hide/show it when it's clicked on. I'm not sure what the best practice is for doing so in Angular.
I don't think I can use ngClass, as these are dynamically created, and if I had dropdownIsToggled: boolean = false because it would open every dropdown instead of the specific one that was clicked. I am still somewhat inexperienced with Angular and I want to know what the best practice would be in this situation.
So far I have this html inside an ngFor
<span class="item-dropdown">
<button class="item-dropdown-button" (click)="toggleDropdown()">
<i class="bicon bicon-more-vertical bicon-md"></i>
</button>
<div *ngIf="dropdownIsToggled" class="item-dropdown-menu shadow-md">
<ul>
<li>
<button class="list-item"
(click)="deleteItem(item.id)">
delete
</button>
</li>
<li>
<button class="list-item" (click)="markRead(item.id)">
mark as read
</button>
</li>
</ul>
</div>
</span>
And here is my non-working toggle function (as it toggles every dropdown in the list when clicked) in the component
dropdownIsToggled: boolean = false;
toggleDropdown() {
this.dropdownIsToggled = !this.dropdownIsToggled;
}
My immediate idea is to set each item to have an id of the index of the loop *ngFor="let item of items; let i = index", pass the index through the toggleDropdown function, and then add/remove classes to hide/show the dropdown in that way, but I am not sure of best practices in this situation, and I would like input. Is it going against best practice to change css classes from the component?
Thanks!
I would recommend you to add the dropdownIsToggled to the itemmodel.
Like
interface Item {
id: any,
dropdownIsToggled: boolean,
... other props
}
In your html you do something like
<span class="item-dropdown">
<button class="item-dropdown-button" (click)="toggleDropdown(item)">
<i class="bicon bicon-more-vertical bicon-md"></i>
</button>
<div *ngIf="item.dropdownIsToggled" class="item-dropdown-menu shadow-md">
<ul>
<li>
<button class="list-item"
(click)="deleteItem(item.id)">
delete
</button>
</li>
<li>
<button class="list-item" (click)="markRead(item.id)">
mark as read
</button>
</li>
</ul>
</div>
TS:
toggleDropdown(item) {
item.dropdownIsToggled = !item.dropdownIsToggled;
}
If you don't want to add UI specific properties to your model, you can do something like this:
interface Item {
id: any,
... other props
}
interface ItemUI extends Item {
dropdownIsToggled: boolean,
}
In this way you can provide a clean state for every item.
I am stuck at this issue i have a list of menu item
<md-menu-item ng-value="menuItem.value" ng-repeat="menuItem in filtermenu.menuItems" ng-click="activeFilterCtrl.selectedfilter(menuItem)" translate>
<md-button>
{{ menuItem.name }}
</md-button>
</md-menu-item>
Following is the code i want to display the value of selected menu item on a button or on a label which should be displayed after selection of menu item.
Please help me resolve the issue
In your code you call the selectedFilter(menuItem) function. You can then add this in your function:
$scope.selectedFilter = function(menuItem){
// your code here
$scope.myLabel = menuItem.name;
}
And in your HTML:
<md-button>
{{myLabel}}
</md-button>
Probably the md-button directive creates its own scope and therefore menuItem is not present at this scope. Try $parent.menuItem to access the parent scope which should be the scope of ng-repeat.
I have a set of tabs:
<tabset class="tab-container">
<tab id = "tabContent" ng-repeat="tab in tabs" active="tab.active" ng-model = "cmModel"> <!-- the tab highlight directive -->
<tab-heading>
<span>{{tab.title}}</span>
<i class="glyphicon glyphicon-remove" ng-click="removeTab($event, $index)"></i> <!-- the tab close button -->
</tab-heading>
<textarea ui-codemirror='cmOption' id="{{ 'Tab ' + ($index+1) }}" ng-model = "Text"> + "awefwef" + </textarea>
</tab>
<button class="btn btn-default" ng-click="addTab()"></button>
</tabset>
I'm attempting to set a dynamic ng-model with ng-model = "Text".
First of all, I know that if I really want to add dynamic ng-models, I can't have them all the same (need to somehow add $index to Text). However, the most pressing issue is that $scope.Text is undefined, even though I clearly have added a textarea with ng-model Text.
What am I doing wrong?
Thanks.
I thinks if the value of your ng-model can not be find in the tabs, it will get a undefined.
Why don't you just make text a property of each tab? So instead of doing this:
<textarea ng-model="Text[tab.title]"></textarea>
Do this:
<textarea ng-model="tab.text"></textarea>
Text[Value]
That implies you're trying to access a property called "Value" on an object called "Text". If it was just "Text" then angular would create a variable called "Text" on your scope object and all would be well but since you're trying to access a property on the variable before it has actually been created you're getting the reference error.
I'm assuming you have a controller, in which case do:
$scope.Text = {}
within the controller and what you have should work.
I am assuming the issue you're running into is trying to have each tab have their own scope for the tab title. To work with your current setup, you will need to just have the model for the <textarea> be set to the tab.title itself. This way, it will be tied to that particular tab.
<tabset class="tab-container">
<tab ng-repeat="tab in tabs" ng-model="cmModel">
<tab-heading>
<p>{{tab.title}}</p>
</tab-heading>
<br/>
<textarea id="{{"Tab"+($index+1)}}" ng-model="tab.title"></textarea>
</tab>
<p><button class="btn btn-default" ng-click="addTab()">Add Tab</button></p>
</tabset>
Working JSFiddle Demonstration