sharing event driven variable across component in angular 2 - javascript

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>

Related

Functions as Element Attributes and the Angular ExpressionChangedAfterItHasBeenCheckedError

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.

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.

angular5:ngFor works only in second button click

My scenario as follows
1) When the user enters a keyword in a text field and clicks on the search icon it will initiate an HTTP request to get the data.
2)Data is rendered in HTML with ngFor
The problem is on the first click the data is not rendered in HTML but I am getting the HTTP response properly, and the data rendered only on second click.
component.ts
export class CommerceComponent implements OnInit {
private dealList = [];
//trigger on search icon click
startSearch(){
//http service call
this.getDeals();
}
getDeals(){
this.gatewayService.searchDeals(this.searchParams).subscribe(
(data:any)=>{
this.dealList = data.result;
console.log("Deal list",this.dealList);
},
(error)=>{
console.log("Error getting deal list",error);
this.dealList = [];
alert('No deals found');
}
);
}
}
Service.ts
searchDeals(data){
var fd = new FormData();
fd.append('token',this.cookieService.get('token'));
fd.append('search',data.keyword);
return this.http.post(config.url+'hyperledger/queryByParams',fd);
}
HTML
//this list render only on second click
<div class="deal1" *ngFor="let deal of dealList">
{{deal}}
</div>
UPDATE
click bind html code
<div class="search-input">
<input type="text" [(ngModel)]="searchParams.keyword" class="search" placeholder="" autofocus>
<div class="search-icon" (click)="startSearch()">
<img src="assets/images/search.png">
</div>
</div>
According to Angular official tutorial, you could have problems if you bind a private property to a template:
Angular only binds to public component properties.
Probably, setting the property dealList to public will solve the problem.
Remove "private" from your dealList variable. That declaration makes your component variable available only during compile time.
Another problem: you are implementing OnInit in yout component but you are not using ngOnInit. Angular is suposed to throw an error in this situation.
My suggestion is to switch to observable:
I marked my changes with CHANGE
component.ts
// CHANGE
import { Observable } from 'rxjs/Observable';
// MISSING IMPORT
import { of } from 'rxjs/observable/of';
export class CommerceComponent implements OnInit {
// CHANGE
private dealList: Observable<any[]>; // you should replace any with your object type, eg. string, User or whatever
//trigger on search icon click
startSearch() {
//http service call
this.getDeals();
}
getDeals() {
this.gatewayService.searchDeals(this.searchParams).subscribe(
(data:any)=>{
// CHANGE
this.dealList = of(data.result);
console.log("Deal list",this.dealList);
},
(error)=>{
console.log("Error getting deal list",error);
// CHANGE
this.dealList = of([]);
alert('No deals found');
}
);
}
}
HTML
<!-- CHANGE -->
<div class="deal1" *ngFor="let (deal | async) of dealList">
{{deal}}
</div>
Try this:
this.dealList = Object.assign({},data.result);
Better do this inside the service.
By default, the angular engine renders the view only when it recognizes a change in data.

Angular 5 w/Angular Material - Extracting an object property from ngFor to use in component

<div *ngFor="let player of players">
<h4 mat-line>{{player.firstName}} {{player.lastName}} - {{player.id}}</h4>
</div>
I'm doing a HTTP get call from my player.service.ts file, and then looping through the player object that gets returned, printing out the firstName, lastName and id properties in a massive player list.
I need to extract a specific player ID at a given point in the loop so that I can pass that down to a child Edit Player component that opens a modal with that specific player's information pre-filled in the form (using NgModel and a getbyId call to the API to get the player object). How would I go about doing this?
It looks like you're using #angular/material. If so, you should be able to use a click handler that loads the player data and opens up a dialog with their provided dialog service.
eg:
Template:
<div *ngFor="let player of players">
<h4 (click)="handlePlayerClick(player.id)"
mat-line>
{{player.firstName}} {{player.lastName}} - {{player.id}}
</h4>
</div>
Component:
constructor(private dialogService: MatDialog, private playerApi: PlayerApiService) { }
handlePlayerClick(playerId: string): void {
// potentially open a MatDialog here
this.playerApi.getById(playerId).subscribe((playerData: PlayerInterface) => {
const dialogConfig = {
data: {
playerData: playerData
}
} as MatDialogConfig;
this.dialogService.open(EditPlayerComponent, dialogConfig);
});
}
Documentation: https://material.angular.io/components/dialog/api
You'd want your child component to have a property like #Input() playerId: any; and then simply pass it in square brackets into the child tag like so:
<div *ngFor="let player of players">
<h4 mat-line>{{player.firstName}} {{player.lastName}} - {{player.id}}</h4>
<edit-player [playerId]="player.id"></edit-player>
</div>

Trouble navigating an Angular form to access it's controls

Preface:
I am having the hardest time trying to figure out what sounds like an easy process for nested angular forms. I am dealing with a few components here and some of the formGroups and formArrays are being dynamically created and its throwing me off.
Apologies for the large code dump, but its the minimal example I was able to come up with to try and explain my problem.
The parent component is very straight forward as it only has two formControls. I then pass the form to the tasks component to have access to it.
Parent Component
this.intakeForm = this.fb.group({
requestor: ['', Validators.required],
requestJustification: ['', Validators.required]
});
HTML:
<form [formGroup]=“intakeForm”>
<app-tasks
    [uiOptions]="uiOptions"
    [intakeForm]="intakeForm">
</app-tasks>
</form>
Tasks Component
Some method in here will trigger generateTask which creates the new form group.
ngOnInit() {
this.intakeForm.addControl('tasks', new FormArray([]));
}
// Push a new form group to our tasks array
generateTask(user, tool) {
const control = <FormArray>this.intakeForm.controls['tasks'];
control.push(this.newTaskControl(user, tool))
}
// Return a form group
newTaskControl(user, tool) {
return this.fb.group({
User: user,
Tool: tool,
Roles: this.fb.array([])
})
}
HTML:
<table class="table table-condensed smallText" *ngIf="intakeForm.controls['tasks'].length">
<thead>
<tr>
<th>Role(s)</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let t of intakeForm.get('tasks').controls let i = index; trackBy:trackByIndex" [taskTR]="t" [ui]="uiOptions" [intakeForm]="intakeForm" [taskIndex]="i">
</tr>
</tbody>
</table>
TR Component
Some method in here will trigger the addRole method which will add the form group.
#Input('taskTR') row;
#Input('ui') ui;
#Input('intakeForm') intakeForm: FormGroup;
// Add a new role
addRole($event, task) {
let t = task.get('Roles').controls as FormArray;
t.push(this.newRoleControl($event))
}
// Return a form group
newRoleControl(role) {
return this.fb.group({
Role: [role, Validators.required],
Action: [null, Validators.required]
})
}
HTML
<td class="col-md-9">
<ng-select [items]="ui.adminRoles.options"
bindLabel="RoleName"
bindValue="Role"
placeholder="Select one or more roles"
[multiple]="true"
[clearable]="false"
(add)="addRole($event, row)"
(remove)="removeRole($event, row)">
</td>
The Question
I need to add formControlName to my TR Component, specifically on the ng-select. However, when I try and add a formControlName, it tells me that it needs to be within a formGroup.
From what I can tell, the formGroup is in the tasksComponent and is wrapping the whole table so its technically within a formGroup?
My end goal is to be able to add the formControlName to this input but I am having a hard time trying to figure out the path to get there.
Here is an image of the full form object.
The last expanded section, Role, is what this input should be called via formControlName so that I can perform validation and what not on the control.
Updates
Edit 1 - Changes for #Harry Ninh
Tasks Component HTML
<tbody>
<tr *ngFor="let t of intakeForm.get('tasks').controls let i = index; trackBy:trackByIndex" [taskTR]="t" [ui]="uiOptions" [intakeForm]="intakeForm" [taskIndex]="i" [formGroup]="intakeForm"></tr>
</tbody>
TR Component HTML
<td class="col-md-9">
<ng-select [items]="ui.adminRoles.options"
bindLabel="RoleName"
bindValue="Role"
placeholder="Select one or more roles"
[multiple]="true"
[clearable]="false"
formControlName="Roles"
(add)="addRole($event, row)"
(remove)="removeRole($event, row)">
</td>
Result: ERROR Error: formControlName must be used with a parent formGroup directive.
You are expected to declare [formGroup]="intakeForm" in the root tag of every component that wraps all formControlName, formGroupName and formArrayName properties. Angular won't try to go up the hierarchy to find that when compiling the code.
In TR Component template, the root element (ie. the <td>) should have [formGroup]="intakeForm", in order to tell Angular the formControlName who is related to.

Categories

Resources