Angular 15, child component doesn't emit string to parent - javascript

I have this child component (filters.component.ts) from which I am trying to emit a string to the parent component. I had already done this once with a different component but it seems like Angular doesn't like me implementing an *ngFor to loop through a string array and pass the category string to the method? I've tried adding a console log to the onShowCategory() method in home.component.ts and it does not log any string values to the console, leading me to believe that the values are not being passed to the parent when the click event is activated. Here is the code (I've added arrows to point to the relevant lines of code, they are not part of my code and not the issue.):
filters.component.html:
<mat-expansion-panel *ngIf="categories">
<mat-expansion-panel-header>
<mat-panel-title>CATEGORIES</mat-panel-title>
</mat-expansion-panel-header>
<mat-selection-list [multiple]="false">
<mat-list-option *ngFor="let category of categories" [value]="category"> <--------
<button (click)="onShowCategory(category)">{{ category }}</button> <--------
</mat-list-option>
</mat-selection-list>
</mat-expansion-panel>
filters.component.ts:
#Component({
selector: 'app-filters',
templateUrl: './filters.component.html',
styleUrls: []
})
export class FiltersComponent {
#Output() showCategory = new EventEmitter<string>() <-------
categories: string[] = ['shoes', 'sports']; <-------
onShowCategory(category: string): void { <-------
this.showCategory.emit(category); <-------
}
}
home.component.html:
<mat-drawer-container [autosize]="true" class="min-h-full max-w-7xl mx-auto border-x">
<mat-drawer mode="side" class="p-6" opened>
<app-filters (showCategory)="onShowCategory($event)"></app-filters> <-------
</mat-drawer>
<mat-drawer-content class="p-6">
<app-products-header (columnsCountChange)="onColumnsCountChange($event)"></app-products-header>
{{ category }} <----- should display the category when selected
</mat-drawer-content>
</mat-drawer-container>
home.component.ts:
import { Component } from '#angular/core';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: []
})
export class HomeComponent {
cols = 3;
category: string | undefined; <-------
onColumnsCountChange(colsNumber: number): void {
this.cols = colsNumber
}
onShowCategory(newCategory: string): void { <-------
this.category = newCategory; <-------
}
}
I have read through and followed the variables many times and I don't see the issue. From the child component template I pass the category to the onShowCategory method and emit it to the parent. From the parent I call the EventEmitter and pass the $event variable which should change the value of the category property in the home component. I've checked spelling, and tried moving the tags around. I don't see a console.log in the console when I add one to the method, and I cannot get the string to appear on the home template. What am I doing wrong?

your parent html should have messageEvent. Give this a try and see if it works.
<app-filters (messageEvent)="onShowCategory($event)"></app-filters>
<app-products-header (messageEvent)="onColumnsCountChange($event)"></app-products-header>
I also like to put console.log statements at every point when setting up EventEmitters, just to see where it's getting stuck.

After taking a break and spending a few more hours combing the internet for answers (to no avail) I decided to try something completely unintuitive and solved it.
I changed:
<mat-list-option *ngFor="let category of categories" [value]="category">
<button type="button" (click)="onShowCategory(category)">{{ category }}</button>
</mat-list-option>
To
<mat-list-option *ngFor="let category of categories" (click)="onShowCategory(category)" [value]="category">
<div>{{ category }}</div>
</mat-list-option>
and now it works, not entirely sure why the click event needs to be on the parent tag and cannot be on the button itself. The thought process that got me to this solution was that the click event seemed to not be activating and/or the strings in the categories array weren't being passed to the method properly. I hope this helps someone in the future.

Related

Getting undefined object error in Typescript using Angular

So I am following the Traversy Media new [Angular crash course]: https://www.youtube.com/watch?v=3dHNOWTI7H8 around 39:45 and getting object undefined errors constantly.
Here is the picture of the error which says object is possibly 'undefined':
object is possibly 'undefined
This worked in task-item.component.html
<p *ngFor="let task of tasks">
{{task.text}}
</p>
This object is possibly 'undefined' task-item.component.html
<div class="task">
<h3>{{task.text}}</h3>
<p>{{task.day}}</p>
</div>
task-item.component.ts
import { Component, OnInit, Input } from '#angular/core';
import {Task} from 'src/Task';
#Component({
selector: 'app-task-item',
templateUrl: './task-item.component.html',
styleUrls: ['./task-item.component.css']
})
export class TaskItemComponent implements OnInit {
#Input() task?: Task;
constructor() {
}
ngOnInit(): void {
}
}
I have put a tsconfig.json rule "strictPropertyInitialization": false
It's because you set it as a an optional using ?
You can either remove ? which is not a good practice if your item can be null, or you can just do this in the html
<div class="task">
<h3>{{task?.text}}</h3>
<p>{{task?.day}}</p>
</div>
It will display the value if the variable is initialized, and don't throw an error
The ? here indicates that this is a nullable object.
#Input() task?: Task;
If you always pass task to app-task-item then just remove the ?
#Input() task: Task;
Or you can add a conditional in your html, and remove the ? in your .ts
#Input() task: Task;
...
<div class="task">
<h3>{{task?.text}}</h3>
<p>{{task?.day}}</p>
</div>
If you're not passing a value into task when you instantiate the component, which than it would be undefined.
If are ever going to instantiate task-item without a task input you could add something like this to your ngOnInit
const blankTask: Task = {
text: 'placeholder text',
day: 'placeholder day',
};
...
ngOnInit() {
this.task = this.task || blankTask;
}
This means you do not always have to pass in a task to the component.
Working StackBlitz
To solve this I add *ngIf
<div class="task" *ngIf="task">
<h3>{{task.text}}</h3>
<p>{{task.day}}</p>
</div>

How to add navigation link to different buttons on the menu in Angular?

I want to add navigation path to all my buttons in the left menu (which is not the main menu).
I am getting the menu items name as #Input. I have created a dictionary for all the items name and their navigation path.
Here is the HTML:
<div class="row-styles" id="elements" *ngFor="let item of elements">
<button *ngIf="(item.action !== NO_ACCESS )" class="inner-children" routerLinkActive="active" id="inner-children"
[routerLink]="">
<span>{{item.resource}}</span>
</button>
</div>
Here is the TS file
import { Component, Input, OnInit } from '#angular/core';
#Component({
selector: 'apm-menu-resource',
templateUrl: './menu-resource.component.html',
styleUrls: ['./menu-resource.component.less']
})
export class MenuResourceComponent implements OnInit {
#Input() public elements = [];
constructor() {
const menupath = new Map<string, string>();
menupath.set('General', '/Adigem/config/general');
menupath.set('Messaging', '/Adigem/config/messaging');
menupath.set('Server', '/Adigem/config/email/server');
menupath.set('Alerting', '/Adigem/config/email/alert');
menupath.set('Network', '/Adigem/config/network');
menupath.set('Inventory', '/Adigem/config/inventory');
menupath.set('External port', '/Adigem/config/snmp/external-port');
menupath.set('Cloud Data', '/Adigem/config/clouddata');
menupath.set('Performance', '/Adigem/config/Performance');
menupath.set('CFG', '/Adigem/config/cfg');
menupath.set('System', '/Adigem/config/system');
console.log(menupath);
}
ngOnInit() {
}
}
I want to know what to add in the router link in the HTML so that it navigates to the proper menu item.
If you have access to the elements array, that's being passed to the component, you could simplify things a lot - you just add the target path to each of the items and your MenuResourceComponent won't have to deal with any path-related logic.
From your snippets I infer that there is a resource property, which is the element's title. If so, the elements array can be modified like this:
elements = [
{resource:'General', path: '/Adigem/config/general'},
{resource:'Messaging', path: '/Adigem/config/messaging'},
...
]
and then in the template:
<div class="row-styles" id="elements" *ngFor="let item of elements">
<button *ngIf="(item.action !== NO_ACCESS )" class="inner-children"
routerLinkActive="active" id="inner-children"
[routerLink]="item.path">
<span>{{item.resource}}</span>
</button>
</div>
However, if you have no other options and need to menupath map, then you can make it a class field:
import { Component, Input } from '#angular/core';
#Component({
selector: 'apm-menu-resource',
templateUrl: './menu-resource.component.html',
styleUrls: ['./menu-resource.component.less']
})
export class MenuResourceComponent{
#Input() public elements = [];
menupath = new Map<string, string>();
constructor() {
this.menupath.set('General', '/Adigem/config/general');
this.menupath.set('Messaging', '/Adigem/config/messaging');
this.menupath.set('Server', '/Adigem/config/email/server');
this.menupath.set('Alerting', '/Adigem/config/email/alert');
this.menupath.set('Network', '/Adigem/config/network');
this.menupath.set('Inventory', '/Adigem/config/inventory');
this.menupath.set('External port', '/Adigem/config/snmp/external-port');
this.menupath.set('Cloud Data', '/Adigem/config/clouddata');
this.menupath.set('Performance', '/Adigem/config/Performance');
this.menupath.set('CFG', '/Adigem/config/cfg');
this.menupath.set('System', '/Adigem/config/system');
console.log(this.menupath);
}
}
and the route binding looks like:
[routerLink]="menupath.get(item.resource)"
I wouldn't encourage the second solution, because you will have to handle the potential case where you receive an item, which is unknown for your menupath map.
Also I have a concern with the NO_ACCESS constant that you use in your template. There is no such property of the component, so this probably breaks the compilation.

How I can deselect button in angular?

I have some buttons like a category. When I click on one of them I get the card with some information.
<app-category-button [label]='category.name'
[isSelected]='category.id === (selectedCategoryId$ | async)'
[routerLink]='["."]' [queryParams]='{categoryId: category.id, page: 1}'
queryParamsHandling='merge'>
</app-category-button>
app-category-button selector:
<button mat-stroked-button
class='category-item font-medium'
[ngClass]='{"bg-primary-500 text-white": isSelected,
"text-primary-500 bg-lightbluebg-500": !isSelected}'>
{{label}}
</button>
When I click on the button I get in my route the queryParams that open my cards with information.
The question is how on the second click to that button hide queryParams and deselect the button?
You can do something like
this.route.queryParams.subscribe(
(params: Params) => {
this.queryParams = params.hasOwnProperty('categoryId') ?
{} : {categoryId: category.id, page: 1}
}
);
And use queryParams in your template as
<app-category-button [label]='category.name'
[isSelected]='category.id === (selectedCategoryId$ | async)'
[routerLink]='["."]' [queryParams]='queryParams'
queryParamsHandling='merge'>
</app-category-button>
If you want to remove the category only on the button for the selected one, you can change the categoryId query param to null only on click on that button.
<app-category-button [label]='category.name'
[isSelected]='category.id === (selectedCategoryId$ | async)'
[routerLink]='["."]' [queryParams]='{categoryId: (selectedCategoryId$ | async) ? null : category.id, page: 1}'
queryParamsHandling='merge'>
</app-category-button>
For the rest categories, it would still change load the respective category.
Accessing the router state
To retrieve the current variables from a route or current query params, use the ActivatedRoute-service.
In your routing, e.g. AppRouting:
import RouterModule.forRoot([
{ path: 'test/:id', component: MyComponent }
])
in your component:
#Component({ ... })
export class MyComponent implements OnInit, OnDestroy {
private readonly destroy$ = new Subject<void>();
constructor(readonly activatedRoute: ActivatedRoute) { }
ngOnInit(): void {
// snapshot gets the params just once
this.doSomethingWithRouteParams(
this.activatedRoute.snapshot.params,
this.activatedRoute.snapshot.queryParams);
// you can also listen to the changes using RxJS observables
combineLatest([
this.activatedRoute.paramMap,
this.activatedRoute.queryParamMap
])
.pipe(takeUntil(this.destroy$))
.subscribe(([params, queryParams]) =>
this.doSomethingWithRouteParams(params, queryParams));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private doSomethingWithRouteParams(params: ParamMap, queryParams: ParamMap): void {
console.log({
id: params.get('id'), // use the same param-name as in the router-config
categoryId: queryParams.get('categoryId'),
page: queryParams.get('page'),
});
}
}
See https://angular.io/guide/router-reference#activated-route for a list of properties on the activated route.
Conditional class binding
I assume by "deselecting the button" you probably mean conditionally binding the CSS classes. There are several ways to achieve this in Angular, the one I prefer over others due to readability would look like so:
<button
mat-stroked-button
class="category-item font-medium"
[class.text-white]="isSelected"
[class.bg-primary-500]="isSelected"
[class.text-primary-500]="!isSelected"
[class.bg-lightbluebg-500]="!isSelected"
>
{{ label }}
</button>
See the guide on binding to attributes and to the class attribute in particular.
Hint regarding the async pipe
Also, just as a side note: Note that [isSelected]='category.id === (selectedCategoryId$ | async)' will create a new subscription for every single app-category-button instance in this template. This might cause poor performance so you probably should refactor this to get the selected category just once, e.g.:
<ng-container *ngIf=(selectedCategoryId$ | async) as selectedCategoryId>
<app-category-button
label="Category #1"
[isSelected]="1 === selectedCategoryId"
></app-category-button>
<app-category-button
label="Category #2"
[isSelected]="2 === selectedCategoryId"
></app-category-button>
<app-category-button
label="Category #3"
[isSelected]="3 === selectedCategoryId"
></app-category-button>
</ng-container>
Please refer to the "Consuming observable data with ngIf and the async pipe"-section in the Angular ngIf: Complete Guide

Angular binding object from parent to child component

I have a problem with Anglular 8 and binding input parameters from parent component to child component.
I have the following setup:
-app.component.ts
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'parent-child-binding';
showTitle: boolean = true;
childData = [];
onBoolean(){
this.showTitle = !this.showTitle;
}
onComplexAdd(){
this.childData.push("data 4");
this.childData.push("data 5");
}
onComplexEdit(){
this.childData[0] = "data 1 edited";
this.childData[1] = "data 2 edited";
}
onComplexNew(){
this.childData = [
"data 1",
"data 2",
"data 3"
]
}
}
-app.component.html
<button (click)="onBoolean()">Boolean Bind</button>
<button (click)="onComplexNew()">Complex Object New</button>
<button (click)="onComplexEdit">Complex Object Edit</button>
<button (click)="onComplexAdd">Complex Object Add</button>
<app-child [data] = "childData" [showTitle]="showTitle"></app-child>
-child.component.ts
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '#angular/core';
#Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, OnChanges {
#Input() showTitle : boolean = true;
#Input() data : Array<any>;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
}
ngOnInit(): void {
}
}
-child.component.html
<h3 *ngIf="showTitle">Hello from child</h3>
<p *ngFor="let item of data">{{item}}</p>
So when I start I see the following:
and the console:
When I click on the first button, as expected the title Hello from child shows and disappears.
When I click on the second button, as expected I see:
and the console:
When I click on the third and forth buttons, nothing happens, not in the UI or console (the onChanges method seems that is not firing).
An I doing something wrong, or this that I want to achieve is not possible?
Best regards,
Julian
EDIT: After a comment and an answer from #MBB and #Apoorva Chikara, I've edited the code.
<button (click)="onBoolean()">Boolean Bind</button>
<button (click)="onComplexNew()">Complex Object New</button>
<button (click)="onComplexEdit()">Complex Object Edit</button>
<button (click)="onComplexAdd()">Complex Object Add</button>
<app-child [data] = "childData" [showTitle]="showTitle"></app-child>
The edition made the buttons to act (do something), but it is not what I expect.
What I mean:
When I click on the Complex Object Edit button in the UI I see:
But in the console, there is no ngOnChanges callback firing, but the binded object has changed, as we can see on the print screen (<p *ngFor="let item of data">{{item}}</p>) fired and printed out the new values.
The same happens when I click on the Complex Object Add button. In the UI I see:
But in the console the ngOnChanges callback is not firing, but the UI is containing the new added data.
I'm confused, can anyone advice please?
You have a very simple fix, you are not calling a function instead assigning its definition :
<button (click)="onComplexEdit()">Complex Object Edit</button> // call it as a function
<button (click)="onComplexAdd()">Complex Object Add</button>// call it as a function
The issue, you are facing for NgonChanges is due to the arrays passed by reference, this has a good explanation why this happens.

Pass transcluded content to grandchild component of a nested list

I know there are a few questions similar to this one but they aren't quite the same. I'm building a nested list and I want to display a custom html content in each grandchild along side common html. When I add the to ListComponent outside of the loop works, but if I pass it inside the loop to the inner child, it doesn't work like the example bellow. The html I pass in the in the code bellow isn't shown. I'm probably trying to solve this the wrong way but I couldn't get it to work any way I tried. Any of you guys know how to make this work?
Thanks!
export class Model {
title: string;
children?: Model[] = [];
}
#Component({
selector: 'list',
template: `
<ul>
<li *ngFor="let item of items">
<list-item [item]="item">
<div main-content>
<ng-content select="[main-content]"></ng-content>
</div>
</list-item>
<list [items]="item.children"></list>
</li>
</ul>
`
})
export class List {
#Input() items: Model[];
}
#Component({
selector: 'list-item',
template: `
<h1>{‌{ item.title }}</h1>
<div class="content">
<ng-content select="[main-content]"></ng-content>
</div>
`
})
export class ListItem {
#Input() item: Model;
}
#Component({
selector: 'app-main',
template: `
<list [items]="items">
<div main-content>
<h1>Test</h1>
</div>
</list>
`
})
export class AppMainComponent {
}
After much testing and going further through the duplicate question that was mentioned and its inner-links, it doesn't quite solve my issue, because the template I'm not trying to duplicate the content as it is in plain. I'm trying to inject into another component with other common html inside it, so if I just iterate through I'll just replicate the the template that I'm passing but I'll lose all other html that is inside.
I know it's a little too late but I've recently encountered a similar problem and an answer here would have helped me.
From what I understood you wish to define the template for your list items in the component in which you are using the list.
To do that you need to use ng-template in both the child component(the list) and also in the grandchild(the item).
The grandchild should be something like this:
#Component({
selector: 'app-item',
template:`
<ng-container *ngTemplateOutlet="itemTemplate; context: {$implicit: data}">
</ng-container>
`
})
export class ItemComponent {
#Input() public data;
#ContentChild('itemTemplate') public itemTemplate;
}
And the child:
#Component({
selector: 'app-list',
template:`
<app-item [data]="dataItem" *ngFor="let dataItem of data">
<ng-template #itemTemplate let-item>
<ng-container *ngTemplateOutlet="itemTemplate1; context: {$implicit: item}">
</ng-container>
</ng-template>
</app-item>
`
})
export class ListComponent {
#Input() public data;
#ContentChild('itemTemplate') public itemTemplate1;
}
And the component that uses the list:
<app-list [data]="data">
<ng-template #itemTemplate let-item>
<h1>--{{ item?.value}}--</h1>
</ng-template>
</app-list>
In the item you use an ng-template received as content and pass it the input as context. Inside this template you can define how the item will look inside of your list and though the context you have access to the data of that item. Because you can't just skip a level, you must also expect an ng-template in the list. Inside the list you define the ng-template for the item and inside that ng-template you are just using the ng-template received from above, through an ng-container. And finally at the highest level you can simply define the template for the item.

Categories