Angular MatMenu not working in double ngFor loop - javascript

Using Angular 13, I have been trying to add a menu using Angular MatMenu (https://material.angular.io/components/menu/overview) which is conditionally shown. Basically a bar shows button (operations) and some may have suboperations. In this case I want to display the suboperations when clicking on the button.
Here is the basic code I have, which is the complete html since there is another menu in the beginning. Note that removing it does not change the behavior.
<div class="rounded-box" *ngIf="!!selectedCategory">
<div class="selector" *ngIf="rateOrDislocationEnabled; else simpleAggregation">
<button color="primary" mat-button mat-icon-button [matMenuTriggerFor]="categoryMenu" class="dropdownButton">
<div class="dropdownText">{{ selectedCategory | agReplace: "_":" " }}</div>
<mat-icon class="dropdownIcon">arrow_drop_down</mat-icon>
</button>
<mat-menu #categoryMenu="matMenu" [overlapTrigger]="true" data-testid="categories-menu">
<button mat-menu-item *ngFor="let category of availableCategories" (click)="setSelectedCategory(category)">
<span [ngClass]="{ selectedLabel: isSelected(category) }">{{ category | agReplace: "_":" " }}</span>
<div *ngIf="category === BlockCategory.RATE_MAKING" class="alpha">ALPHA</div>
</button>
</mat-menu>
</div>
<ng-container *ngFor="let operationCategory of getOperations(); let lastItem = last">
<ng-container *ngFor="let operation of operationCategory">
<button *ngIf="operation.subOperations.length === 0"
mat-icon-button
class="iconWrapper"
id="operation-icon-{{ operation.value }}"
(click)="addOperation(operation.value)"
[disabled]="operation.disabled$ | async">
<mat-icon [ngClass]="operation.icon" class="icon" [agToolTip]="operation.tooltip$ | async"></mat-icon>
</button>
<ng-container *ngIf="operation.subOperations.length !== 0">
<button
mat-button
mat-icon-button
class="iconWrapper"
id="operation-menu-icon-{{ operation.value }}"
[matMenuTriggerFor]="subMenu">
<mat-icon [ngClass]="operation.icon" class="icon" [agToolTip]="operation.tooltip$ | async"></mat-icon>
</button>
<mat-menu #subMenu="matMenu">
<button mat-menu-item>Settings</button>
<button mat-menu-item>Log off</button>
</mat-menu>
</ng-container>
</ng-container>
<div class="divider" *ngIf="!lastItem"></div>
</ng-container>
</div>
I have created a stackblitz reproducing the issue: https://angular-ivy-cvv2xk.stackblitz.io
The issue is that when I click on the button, nothing happens. When I move the button out of the ngFor loops though, it works properly. I have tried things such as removing the ngIf condition (so all buttons are doubled) and none show the menu when clicking on it.
So the "submenu" is never displayed.
I wonder if I need to make the mat-menu specific or give it some id to ensure there are no conflicts? Since I'm a bit new to Angular I maybe be missing something.
Thanks in advance

Your button which triggers submenu got multiple button tags.
<button
mat-button
mat-icon-button
Can you remove one and try again?
Edit:
You use <ng-container *ngFor="let operationCategory of getOperations();
to populate your array.
If you press your button, angular will trigger change detection and this will trigger again your getOperations().
This will lead to this behaviour and dosent open your submenu.
So you should try to replace getOperations() with async pipe (if this is the case) or use properties.

Related

How to use ngb-accordion in a ngFor loop?

I'm using the ngb-accordion in a for loop. It's working for a part, but it seems that it is not possible to open the active element in a for loop.
I want make it possible that when one of the accordion elements includes the property 'OpenAccordion' the panel of that element opens. So I need to define the activeIds and the id for each element inside the array to make it possible.
When I define the *ngFor outside the <ngb-accordion></ngb-accordion> it works because I can link the index to the activeIds and id. But his time it's not possible to open one panel at a time because the panels are seperate.
[closeOthers] is working on this one but open one panel at a time is not working:
<ngb-accordion
class="accordion-item"
[closeOthers]="true"
activeIds="true-{{i}}">
<div *ngFor="let child of items?.children | async; let i = index">
<ngb-panel id="{{(child?.value?.properties | async)?.property.includes('OpenAccordion')}}-{{i}}">
<ng-template ngbPanelHeader let-opened="opened">
<div class="d-flex align-items-center justify-content-between">
<button ngbPanelToggle class="btn btn-link container-fluid text-left">
<h5>
{{ (child?.value?.properties | async)?.title }}
</h5>
</button>
</div>
</ng-template>
<ng-template ngbPanelContent>
<h4>test</h4>
</ng-template>
</ngb-panel>
</div>
</ngb-accordion>
When I define the *ngFor inside the <ngb-accordion></ngb-accordion> the [closeOthers] functionality works, but this time it's not possible to open the active accordion panel. Because the index of the activeId is unknown and does not match the id of the active element.
Open one panel at a time is working but [closeOthers] is not working on this one:
<ng-template ngFor let-child [ngForOf]="items?.children | async" let-i="index">
<ngb-accordion
class="accordion-item"
[closeOthers]="true"
activeIds="true-{{i}}">
<ngb-panel id="{{(child?.value?.properties | async)?.property.includes('OpenAccordion')}}-{{i}}">
<ng-template ngbPanelHeader let-opened="opened">
<div class="d-flex align-items-center justify-content-between">
<button ngbPanelToggle class="btn btn-link container-fluid text-left">
<h5>
{{ (child?.value?.properties | async)?.title }}
</h5>
</button>
</div>
</ng-template>
<ng-template ngbPanelContent>
<h4>test</h4>
</ng-template>
</ngb-panel>
</ngb-accordion>
</ng-template>
How can I make it possible to let both functions [closeOthers] and open one panel at a time works in a *ngFor loop?
In you first solution, there is a mistake, you used the variable i before it is declared:
<ngb-accordion
class="accordion-item"
[closeOthers]="true"
activeIds="true-{{i}}"> // i used before declaration?
<div *ngFor="let child of items?.children | async; let i = index">
I just created a simple sample to use ngFor with ngb-accordion, it works good.
You should try to ngFor inside the <ngb-accordion>:
TS:
import { Component } from '#angular/core';
#Component({
selector: 'ngbd-accordion-static',
templateUrl: './accordion-static.html'
})
export class NgbdAccordionStatic {
accordionSamples = [
{
id: 1,
title: 'Simple',
content: `Simple content sample`
},
{
id: 2,
title: 'Fancy',
content: `Fancy content interesting`
}
]
}
HTML:
<ngb-accordion [closeOthers]="true" activeIds="acc-2">
<ngb-panel *ngFor="let acc of accordionSamples" id="acc-{{acc.id}}" title="{{acc.title}}">
<ng-template ngbPanelContent>
{{acc.content}}
</ng-template>
</ngb-panel>
</ngb-accordion>
https://stackblitz.com/edit/angular-mdbedd?file=src%2Fapp%2Faccordion-static.html

show and hide between 2 different component Angular

I have Parent component with 2 different child, inquiryForm and inquiryResponse, I got situation when I need to hide and show this 2 component based on condition:
If user had click submit on inquiryForm, it will hide inquiryForm component and show inquiryResponse component
On inquiryResponse component, there are button display inquiry form, where user clicked it and will hide inquiryResponse component and show inquiryForm.I cant solved this.
I know it can be solved using router but I want different solution like using service or subject
this is demo I created using stackblitz, this is what I had tried;
inquiry-response.ts
getReceivedSummons() {
this.inquiryStore.summons$.subscribe(result => {
this.receivedSummon = result;
this.addCheckboxes();
this.isShowResponse = true;
});
}
showInquiryForm() {
// do something
}
inquiry-response.html
<div *ngIf="isShowResponse">
<p>Inquiry Response</p>
<form [formGroup]="form" (ngSubmit)="submitSelectedCheckboxes()">
<ng-container formArrayName="receivedSummons" *ngFor="let summon of formReceivedSummons.controls; let i = index">
<ng-container [formGroup]="summon">
<ng-container formArrayName="items" *ngFor="let item of formReceivedSummonsItems(i).controls; let j = index">
<ng-container [formGroup]="item">
<input type="checkbox" formControlName="isChecked"> {{item.value.name}}
</ng-container>
</ng-container>
</ng-container>
<div *ngIf="!summon.valid">At least one order must be selected</div>
</ng-container>
<br>
<span class="button">
<button [disabled]="!form.valid">submit</button>
</span>
<button (click)="showInquiryForm()"> ( change ID number ) display inquiry form</button>
</form>
</div>
As AJT_82 say, you app.component can be like
<app-inquiry-form *ngIf="step==1" (submit)="step=2"></app-inquiry-form>
<br>
<app-inquiry-response *ngIf="step==2" (click)="step=1"></app-inquiry-response>
//you has a variable
step:number=1;
And in each component
#Output() submit=new EventEmitter<any>()
..in somewhere...
this.submit.emit()
#Output() click=new EventEmitter<any>()
..in somewhere...
this.click.emit()

ng-select add new option with async search

I use the [#ng-select/ng-select][1] component many places in my software. It works well, it searches by a backend method as the user types and shows the results for selection.
Now I would like to let the user to create new items that are not on the list. How can I do this with an async list? productNames is an Observable.
<ng-select [name]="'productname'+index$" [items]="productNames | async"
[attr.id]="'productname'+index$"
[(ngModel)]="detail.productNameNew"
[typeahead]="productNameInput"
(change)="productNameChanged($event, detail)"
(keydown)="productNameKeyPress($event, detail)"
[disabled]="currentSuggestion.acceptUser === null ? null : true"
[clearable]="false">
<ng-template ng-label-tmp let-item="item">
{{item.productName}}
</ng-template>
<ng-template ng-option-tmp let-item="item">
<div class="noproduct-item">
<div class="noproduct-head">
<div class="productname">{{item.productName}}</div>
<div class="description">{{item.description}}</div>
</div>
<div class="noproduct-details">
<div class="headid">
<span class='glyphicon glyphicon-send'></span>
{{item.suggestionHeadId}}
</div>
<div class="createduser">
<span class='glyphicon glyphicon-user'></span>
{{item.createUser?.name}}
</div>
<div class="createddate">
<span class='glyphicon glyphicon-calendar'></span>
{{item.createDate | date: 'yyyy.MM.dd.'}}
</div>
</div>
</div>
</ng-template>
</ng-select>
Add a second observable to capture the user added products and then use combineLatest to merge them. Subscribing to the combined list will produce a new list that you can use in the ng-select.
https://stackblitz.com/edit/angular-rxjs-combinelatest

Remove ionic 2 ion-card div using Javascript in controller

I need to remove an ionic 2 card when someone clicks on the trash icon. The code I have so far gives a querySelector is null error when I run it.
The view html looks like this:
<ion-card *ngFor="#mediaItem of mediaItems" id="item{{mediaItem.id}}" class="media-item">
<ion-card-header class="title-header">
<div class="title-item">
{{mediaItem.title}}
</div>
<ion-icon name="trash" class="bookmark_trash_icon" (click)="removeItem(mediaItem.id)"></ion-icon>
</ion-card-header>
<ion-card-content class="outer-content" >
<img src='{{mediaItem.url}}'>
</ion-card-content>
<ion-item class="bookmark-media-item">
<!-- <button (click)="topup(mediaItem)" clear item-left> -->
<div style="float:left;">
<ion-icon name="heart"></ion-icon>
{{mediaItem.liked}}
<ion-icon name="close-circle"></ion-icon>
{{mediaItem.disliked}}
</div>
<a (click)="showUserProfile(mediaItem.owner, mediaItem.username)" style="float:left;">
{{mediaItem.username}}
</a>
<div style="float:right;">
<img src="img/tiny-v.png" class="bookmark-v-icon">
{{mediaItem.credits_left}}
</div>
</ion-item>
</ion-card>
My controller javascript code looks like this:
removeItem(theItemID) {
let cardToHide = '#item'+theItemID;
document.querySelector(cardToHide).style.display = 'none';
}
The error I get is
querySelector is null
You can send the object instead of the id by changing that part of the code:
(click)="removeItem(mediaItem)"
And then find that object in you mediaItems array and delete it from there. Then your view will be automatically updated.
First get the index of that object in the array, and then you can use the mediaItems.splice(index, 1) to delete it from the array:
removeItem(theItem) {
let index = this.mediaItems.indexOf(theItem);
this.mediaItems.splice(index,1);
}

Angular select all visible in list

Say you have the following ng-repeat:
<ul class="list-group">
<ng-user-item
ng-repeat="user in users | filter:search" user="user" ng-if="assigned.indexOf(user.id) < 0"
ng-click="selectFunction(user);"></ng-user-item>
</ul>
Now as you can see this has a filter.
when the filter is not null i have the following button:
<button class="btn btn-default" ng-if="search.division != null" style="margin-bottom: 10px;">Select all</button>
when this button is pressed i want a list of all ng-user-item that is visible.
How can this be done with angular?
I think if you change your ng-repeat to this it should work:
ng-repeat="user in filteredUsers = (users | filter:search)"
You can now use filteredUsers as a normal scope variable.

Categories

Resources