I want to have a menu in my app bar that can get its menu items from child components. I am working with the Angular Material "mat-menu" and I'm able to display the menu item but I can't seem to fire off the associated function on the child component.
The app.component.html (parent):
<div>
<mat-toolbar style="display: flex; flex-direction: row; justify-content: space-between; margin-bottom: 12px">
<div>
<button type="button" mat-icon-button id="btnMore" [matMenuTriggerFor]="appMenu" [matMenuTriggerData]="menuData">
<mat-icon>more_horiz</mat-icon>
</button>
<mat-menu #appMenu="matMenu" xPosition="before">
<ng-template matMenuContent let-aliasMenuItems="menuItems">
<button mat-menu-item *ngFor="let item of aliasMenuItems" (click)="handleMenuAction(item.action)">
{{item.text}}
</button>
</ng-template>
</mat-menu>
</div>
</mat-toolbar>
</div>
<div>
<router-outlet></router-outlet>
</div>
Here is app.component.ts (parent). It retrieves the menu data from the appService component. It also (should) execute the callback.
ngOnInit() {
this.appService.getMenuData().subscribe(menuData => this.menuData = menuData);
}
handleMenuAction(action: Function) {
action();
}
Here is the child component "company.component.ts" which passes its menu items to app.service so they can be retrieved by app.component. Notice the menuData is an object that contains an array of objects of types string and callback function.
ngOnInit(): void {
this._appService.setMenuData({
menuItems: [
{text: "Add Company", action: this.addCompany}
]});
}
addCompany(): void {
this._router.navigate(['/company', 0])
}
For some reason the click event handler is not showing up in my Chrome dev tools. I would like the menu clicks to call functions, not just perform navigation.
There may be a better way to solve this problem. If so, please provide a link to an example. TIA.
Edit: Stackblitz is at https://stackblitz.com/edit/angular-nbzoe6
Updated Answer
According to your stackblitz:
You just only need to bind the this of child component to new menu actions to run in child scope when you decide to call menu action
something like this:
company-list.component.ts
ngOnInit(): void {
this._appService.setMenuData({
menuItems: [
{text: "Add Company", action: this.addCompany.bind(this)}
]});
}
Old Answer
You can create the menuItems$ observable in your appService and subscribe on it in app.component.ts and from child component you just add new menuItems to this observable, The menuItems in your app.component will have new value
Something like this
appService.ts
class AppService {
// ...
menuItems$: BehaviourSubject<any[]> = new BehaviourSubject([]);
constrcutor() {}
// ...
}
app.component.ts
class AppComponenet {
// ...
menuItems: any[] = [];
constrcutor(private appService: AppService) {}
ngOnInit() {
// ...
this.appService.menuItems$.subscribe(newMenu => {
this.menuItems = newMenu;
});
}
}
child.compnenet.ts
class ChildComponenet {
// ...
constrcutor(private appService: AppService) {}
ngOnInit() {
// ...
this.appService.menuItems$.new(['home', 'about']);
}
}
Related
I have a component that accepts a callback function #Input() closeCallback: () => void;
and calls it in a close function like this :
close() {
this.closeCallback();
}
<button class="close" (click)="close()">
<img src="assets/img/share/icon_close_dark.svg" />
</button>
So in the parent component I passed a function to it :
<ng-container *ngIf="isPopupOpen">
<e2-fullscreen-popup
[title]="'top promotions'"
[closeCallback]="closePopup"
>
...
</e2-fullscreen-popup>
</ng-container>
closePopup() {
this.isPopupOpen = false;
console.log('this.isPopupOpen',this.isPopupOpen);
}
so why I'm not able to remove the component when clicking on the button, even thou the logs themselves show that this.isPopupOpen is false?
#Input is used to pass data to the child component; to implement a callback you need to use #Output.
In your child component:
#Output() closeCallback = new EventEmitter();
// ...
close() {
this.closeCallback.emit();
}
In your parent component:
<ng-container *ngIf="isPopupOpen">
<e2-fullscreen-popup
[title]="'top promotions'"
(closeCallback)="closePopup($event)"
>
...
</e2-fullscreen-popup>
</ng-container>
closePopup(dataFromChild: any){
// handle callback
this.isPopupOpen = false;
console.log('this.isPopupOpen',this.isPopupOpen);
}
I have been dealing with this scenario for a while, I appreciate your advice in advance
ngOnChanges runs in a context that I understand it shouldn't run. When modifying a property of a class which was initially set through #Input. This modification causes ngOnchanges hook to be executed in one context and not in another. I describe my scenario below
I have the following parent component that contains a list of customers that is passed to a child component,
Parent controller
export class AppComponent {
customers: ICustomer[];
currentlySelected: Option = 'All';
constructor() {
this.customers = [
{
id: 1,
name: 'Task1',
status: 'Pending',
},
{
id: 2,
name: 'Task2',
status: 'Pending',
},
{
id: 3,
name: 'Task3',
status: 'Progress',
},
{
id: 4,
name: 'Task4',
status: 'Closed',
},
];
}
selectBy(option: Option): void {
this.currentlySelected = option;
}
filterBy(): ICustomer[] {
if (this.currentlySelected === 'All') {
return this.customers;
}
return this.customers.filter(
(customer) => customer.status === this.currentlySelected
);
}
}
Parent template
<nav>
<ul>
<li (click)="selectBy('All')">All</li>
<li (click)="selectBy('Pending')">Pending</li>
<li (click)="selectBy('Progress')">Progress</li>
<li (click)="selectBy('Closed')">Closed</li>
</ul>
</nav>
<app-list [customers]="filterBy()"></app-list>
Before passing customer to the child component they are filtered according to the customer status property, that is the purpose of the filterBy function.
The child component in the hook ngOnChanges modifies each customer by adding the showDetail property and assigns it the value false
export class ListComponent implements OnInit, OnChanges {
#Input() customers: ICustomer[] = [];
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
this.customers = changes.customers.currentValue.map(
(customer: ICustomer) => ({
...customer,
showDetail: false,
})
);
}
ngOnInit(): void {
console.log('init');
}
toggleDetail(current: ICustomer): void {
this.customers = this.customers.map((customer: ICustomer) =>
customer.id === current.id
? { ...customer, showDetail: !customer.showDetail }
: { ...customer }
);
}
}
Calling the toggleDetail method changes the value of the showDetail property to show the customer's detail
child template
<table>
<thead>
<tr>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let customer of customers">
<tr>
<td>{{ customer.name }}</td>
<td>
<button (click)="toggleDetail(customer)">Detail</button>
</td>
</tr>
<tr *ngIf="customer.showDetail">
<td colspan="2">
<pre>
{{ customer | json }}
</pre>
</td>
</tr>
</ng-container>
</tbody>
</table>
The behavior that occurs is the following, when all clients are listed and click on detail it works as expected, but if I change to another state and the list is updated and I click on detail it does not show the detail. The reason is that the ngOnchanges hook is re-executed causing the showDetail property to be set to false again, thus defeating my intention.
Why is ngOnChanges executed in this context? What alternative is there to solve it?
Update 1
I have added sample app: https://stackblitz.com/edit/angular-ivy-dkvvgt?file=src/list/list.component.html
You has in your code
<app-list [customers]="filterBy()"></app-list>
Angular can not know the result of the function "filterBy" until executed, so it's executed each time check. This is the reason we should avoid use functions in our .html (We can use, of course, but we need take account this has a penalty). Use an auxiliar variable
customerFilter:ICustomer[];
In constructor you add
constructor() {
this.customers = [...]
this.customerFilter=this.customers //<--add this line
}
And in selectBy
selectBy(option: Option): void {
this.currentlySelected = option;
this.customerFilter=this.filterBy() //<--this line
}
Now pass as argument the customerFilter
<app-list [customers]="customerFilter"></app-list>
Your forked stackblitz
Angular runs ngOnChanges when any of the inputs change. When you use an object as an input parameter Angular compares references. As Eliseo said Angular calls your filterBy function on each change detection, and it's not a problem when the currentlySelected is All, beacuse you return the same array reference and it won't trigger change detection in your list component. However when it's not, that causes an issue. You filter your array on each change detection and that results in a new array every time. Now Angular detects that the #Input() changed and runs ngOnChanges.
You can do as Eliseo said, that's a solution too. My suggestion is to create a pipe, it's makes the component.ts less bloated.
#Pipe({
name: 'filterCustomers',
})
export class FilterCustomersPipe implements PipeTransform {
transform(customers: ICustomer[] | null | undefined, filter: Option | undefined | undefined): ICustomer[] | undefined {
if (!customers) {
return undefined;
}
if (!filter || filter === 'All') {
return customers;
}
return customers.filter((customer) => customer.status === filter);
}
}
I prefere writing out null | undefined too, so it's safer with strictTemplates.
You can use this pipe like this:
<app-list [customers]="customers | filterCustomers : currentlySelected"></app-list>
Here you can read more about Angular pipes.
Another suggestion:
Your nav doesn't have button elements, you bind your (click) events on li elements. That's a really bad practice as it not focusable by keyboard. More about HTML Accessibility.
How can I add an eventlistener on a <div> or other element, to hide something I am displaying via an *ngIf - using Angular, when I click away from that element?
Explanation: I am showing a custom CSS dropdown via *ngIf when you click on <label>Filter</label>, and I want the user to be able to click as many times as they wish in the custom dropdown, but when they click outside the custom dropdown, I would like to hide the custom dropdown via the *ngIf again.
The method called when a user clicks on the label is showHideSectionOptions(), which toggles the showHide variable to true or false.
This is my HTML code:
showHide = false;
<div class="form-row">
<div class="form-group" id="showAndHideSections">
<label (click)="showHideSectionOptions()">
<img src="../../../assets/icons/Filter.png" alt="" class="mr-3">Filter</label>
<div *ngIf="showHide" class="section-options">
// show or hide content
</div>
</div>
</div>
This is my component code:
showHideSectionOptions() {
this.showHide = !this.showHide;
}
I have tried adding an eventlistener as per the below, but I cannot set the value of my showHide variable, as I get the following error: Property 'showHide' does not exist on type 'HTMLElement'.ts(2339):
body.addEventListener('click', function() {
alert('wrapper');
}, false);
except.addEventListener('click', function(ev) {
alert('except');
ev.stopPropagation();
}, false);
Thanks in advance!
First of all, this already has an answer here
However, if you want an Angular solution, you can use a custom directive:
#Directive({
selector: '[clickOutside]'
})
export class ClickOutsideDirective {
#Output()
readonly clickOutside = new EventEmitter<MouseEvent>();
#Input()
include?: HTMLElement;
constructor(private el: ElementRef<HTMLElement>) {}
#HostListener('window:click', [ '$event' ])
onClick(event: MouseEvent): void {
if (this.isEventOutside(event)) {
this.clickOutside.emit(this.event);
}
}
private isEventOutside(event: MouseEvent): boolean {
const target = event.target as HTMLElement;
return !this.el.nativeElement.contains(target) &&
(!this.include || !this.include.contains(target))
}
}
Which you can use like this:
<div class="form-group" id="showAndHideSections">
<label (click)="showHideSectionOptions()" #label>
<img src="../../../assets/icons/Filter.png" alt="" class="mr-3">
Filter
</label>
<div *ngIf="showHide" class="section-options"
[include]="label" (clickOutside)="showHide = false">
// show or hide content
</div>
</div>
A more performant one would be one running outside of the ngZone. Because the subscribe happens outside of the directive it will be inside the ngZone when subscribing to the Output
#Directive({
selector: '[clickOutside]'
})
export class ClickOutsideDirective {
#Input()
include?: HTMLElement;
#Output()
readonly clickOutside = this.nz.runOutsideAngular(
() => fromEvent(window, 'click').pipe(
filter((event: MouseEvent) => this.isEventOutside(event))
)
);
constructor(private el: ElementRef<HTMLElement>, private nz: NgZone) {}
private isEventOutside(event: MouseEvent): boolean {
const target = event.target as HTMLElement;
return !this.el.nativeElement.contains(target) &&
(!this.include || !this.include.contains(target))
}
}
working stack
Component A has 2 components inside:
<div class="classStyleLeft">
<app-picture [IdSectionPicture]="2" [PData]="modelPage"></app-picture>
</div>
<div class="classStyleRight">
<app-list [IdSectionList]="2" [PData]="modelPage"></app-list>
</div>
In the component app-list, select an item from the list and click the method showInfo:
<ul *ngFor="let value of PData.content[IdSectionList].content; let valueIndex = index;">
<li (click)="showInfo(IdSectionList, valueIndex)">
<b>{{value.value}}</b>
</li>
</ul>
I want the method parameters showInfo they were transferred to the component app-picture. Component content app-picture:
<div>{{PData.content[IdSectionList].value[valueIndex].value}}</div>
I do not know how to do it.
I would ask for help.
Thank you
Sharing the data using service won't be enough as they events has to be listened to.
Since it is a sibling component, create a service with BehaviorSubject as:
export class InfoService{
private infoSubject = new BehaviorSubject<any>({id: '',value: ''});
showInfoEventEmitter(data){
this.infoSubject.next(data);
}
showInfoEventListener(){
return this.infoSubject.asObservable();
}
}
and then use it in component as
app-list.component
constructor(public infoSvc: InfoService){}
showInfo(IdSectionList, valueIndex){
this.infoSvc.showInfoEventEmitter({id:IdSectionList, value: valueIndex})
}
and in app-picture.component
eventSubscription: Subscription;
constructor(public infoSvc: InfoService){}
ngOnInit(){
this.eventSubscription = this.infoSvc.showInfoEventListener().subscribe(res => {
console.log(res); // you have the event here
})
}
ngOnDestroy(){
this.eventSubscription.unsubscribe(); // to stop listening to this event after component is destroyed
}
I have a page with three components:
1. Products list component which gets some products as input and display them.
2. Filters component which displays some filters list i.e. (size, colour,...) and also display the added filters.
3. Main component which is the root component
Let say a user adds 1 filter which fires a http request to get new filtered products and while the request is pending he removes the added filter which fires another http request to fetch all the products
How to cancel the first request so we don't display the filtered products?
Here is my code:
class FiltersService {
private _filters: any[];
get filters() {
return this._filters;
}
addFilter(filter) {
this._filters.push(filter);
}
removeFilter(filter) {
// Remove filter logic ...
}
}
class DataService_ {
constructor(private http: HttpClient) {
}
getProducts(filters) {
return this.http.post<any[]>('api/get-products', filters)
}
}
#Component({
selector: 'app-main',
template: `
<div>
<app-filters [filtersChanged]="onFiltersChange()"></app-filters>
<app-products-list [products]="products"> </app-products-list>
</div>
`
})
class MainComponent {
products: any[];
constructor(private dataService: DataService_, private filtersService: FiltersService) {
}
ngOnInit() {
this.setProducts()
}
setProducts() {
let filters = this.filtersService.filters;
this.dataService.getProducts(filters)
.subscribe(products => this.products = products)
}
onFiltersChange() {
this.setProducts();
}
}
#Component({
selector: 'app-filters',
template: `
<div>
Filters :
<ul>
<li *ngFor="let filter of filters" (click)="addFilter(filter)"> {{ filter.name }}</li>
</ul>
<hr>
Added Filters:
<ul>
<li *ngFor="let filter of filtersService.filters"> {{ filter.name }} <button (click)="removeFilter(filter)"> Remove</button></li>
</ul>
</div>
`
})
class FiltersComponent {
filters = [{ name: 'L', tag: 'size' }, { name: 'M', tag: 'size' }, { name: 'White', tag: 'colour' }, { name: 'Black', tag: 'colour' }]
#Output() filtersChanged = new EventEmitter()
constructor(public filtersService: FiltersService) {
}
addFilter(filter) {
const isAdded = this.filtersService.filters.find(x => x.name === filter.name);
if (isAdded) return;
this.filtersService.addFilter(filter);
this.filtersChanged.emit()
}
removeFilter(filter) {
this.filtersService.remove(filter);
this.filtersChanged.emit()
}
}
#Component({
selector: 'app-products-list',
template: `
<div>
<h1>Products</h1>
<ul *ngIf="products.length">
<li *ngFor="let product of products">
{{product.name }}
</li>
</ul>
</div>
`
})
class ProductsListComponent {
#Input() products
constructor() {
}
}
Long story short:
Easiest way to handle such situations is by using the switchMap operator. What this does is cancel the internal subscription as soon as a new event comes along.
One implementation would be:
class MainComponent {
products: any[];
private _filters$ = new Subject();
constructor(private dataService: DataService_, private filtersService: FiltersService) {
}
ngOnInit() {
this.setProducts()
}
setProducts() {
this._filters$
.switchMap((filters)=> this.dataService.getProducts(filters)) // or .let(switchMap...) if you are using rxjs >5.5
.subscribe(products => this.products = products);
}
onFiltersChange() {
this._filters$.next(this.filtersService.filters);
}
}
Long story:
What happens here is:
When you change filter the onFilterChange is triggered. You then emit the latest filters (inside this.filtersService.filters) through the _filters$ Subject (a subject is almost identical to an EventEmitter).
Back in time during component initialization the ngOnInit method has called setProducts, which has subscribed to the _filters$ subject for future events (none has happened at this point). When an event arrives on _filters$ then we trigger the getProducts method of dataservice, passing it the filters that where contained in the event. We will be waiting on this line until the http call has completed. As soon as it completes the result of the http call will be assigned to the products of the component.
If while we are waiting for the http response to get back, onFiltersChange is fired again, then a new event will arive at the switchMap and it will cancel the previous http request so that it can handle the new event.
This is a very powerful approach as changing a single operator, you can easily change the behavior of your app. For instance, changing switchMap to concatMap will make the request wait for the previous one to complete (will happen serially). Changing it to flatMap will have the same behaviour as the original code you posted (http requests will happen as soon as filters change, without affecting previous ones, order of responses will not predictable) and so on.
Note : to cancel the request just use unsubscribe.
For exmple
const course$ = this.service$.getCourses(`/api/courses`).subscribe(courses => { console.log(courses) }
setTimeout(() => course$.unsubscribe(),1000) // cancel the request