I have an angular component that I use as a tab in a for loop on the html page:
...
<ng-container *ngFor="let tabData of data$ | async;">
<tab-component
id="{{ tabData.id }}"
name="{{ tabData.name }}"
>
</tab-component>
</ng-container>
<child-component [selectedData]="selectedData"></child-component>
And in the .ts file:
public data$: Observable<Data[]>
public selectedData: Data
ngOnInit() {
this.data$ = this.service.getAllData();
}
ngAfterContentInit() {
this.data$.subscribe(items => this.selectedData = items[0])
}
I would like the first tab to always be the selectedData by default when first loading the page (element 0 in the array).
Then on click or the right/left arrow keys, dynamically update the value of selectedData passed to the child component.
So far, I've tried everything and the value of selectedData in the child component has always been undefined
Please help me, how can I achieve this!
I managed to get it so that the passed value on the child side is no longer undefined with an ngIf, so:
<child-component *ngIf=selectedData [selectedData]="selectedData"></child-component>
Subscribe for allData in the ngOnInIt itself and do check the value of items before assigning it - whether you are getting it or not and if you are not able to find the value there, then there must be the issue with the getAllDataService.
For child component, use double quotes to pass the value like this : <child-component [selectedTab]="selectedTab"></child-component>
Create a dummy variable in the parent ( or a hardcoded value ) and pass it to child. If your child component is working fine, then there's issue with only data and how you are assigning it.
Hope this helps!
Where exactly are you using the selectedData in your template HTML file?
In the snippet you provided there is a selectedTab used, but no selectedData anywhere...
<ng-container *ngFor="let tabData of data$ | async;">
<tab-component
id="{{ tabData.id }}"
name="{{ tabData.name }}"
>
</tab-component>
</ng-container>
<child-component [selectedTab]=selectedTab></child-component>
Also, you can follow #Eugene's advice and do:
ngOnInit() {
this.data$ = this.service.getAllData().pipe(
tap((items) => this.selectedData = items[0])
);
}
without using ngAfterContentInit() and the need to subscribe a second time.
You could use a subject to express the currently selected tab data, then use combineLatest to create an observable of both sources.
private data$: Observable<Data[]> = this.service.getAllData();
private selectedData$ = new BehaviorSubject<Data>(undefined);
vm$ = combineLatest([this.data$, this.selectedData$]).pipe(
map(([tabData, selected]) => ({
tabData,
selectedTab: selected ?? tabData[0]
})
);
setSelected(data: Data) {
this.selectedData$.next(data);
}
Here we create a single observable that the view can use (a view model) using combineLatest. This observable will emit whenever either of its sources emit.
We set the selectedData$ BehaviorSubject to emit an initial value of undefined. Then, inside the map, we set the selectedTab property to use tabData[0] when selected is not yet set. So, initially, it will use tabData[0], but after setSelected() gets called, it will use that value.
<ng-container *ngIf="let vm$ | async as vm">
<tab-component *ngFor="let tabData of vm.tabData"
[id] = "tabData.id"
[name] = "tabData.name"
(click) = "setSelected(tabData)">
</tab-component>
<child-component [selectedTab]="vm.selectedTab"></child-component>
</ng-container>
Related
Stackblitz Demo of my app
In my app I try to loop over a map (with *ngFor) and display every map key in a new expansion panel and the values in the "body" of the expansion panel:
The map has strings as keys and string arrays as values. After I have filled my map I pass it into the this.showMap. The only reason for this is that I can wait for showMap with *ngIf="showMap" in my HTML to make sure that all items are in the map before I show the Website to the user:
showMap: any;
ngOnInit() {
let myMap: Map<string, string[]> = new Map<string, string[]>();
myMap.set("food", ["apple", "sausage"]);
myMap.set("money", ["bitcoin", "dollar"]);
//... add more key, value pairs dynamically
this.showMap = myMap;
}
In my HTML I use an accordion and expansion-panels from material:
<div *ngIf="showMap">
<mat-accordion>
<mat-expansion-panel hideToggle *ngFor="let key of getKeys(showMap); index as i">
<mat-expansion-panel-header>
<mat-panel-title>
{{ key }}
</mat-panel-title>
</mat-expansion-panel-header>
<p>{{ getValues(showMap)[i] }}</p>
</mat-expansion-panel>
</mat-accordion>
</div>
and the getKeys() and getValues() function look like this and simply return the keys and values of the showMap:
getKeys(myMap: Map<string, string[]>) {
let keys = Array.from(myMap.keys());
console.log("keys: ", keys);
return keys;
}
getValues(myMap: Map<string, string[]>) {
let values = Array.from(myMap.values());
console.log("values: ", values);
return values;
}
My app is running as wanted, but in the console logs I see that the functions getKeys() and getValues() are called multiple times. I'd expect that getKeys() gets called once (because of the *ngFor statement) and the getValues() gets called twice (once for each key). But it's way more, about 10 times?:
It is not consider a good practice call a function on the template, as mentioned by #Raz Ronen, function is executed every time Angular change detection runs. And that can be too many times! I would extract the values and keys into instance variables and use them instead of calling the function directly on the template.
Check out this article about it: https://medium.com/showpad-engineering/why-you-should-never-use-function-calls-in-angular-template-expressions-e1a50f9c0496
I have a component that is passing props down to a child component. I would like to take the resulting value of [info] and use it as my condition for [value]
Basically, is the [info] prop equal to the String good-info ? If yes, set value to Happy otherwise set it to Sad
<some-component [info]="row.calculation.anotherCalculation.information"
[value]="info === 'good-info' ? 'Happy' : 'Sad' "
></some-component >
Of course I could use the same calculation for value that I'm using for info but that seems redundant. Also the calculation used in the info prop is much longer than the example one shown above.
You can refer to the child component with a template reference variable (e.g. #child), which will allow you to get the value of the info property. However, the code below causes an ExpressionChangedAfterItHasBeenCheckedError because one property depends on the other being set in the same detection cycle.
<some-component #child
[info]="row.calculation.anotherCalculation.information"
[value]="child.info === 'good-info' ? 'Happy' : 'Sad' ">
</some-component >
See this stackblitz for a demo.
The exception mentioned above can be avoided if you set value when a change event is triggered from the child component after info has been set:
export class SomeComponent {
private _info: string;
#Output() infoChanged = new EventEmitter<string>();
#Input() public get info(): string {
return this._info;
}
public set info(x: string) {
this._info = x;
this.infoChanged.emit(x);
}
#Input() public value: string;
}
<some-component #child
[info]="row.calculation.anotherCalculation.information"
(infoChanged)="child.value = $event === 'good-info' ? 'Happy' : 'Sad'" >
</some-component>
See this stackblitz for a demo.
I would suggest using a method doing your calculations (in this example only returning the nested attribute), something like
class SomeComponent {
calculate(row: any) {
return row.calculation.anotherCalculation.information;
}
}
Then, in your HTML you can do
<div *ngFor="let row of rows">
<some-component [info]="calculate(row)"
[value]="calculate(row) === 'good-info' ? 'Happy' : 'Sad' "
></some-component>
</div>
Here is a parent component's template:
<ng-container *ngFor="let set of timeSet; index as i">
<time-shift-input *ngIf="enabled"
[ngClass]="{
'mini-times' : miniTimes,
'field field-last': !miniTimes,
'field-central': !canAddSet,
'field-central--long': (canAddSet || canDeleteSet) && !miniTimes }"
[startHour]="set.startHour"
[endHour]="set.endHour"
[endsNextDay]="set.endsNextDay"
[canAddSet]="canAddSet()"
[canDeleteSet]="canDeleteSet(i)"
[required]="true"
(onAddSet)="onAddSet(i)"
(onDeleteSet)="onDeleteSet(i)"
(onChange)="onShiftTimes($event, i)"></time-shift-input>
</ng-container>
Here is the code which will update the timeSet array after onChange event has been triggered:
public onShiftTimes( set: TimeSchedule | Array<TimeSchedule>, ind?: number ): void {
if ( ind !== undefined ) {
this.timeSet[ind] = <TimeSchedule>set;
} else {
this.timeSet = <Array<TimeSchedule>>set;
}
this.timeChanged.emit({
data: this.timeSet,
di: this.dayIndex
});
}
The child component, <time-shift-input> is getting re-rendered every time the onShiftTimes method has been called, EVEN when the the length of the array stays the same.
Which is a bummer, because it breaks user experience in an annoying way (removes focus, etc). I thought that pushing OR updating an index of an existing array won't change the object reference for the array, so the ngFor loop will not be triggered. However ngOnInit in <time-shift-input> is getting called every time after onShiftTimes...
Any ideas how to prevent re-rendering?
RTFM, as they say.
trackByFn to the rescue - that was the simple and correct solution to my problem. More on this gem:
https://angular.io/api/common/NgForOf#ngForTrackBy
https://www.concretepage.com/angular-2/angular-4-ngfor-example#trackBy
I'm trying to assign a dynamic object value named destination inside ngFor like this:
<div *ngFor="let obj of object.destination"><p [innerHTML]="cont.disc"></p> </div>
This destination is supposed to change dynamically using property binding (it works)
In my component.ts there is a function which assigns the dynamic value I get to the destination value, and connect it to the object key:
public getDestination() {
this.destination = this.to;
return (this.destination = `${this.content}.${this.destination}`); }
But all I get is that {{ object.destination }} equal to
<p>[object Object].india</p>
How can I make the object value dynamic so it will change depending on the property binding?
I want to call a method inside ngFor and want to assign the return value to a local variable. I tried this approach:
<div *ngFor="let card of partNumberPlanningCards; let i = index" class="partnumber-basic-card">
<div
*ngFor="let scope of containerScopeLineItems; let lineItem = getOperatingWeightBy(scope.containerScopeId, card.typeId)">
</div>
</div>
But it's showing this error:
Parser Error: Unexpected token (, expected identifier, keyword, or string at column 74 in [let scope of containerScopeLineItems; let lineItem = getOperatingWeightBy(scope.containerScopeId, card.typeId)] in ng
You can store function returned value in any of the attribute of element and use the element reference to get value.
<div *ngFor="let card of partNumberPlanningCards; let i = index" class="partnumber-basic-card">
<div *ngFor="let scope of containerScopeLineItems" #lineItem [title]="getOperatingWeightBy(scope.containerScopeId, card.typeId)">
{{lineItem.title}}
</div>
</div>
Here title has been used but you can use any of valid attribute of element.
What are you trying to do here? Calling a function within ngFor to update DOM is certainly a bad practice to do.
As it looks, you want to filter based on the containerScopeId and typeId, you could simply do this and assign to a variable in the component.ts level rather than calling a function in the template.
Just declare a variable lineItem and use array.find() with your conditions to get the matching item.
Not sure if it helps you. But just tried something.. You can create a ng-template and use ng-container to display the template.
HTML:
<div *ngFor="let card of partNumberPlanningCards;" class="partnumber-basic-card">
{{card?.typeId}}
<div *ngFor="let scope of containerScopeLineItems;">
<ng-container *ngTemplateOutlet="eng; context: getOperatingWeightBy(card.typeId, scope.containerScopeId)"></ng-container>
{{scope?.containerScopeId}}
</div>
<ng-template #eng let-obj='value'>
{{obj}}
</ng-template>
TS:
export class AppComponent {
partNumberPlanningCards = [
{typeId : 'xx'}
]
containerScopeLineItems = [
{containerScopeId : 2}
];
getOperatingWeightBy(a,b){
return {value:a+b};
}
}
https://stackblitz.com/edit/angular-ngfor-loop