Functions as Element Attributes and the Angular ExpressionChangedAfterItHasBeenCheckedError - javascript

I'm creating a summary row on a tree table that also has filtering (I'm using the ng Prime component library, but I don't think that's germane to the error or question). I want my summary row to only summarize the information currently in view, thus the summary should change when the tree is expanded or when a filter is applied.
I achieved this goal by adding row data to a map via a function I set as an element attribute [attr.summaryFunction]="summaryFunction(i,rowData[col.field])", and reinitializing the map with ngDoCheck. Without this step, reinitializing the map in ngDoCheck, totals accumulate with every repaint or change and result in incorrect values. (I tried reinitializing the map with several other lifecycle hooks ngOnInit, ngOnViewInit, and ngOnChanges, but the only lifecycle hook that effectively reinitialized the map was ngDoCheck).
My approach works perfectly (yay me!), but I get an ExpressionChangedAfterItHasBeenCheckedError in the browser, three times, for each column. To address this error, I tried all 4 approaches to fixing the error recommended in the documentation:
using different lifecycle hooks --> totals are incorrect
I added this.cd.detectChanges() to summary function --> infinite loop
I wrapped the if statement in setTimeout(() => if(statement){}) --> infinite loop
Promise.resolve().then(() => if(statement){}) --> infinite loop
Again, my approach currently works exactly as I want it to, I just want these annoying errors to go away. I'd also like to understand why the usual fixes cause an infinite loop.
Here's my TS layer;
export class PrimetreeComponent implements OnInit, DoCheck {
sampleData: TreeNode[] = []
columns: any[] = [];
summaryMap = new Map();
columnIndex = 0;
constructor(private nodeService: NodeService, private cd: ChangeDetectorRef) {}
ngOnInit(): void {
this.nodeService.getFilesystem().subscribe(data => {this.sampleData = data});
this.columns = [
{field: 'name', header: 'name'},
{field: 'size', header: 'size'},
{field: 'type', header: 'type'}]
}
ngDoCheck(): void {
this.summaryMap = new Map();
}
summaryFunction(columnIndex:number, rowData:any){
if(this.summaryMap.has(columnIndex)){
//if row data is a non-number string we just count the rows
if(Number(rowData)){
this.summaryMap.set(columnIndex, this.summaryMap.get(columnIndex)+Number(rowData));
} else {
this.summaryMap.set(columnIndex, this.summaryMap.get(columnIndex)+1)
}
} else {
if(Number(rowData)){
this.summaryMap.set(columnIndex, Number(rowData));
} else {
this.summaryMap.set(columnIndex, 1)
}
}
}
}
And here's my HTML Template:
<p-treeTable #tt [value]="sampleData" [columns]="columns">
<ng-template pTemplate="caption">
<div style="text-align: right">
<span class="p-input-icon-left">
<i class="pi pi-search"></i>
<input
pInputText
type="text"
size="50"
placeholder="Global Filter"
(input)="tt.filterGlobal($any($event.target).value, 'contains')"
style="width:auto"
/>
</span>
</div>
</ng-template>
<ng-template pTemplate="header" let-columns>
<tr>
<th *ngFor="let col of columns">
{{ col.header }}
</th>
</tr>
<tr>
<th *ngFor="let col of columns">
<input
pInputText
type="text"
class="w-full"
(input)="
tt.filter($any($event.target).value, col.field, col.filterMatchMode)
"
/>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-rowNode let-rowData="rowData" >
<tr>
<td *ngFor="let col of columns; let i = index;" [attr.summaryFunction]="summaryFunction(i,rowData[col.field])">
<p-treeTableToggler
[rowNode]="rowNode"
*ngIf="i == 0"
(columnIndex)="i"
></p-treeTableToggler>
{{ rowData[col.field] }}
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td [attr.colspan]="columns.length">No data found.</td>
</tr>
</ng-template>
<ng-template pTemplate="summary">
<tr>
<td *ngFor="let col of columns; let i = index;">
{{ col.header }}--sumMap--{{summaryMap.get(i) }}
</td>
</tr>
</ng-template>

I am not familiar with the approach of binding a function to an elements attribute, but it seems kind of hacky to me. Correct me if I'm wrong, but the reason for this is to have a way to trigger the function when the node is added to the dom?
I did not test this myself, but I think you could make use of the #ContentChildren decorator see docs. It exposes an observable that you can subscribe to to react to changes of the content dom. Ideally, you could run the summary creation and updating the summaryMap in one step.
Update:
I played around with the idea a bit. One solution for what you are trying to achieve is to use #ViewChildren decorator to get a QueryList of all the rows currently rendered. This can be achieved in the following way:
<ng-template pTemplate="body" let-rowNode let-rowData="rowData" >
<tr [ttRow]="rowNode" #tableRow [attr.file-size]="rowData.size">
<td>
<p-treeTableToggler [rowNode]="rowNode"></p-treeTableToggler>
{{ rowData.name }}
</td>
<td>{{ rowData.size }}</td>
<td>{{ rowData.type }}</td>
</tr>
</ng-template>
Note the template ref on <tr [ttRow]="rowNode" #tableRow [attr.file-size]="rowData.size"> along with binding the rowData.size to an attribute of the element.
You can then grab that list of rows rows that are currently mounted to the dom like so:
// inside your component
#ViewChildren('tableRow') view_children!: QueryList<ElementRef>;
// get the list, calculate initial sum and subscribe to changes.
// We do that in ngAfterViewInit lifecycle hook as the
// list is guaranteed to be available at that point of time:
ngAfterViewInit() {
this.calculateSum(this.view_children);
this.view_children.changes.subscribe(
(list: QueryList<ElementRef>) =>
this.calculateSum(list)
);
}
You can find a running example of this here.

Related

ngFor showing the last element only from array

Trying to loop over an array and display the results, but only the last element showing multiple times.
Here is my code.
Making a get request.
showItems(id: any): Observable<any> {
return this.http.get(`${this.url}${id}`)
}
Console logging works fine here, I can see the expected results.
ngAfterViewInit(): void {
this.showItems()
}
showItems(): void {
const id: any = []
this.idFromList.forEach((val: any) => id.push(val.nativeElement.innerText))
for(let i = 0; id.length > i; i++) {
this.ItemService.showItems(id[i]).subscribe(data => {this.api_items = data.item.current
console.log(this.api_items)});
}
}
HTML
<table>
<thead>
<tr>
<td>Name</td>
<td>Price</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of items | filter: searchQuery" >
<td >
<a href="" #btn>{{item.id}}</a>
{{ item.name }}
</td>
<td *ngFor="let api_item of api_items | keyvalue">
{{ api_item.value }}
</td>
</tr>
</tbody>
</table>
Tried using fake JSON server, same results.
1) Quick answer
Push items in an array instead of replacing its value :
api_items = [];
this.ItemService.showItems(id[i]).subscribe(data => {
this.api_items.push(data.item.current);
});
2) Complete answer
As #MoxxiManagarm said, you're replacing api_items value at each iteration of your for loop.
You might want to check & use RxJS to handle multiple observables in a cleaner way: for example, by generating an array of Observable to resolve them together.
3) Ressources & useful links you might want to check for more information
https://www.learnrxjs.io/

Better version to prop data from child to parent Angular? [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
I have a problem props data from child to parent.
Imagine this. The search input button is the child component and the table is the parent component.
Each type and type I need to pass the information to the parent and further sort array.
Check code:
<app-search (sendEvent)="receivedMessage($event)"></app-search>
<table class="w-100">
<thead>
<tr>
<th> Name</th>
<th> Size</th>
<th> Price</th>
<th> Date added</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let menu of allMenus | searchInput:searchTerm; let i=index">
<td> {{ menu.name }}</td>
<td> {{ menu.size }}cm</td>
<td> {{ menu.price | currency : 'USD' : 'symbol' : '1.0-0' }}</td>
<td> {{ menu.date | date: 'dd/MM/yyyy hh:mm' }}</td>
<td>
<button class="btn btn-secondary mr-3"> Edit </button>
<button (click)="removeMenu(menu)" class="btn btn-secondary"> Remove </button>
</td>
</tr>
</tbody>
</table>
Inside parent ( table ) :
searchTerm: any = '';
receivedMessage(searchMessage: string) {
if (searchMessage) { this.searchTerm = searchMessage; }
}
Inside child component:
<input (ngModelChange)="onChange($event)" [(ngModel)]="searchTerm" type="text" placeholder="Search menu items">
#Output() sendEvent = new EventEmitter;
searchTerm: any = ''
constructor() { }
ngOnInit(): void {
}
onChange(e: string){
this.sendEvent.emit(e);
}
This is work.
But situations in which he does not work:
When I start deleting everything from the input it sometimes doesn't show me all the data again.
When i copy all string inside input
Second situation is not important more for me.
Only is problem when I delete all typed data from input not clear all results ....
It doesn't enter your if (searchMessage) { this.searchTerm = searchMessage; } block because you conditionally check for falsy value - if (searchMessage)
When you delete everything from input, the actual value is empty string - '' which is a falsy value in Javascript.
I would recommend to remove if statement because it can't be anything else than string from your input with NgModel applied.
I could not think of any better way of communication to deliver the searchTerm from the child component to the parent component.
If you consider another way like using a service, I don't recommend using a service in such a case while it has to be good for communication between multiple threads not only two parent-to-child threads.
Another point to consider:
check the condition in the parent component if (searchMessage) { }. This has to be changes to suit your needs because using it like that would not do any thing in case you delete the whole search word searchTerm = '' which will not proceed inside your condition.
Add an else statement to this if to suit your both situations.

How to validate duplicate entry in angular form builder array?

I am using angular6 reactive form with form builder and form array. I am facing problem with duplicate subject entry from drop down in form array. How to validate to avoid duplicate entry in from array.
I have a subject list with drop down. When i click on add button then a subject array will add. If i add similar subject it also be added. But i want to avoid duplicate subject entry. When i entry duplicate subject then a validation message will show and save button will disable.
stackblitz
ts code
olevelSubForm = this.fb.group({
olevelSubArray: this.fb.array([
])
});
olevelSubjectList: any = [ 'Geography','Mathematics',
'Physics','Chemistry'];
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.addItemOlevel();
}
// olevel
createOlevelSub(): FormGroup {
return this.fb.group( {
olevelSubject: new FormControl('', Validators.compose(
[
Validators.required
]
)),
});
}
addItemOlevel() {
const control = <FormArray>this.olevelSubForm.controls.olevelSubArray;
control.push(this.createOlevelSub());
}
saveData() {
console.log('saved')
}
html code
<form [formGroup]="olevelSubForm" >
<div formArrayName="olevelSubArray">
<table>
<thead>
<tr style="font-size: 15px;">
<th>Subject</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of olevelSubForm.get('olevelSubArray').controls; let i = index;" [formGroupName]="i">
<td>
<select formControlName="olevelSubject">
<option *ngFor="let olevelSub of olevelSubjectList" [value]="olevelSub">{{ olevelSub }}</option>
</select>
</td>
<td>
<button style="float: right" [disabled]="olevelSubForm.invalid"(click)="addItemOlevel()"> Add
</button>
</td>
</tr>
</tbody>
</table>
<button [disabled]="olevelSubForm.invalid"(click)=saveData()>Save</button>
<pre> {{ olevelSubForm.value | json }} </pre>
</div>
In my comment I put and answer with a different aproach: that the options you can select was different in each level, so you can not choose some yet choosed. that, if you has a function like
getSubjectForFormArray(i, olevelSubjectList) {
return i == 0 ? olevelSubjectList :
this.getSubjectForFormArray(i - 1, olevelSubjectList.filter(x =>
x != this.olevelSubForm.get('olevelSubArray').value[i-1].olevelSubject))
}
Yes is a recursive function that begins with all the options and in each step, remove from the list the value we are choose. So, your option list can be like
<option *ngFor="let olevelSub of getSubjectForFormArray(i, olevelSubjectList)"
[value]="olevelSub">{{ olevelSub }}</option>
Anyway, this dont' avoid that if, e.g we choose "Mathematics","Chemistry", if change the first combo to Chemistry, we'll get two "Chemistry". So, we are going to check each change of the array. That's in ngOnInit
this.olevelSubForm.get('olevelSubArray').valueChanges.subscribe(res=>{
res.forEach((x,index)=>{
//res is the value of the array,e.g. -in one moment-
//[{olevelSubject:'Mathematics'},{olevelSubject:'Chemistry'},{olevelSubject:null}]
if (index)
{
//before becomes an array with the values selected before, eg. -in one moment-
// ['Mathematics','Chemistry']
const before=res.slice(0,index).map(x=>x.olevelSubject)
if (before.find(x=>x==res[index].olevelSubject))
{
(this.olevelSubForm.get('olevelSubArray') as FormArray)
.at(index).setValue({olevelSubject:null})
}
}
})
})
See the stackblitz

sharing event driven variable across component in angular 2

I have a component which has a data table which I filter using a pipe,
The way I trigger and sent new argument to the pipe is on input-event on a input tag , I capture the input in 'targetInput' variable,
The above setup works, here is how it looks like:
<tr >
<td *ngFor="let column of currentView.columns">
<div *ngIf="column.label">
<input placeholder="{{column.label}}" id="{{column._devName}}" type="text"
(input)="targetInput = {targetValue:$event.target.value,targetId:$event.target.id,currentFilterMap:currentFilterMap}">
</div>
</td>
</tr>
<ng-container *ngFor="let task of (currentView.tasks | countryPipe:targetInput); let i=index">
<tr class="worktask" (click)="setCurrentTask($event, task)" (dblclick)="openWindowNewTab(getOpenTaskURL(task, currentView.process))"
id="workspace_table_wo_{{ task.workOrderId }}_task_{{ task.taskId }}"
[class.table-active]="isSelected(task)">
<td *ngFor="let column of currentView.columns">{{task[column.devName]}}</td>
</tr>
Now I decide , that I want a separate component for the input tag , so I split the html and make a parent-child setup and pass the shared variable using #Input decorator,
This is how the new setup looks ,
Parent html:
<tr >
<td *ngFor="let column of currentView.columns">
<filterTagBox [taskCol] = "column" [currentFilterMap] = "currentFilterMap"></filterTagBox>
</td>
</tr>
<ng-container *ngFor="let task of (currentView.tasks | countryPipe:targetInput); let i=index">
<tr class="worktask" (click)="setCurrentTask($event, task)" (dblclick)="openWindowNewTab(getOpenTaskURL(task, currentView.process))"
id="workspace_table_wo_{{ task.workOrderId }}_task_{{ task.taskId }}"
[class.table-active]="isSelected(task)">
<td *ngFor="let column of currentView.columns">{{task[column.devName]}}</td>
</tr>
Now I can't seem to pass the targetInput from the child component back to the parent on the input event, Not sure if this is the way I should implement this or if there is a better way.
I think in your case parent is Parent html and child is filterTagBox. if you want transfer value from parent to child you need use #input
if you want transfer value from child to parent you need use EventEmitter and #Output
more info.
https://angular.io/docs/ts/latest/cookbook/component-communication.html
I use BehaviorSubject to notify any component (the parent in your situation) that subscribes it. It's a special type of observables. A message service can do it for you. Define a message model (you can even use a simple string if you prefer) and create a message service:
import {Observable, BehaviorSubject} from 'rxjs/Rx'; //
import {Message} from "../../models/message"; // Your model
... inside your message service class:
private _newMessage = new BehaviorSubject<Message>(new Message);
getMessage = this._currentUser.asObservable();
sendMessage(message: Message) { this._newMessage.next(message) }
In a component (e.g. in a parent), you can subscribe getMessage subject like this:
this.messageService.getMessage.subscribe(
message => {
// a message received, do whatever you want
if (message == "so important message")
this.list = newList;
// ... so on
});
This way, multiple components can subscribe to this BehaviorSubject, and any trigger in any component/service that uses sendMessage method can change these subscribed components immediately. For you, that can be a child component:
... you successfully made something in your
... child component, now use the trigger:
this.messageService.sendMessage(new Message("so important message", foo, bar));
Thanks for the answers , I did figure out how I could do this ,and although I found using behaviour service interesting I decided to use a output variable to sent data form the child component to the parent which would ultimatedly sent to the pipe,
Here is what I did :
Child component HTML:
<div *ngIf="taskCol.label">
<div id="{{taskCol._devName}}_tagBox"></div>
<input placeholder="{{taskCol.label}}" id="{{taskCol._devName}}" type="text"
<!-- Call childComponent.onInput passing event parameters -->
(input)="onInput({targetValue:$event.target.value,targetId:$event.target.id})">
</div>
Child component.ts :
#Component({
selector: 'filterTagBox',
template: require('./filterTagBox.component.html')
})
export class FilterTagBox{
private colValues:string[];
public containsQueries:boolean;
private regex:RegExp;
#Input() public taskCol:TaskColumn;
#Output() onItemInput = new EventEmitter<any>(); // bound event to the parent component
// constructor and other hidden methods...
onInput(targetInput : any){
this.onItemInput.emit(targetInput); //trigger onItemInput event on inputBox input
}
}
Parent component html :
<tr >
<td *ngFor="let column of currentView.columns">
<!-- Catch custom onItemInput event which was triggered in the child -->
<filterTagBox (onItemInput)="filterBoxPipeData = {targetValue:$event.targetValue,targetId:$event.targetId,currentFilterMap:currentFilterMap}" [taskCol] = "column" ></filterTagBox>
</td>
</tr>
<!--sent the data retrieve from the input i.e filterBoxPipeData to the pipe i.e tagBoxFilterPipe along with data to be filtered i.e currentView.task -->
<ng-container *ngFor="let task of (currentView.tasks | tagBoxFilterPipe:filterBoxPipeData); let i=index">
<tr>
<!--hidden html -->
</tr>

Angular 2 set boolean at end of iteration using *ngFor

I am loading some data via rest-endport to safe it in an array. In an *ngFor directive i am currently iterating through all that data. So everthing is working.
Is there any possible way in Angular to e.g. set a boolean whenever the iteration of the array is finished (so that i can fire another function on complete)?
-- Edit--
On the ngOnInit lifecycle method i am retreiving the data:
ngOnInit() {
this.restService.getSomeBooks(5000).subscribe(buch => {
this.buecher = buch;
this.fetched = true;
})
}
After that - in HTML - i am iterating over that data:
<table>
<thead>some table-heads</thead>
<tbody>
<tr style="text-align: center" *ngFor="let buchforTable of buecher">
<td>{{buchforTable.author}}</td>
<td>{{buchforTable.erscheinungsdatum}}</td>
<td>{{buchforTable.isbn.toString()}}</td>
<td>{{buchforTable.verlag}}</td>
<td>{{buchforTable.uuid}}</td>
</tr>
</tbody>
</table>
The *ngFor-iteration should set a boolean variable whenever it is done.
As Alex Po already mentioned, trackBy is working. As you can see in the following snippet, with trackBy it is possible to handle events based on the current index of the iteration.
<tbody>
<tr style="text-align: center" *ngFor="let buchforTable of buecher; trackBy: trackByFn">
<td>{{buchforTable.author}}</td>
<td>{{buchforTable.erscheinungsdatum}}</td>
<td>{{buchforTable.isbn.toString()}}</td>
<td>{{buchforTable.verlag}}</td>
<td>{{buchforTable.uuid}}</td>
</tr>
</tbody>
To measure the time of the rendering process of all array-objects the trackBy-function would look like this (array contains 5000 objects here -> index 0 to 4999):
trackByFn(index){
if(index == 0)
this.renderStart = performance.now();
if(index == 4999) {
var renderStopp = performance.now();
var timeToRender = renderStopp - this.renderStart;
}
}

Categories

Resources