I have thins application in angular, in one of my components, I get some data through an #Input() columns from the parent component.
And I iterate it in my view with a normal *ngFor.
let column of columns
But when I change the data my *ngFor doesn't change. I already check the changes in the ngOnChanges(changes: SimpleChanges) and also I already tried to reassign the value, I used lodash and used the ChangeDetectorRef.
import _ from 'lodash';
constructor( private cdr: ChangeDetectorRef){}
ngOnChanges(changes: SimpleChanges){
debugger; // this debugger does enter
this.columns = changes.columns.currentValue; ////didn't work
this.columns = [...this.columns]; ////didn't work
this.columns = _.cloneDeep(this.columns); ////didn't work
this.columns = [..._.cloneDeep(this.columns)]; ////didn't work
this.cdr.detectChanges(); ////didn't work
}
Whatever I tried my view remains the same in the *ngFor, before it, I put a {{colums | json}} to see if my object it's been changed, and it is, it's just the *ngFor the one that is not updating.
-----------------------------Update----------------------------
I also assigned it the changes.columns.currentValue value and I have a debugger in the ngOnChanges that enters every time I change the value
The currentValue in the SimpleChanges holds the current snapshot of the data when its dependencies changes. Add this.columns = changes.columns.currentValue to the ngOnChanges.
ngOnChanges(changes: SimpleChanges){
this.columns = changes.columns.currentValue;
}
You can use set/get methods for change detection.
Let assume that we have an input variable named columnID
We can write our code like following.
ColumnComponent:
import { Component, Input } from '#angular/core';
#Component({
selector: 'app-column',
templateUrl: './column.component.html',
styleUrls: ['./column.component.css']
})
export class ColumnComponent {
_columnID: string;
set columnID(value: string) {
this._columnID = value;
this.propChanged();
}
get columnID(): string {
return this._columnID;
}
}
Your html you can use this component like this;
<app-column [columnID]="someVariable"></app-column>
When someVariable changes, ColumnComponent's set columnID() method will be called.
You can find more information for getter/setter here
Related
I have a simple table with angular and typescript. I am sending table data from parent class to child class(which includes the table) and in this example data name is _domainData. It is taking the data correctly but I want to show it on table and I do not know how to assign it to my main table data variable domain_Data.
As in the example: if i say this.domain_Data = this._domainData;in ngOnInit() method.
#Component({
selector: 'mg-domain-display',
templateUrl: './domain-display.component.html',
styleUrls: ['./domain-display.component.scss']
})
export class DomainWhiteListingDisplayComponent implements OnInit {
private _domainData = new BehaviorSubject<Domain[]>([]);
displayedColumns = ['id', 'domain'];
domain_Data: Domain[] = [];
#Input()
set domainData(value: Domain[]) {
this._domainData.next(value);
}
get domainData() {
return this._domainData.getValue();
}
constructor(private globalSettingService: GlobalSettingsService, private dialog: MatDialog) {
}
ngOnInit() {
this.domain_Data = this._domainData;
}
}
And the error is Type:BehaviourSubject is not assignable to type Domain[]. Property 'includes'is missing in type 'BehaviourSubject'
As I said my main table data variable is domain_Data:
<mat-table #table [dataSource]="domain_Data">
You need to subscribe and get the value from BehaviorSubject
ngOnInit() {
this._domainData.subscribe((data) => this.domain_Data = data);
}
Alternatively, As few have commented, you can subscribe in the template using async pipe:
<mat-table #table [dataSource]="domain_Data | async">
Generally, if you don't need to deal with data in the component, it's best using async pipe, as it takes care of unsubscribe automatically.
I arrived a bit late but I would like to add 2 additional information about #Aragorn answer :
Be careful when using async pipe in the template of a component with ChangeDetectionStrategy.OnPush, as it will completly force components to trigger lifecycle detection changes as often as the default Strategy.
Second important info : don't forget to unsubscribe when your component is destroyed, or you will have subscription still up if you never resolve the BehaviourSubject (here you just do 'next' operations) :
subscription: ISubscription;
this.subscription = this._domainData.subscribe((data) => this.domain_Data = data);
then in onDestroy :
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
I got list of components that I would like to replace on certain conditions:
List of navigation components:
import {navigationDefaultComponent} from '';
import {navigationSmoothComponent} from '';
import {navigationMobileComponent} from '';
navigations: [
{component: navigationDefaultComponent},
{component: navigationSmoothComponent},
{component: navigationMobileComponent},
]
I have an object that comes from API and tel's me what component I should show
const X = {
name: 'John Smith',
navigation: 'navigationDefaultComponent'
}
I have done it this way, as I can't store the component in the api. The API can not return me a component. If there is a way please do let me know.
So my goal is is to have a const that will go through navigation object and based on x.navigation string will map and return me the component.
const nav = ????
Well, this is a typical situation.
You would need to make one of your component to listen to ** route and then use dynamic component loading. https://angular.io/guide/dynamic-component-loader
You can create an array having string and Component
let mapping = [
{'name':'name1', 'component':Component1},
{'name':'name2', 'component':Component2},
{'name':'name3', 'component':Component3},
{'name':'name4', 'component':Component4},
{'name':'name5', 'component':Component5},
];
Please Note that Component1, Component2 are direct reference to the Component and not their string representations.
Create a directive, to be included in your AppComponent
import { Directive, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[app-directive]',
})
export class AppDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}
Include the directive in your template
<ng-template app-directive></ng-template>
Get the reference to your directive in ts file
#ViewChild(AppDirective) appDirective: AppDirective;
Now load the desired component after getting the response from the API
// let's assume name1 is what API returned
let component = this.mapping['name1'];
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(mapping[component);
let viewContainerRef = this.appDirective.viewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent(componentFactory);
I hope this solves your problem...
For my app, the ItemDetailComponent is where info of an item will be displayed. I have a service that retrieves all items using promise. I use ActivatedRoute to retrieve the item ID from the url path, then run the service, get all items, then find the item with the ID retrieved above, and assign it to selectedItem variable.
Here is item-detail.component.ts:
export class ItemDetailComponent implements OnInit {
private title = 'Item Details'
private selectedItem: object
constructor(
private route: ActivatedRoute,
private itemService: ItemService
) {}
ngOnInit() {
const selectedItemId = this.route.snapshot.params.itemId
return this.itemService.getAllItems()
.then((items) => {
return _.find(items, item => item.itemId === selectedItemId)
})
.then((selectedItem) => {
this.selectedItem = selectedItem
console.log('Inside promise', this.selectedItem)
})
console.log('Outside promise', this.selectedItem)
}
}
And here is item-detail.component.html template so I could display my item, just an example:
<div>
<h1>{{title}}</h1>
<div *ngIf="selectedItem">
<div><label>Item ID: </label>{{selectedItem.itemId}}</div>
</div>
</div>
The app returns nothing but the title unfortunately. I then added the two console.log() commands and found out that the one outside of the promise as well as the html template are rendered before the promise is fulfilled, and no selectedItem is available at that time. How could I force the app to execute them only after the promise is resolved in order to have the selectedItem in place for displayed?
EDIT: I added a new line in the html template to examine further:
<div>
<h1>{{title}}</h1>
<div><label>Item ID 1: </label>{{selectedItem.itemId}}</div>
<div *ngIf="selectedItem">
<div><label>Item ID 2: </label>{{selectedItem.itemId}}</div>
</div>
</div>
The app displays "Item ID 1:" label but with no actual id there. The console shows me an error saying that "Cannot read property 'itemId' of undefined", again confirming that the whole template is rendered before promise resolved and is not re-rendered after data is loaded. So weird.
You could create a Resolver for the route that fetches the desired data.
https://angular.io/api/router/Resolve
https://blog.thoughtram.io/angular/2016/10/10/resolving-route-data-in-angular-2.html
Add a boolean variable in to your class like
private dataAvailable:boolean=false;
and in the subscription to the promise,make this true when the data is available
then((selectedItem) => {
this.selectedItem = selectedItem;
this.dataAvailable=true;
console.log('Inside promise', this.selectedItem)
})
and in the template render when the data is available
<div>
<h1>{{title}}</h1>
<div *ngIf="dataAvailable">
<div><label>Item ID: </label>{{selectedItem.itemId}}</div>
</div>
</div>
It should do the trick
Update
ngOnInit() seems to be just a event handler hook - returning anything won't affect anything it seems. Hence my old answer will not work.
There are other workarounds like using *ngIf or putting it in routes etc. but I wish there was something like resolvePromise(): Promise hook that would put a condition on resolution before rendering.
This is instead of developers putting the boilerplate in every component.
Old answer
Most likely that is because you are missing return statement in the second then.
then((selectedItem) => {
this.selectedItem = selectedItem
console.log():
return selectedItem;//
}
Is it possible that the ChangeDetection is set to OnPush somewhere up the component tree?
If that is the case, the template does not automatically rerender, because nothing triggers the ChangeDetection for this component.
Look out for a Component with the setting changeDetection: ChangeDetectionStrategy.OnPush
#Component({
selector: 'example',
template: `...`,
styles: [`...`],
changeDetection: ChangeDetectionStrategy.OnPush
})
Also you already have a valid solution by using a Resolver you could check if this helps:
export class ItemDetailComponent implements OnInit {
private title = 'Item Details'
private selectedItem: object
constructor(
private route: ActivatedRoute,
private itemService: ItemService,
// the reference to the components changeDetector is needed.
private changeDetectorRef: ChangeDetectorRef
) {}
ngOnInit() {
const selectedItemId = this.route.snapshot.params.itemId
return this.itemService.getAllItems()
.then((items) => {
return _.find(items, item => item.itemId === selectedItemId)
})
.then((selectedItem) => {
this.selectedItem = selectedItem
// this triggers the changedetection and the template should be rerendered;
this.changeDetectorRef.detectChanges();
console.log('Inside promise', this.selectedItem)
});
console.log('Outside promise', this.selectedItem)
}
}
Here is a great article about Angulars ChangeDetection: https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
I have a table in which two columns are dates represented as strings. I want to filter the table based on two inputs (which are populated by two ngb-datepickers, default value is null). Using a custom pipe I am currently getting console error Cannot read property '0' of undefined as well as my table not rendering to the page, none of the buttons work, and form validation doesn't work. I import my custom pipe into declarations in app.module.ts. Minimal code is included to show entire scope, let me know if anything is confusing or to include more.
mainpage.component.html:
<div>
<label>Start Date:</label>
<input type="text" [(ngModel)]="startDateValue">
</div>
<label>End Date:</label>
<input type="text" [(ngModel)]="endDateValue">
</div>
//'let idx=index' and 'let even=even' are used to change color of the rows but I took out that code. The 'onClick' function just takes the row and uses an EventEmitter to output it.
<tr *ngFor="let dPoint of theData | searchDates:startDateValue:endDateValue; let idx=index; let even=even;" (click)="onClick(dPoint, idx)">
<td>{{dPoint.tDataPoint}}</td>
<td>{{dPoint.tICCP}}</td>
<td>{{dPoint.tStartDate}}</td>
<td>{{dPoint.tEndDate}}</td>
</tr>
mainpage.component.ts:
#Component({
selector: 'main-page',
styleUrls: ['../app.component.css'],
templateUrl: 'mainpage.component.html',
providers: [DataTableService, DatePipe]
})
export class MainPageComponent implements OnInit {
secondForm : FormGroup;
theData:DataTable[] = [];
constructor(fb: FormBuilder, private datePipe: DatePipe, private dataService: DataTableService, private cdRef:ChangeDetectorRef){
this.secondForm = fb.group({
'startDate' : [null, Validators.required],
'endDate' : [null, Validators.required]
}, {validator: this.endDateAfterOrEqualValidator})
}
getTable(): void {
this.dataService.getTable().then(theData => this.theData = theData);
this.cdRef.detectChanges();
}
}
search-pipe.ts:
import { Pipe, PipeTransform } from "#angular/core";
#Pipe({
name: "searchDates"
})
//if I comment out the code inside the transform function, I get no console errors, all functionality previously mentioned that stopped working now does again, but my entire table still does not render
export class SearchPipe implements PipeTransform {
transform(value, args?){
let firstDate = new Date(args[0]);
let secondDate = new Date(args[1]);
//let [minDate, maxDate] = args;
return value.filter(row => {
return row.tStartDate >= firstDate && row.tEndDate <= secondDate;
});
}
}
I believe my issues are improper syntax/functionality in my transform function. I've seen similar issues like this but I can't seem to format it for my needs and am just too unfamiliar with Angular2, especially pipes.
arg is not an array , is just the first thing after :
export class SearchPipe implements PipeTransform {
transform(value, firstDate , secondDate , arg3 , arg4 ){
return value.filter(row => {
return row.tStartDate >= firstDate && row.tEndDate <= secondDate;
});
}
}
I hope this can help in your example as it was solution for my problem.
Try to reuse async buikd-in pipe to notify Angular to proceed form building only after promise resolving.
Your example may looks like:
<tr *ngFor="let dPoint of theData | async | searchDates:startDateValue:endDateValue; let idx=index; let even=even;" (click)="onClick(dPoint, idx)">
<td>{{dPoint.tDataPoint}}</td>
<td>{{dPoint.tICCP}}</td>
<td>{{dPoint.tStartDate}}</td>
<td>{{dPoint.tEndDate}}</td>
</tr>
But first you need to change your theData type - you need promise as return type
var theData: Promise<DataTable> = null;
this.theData = this.dataService.getTable();
So using this approach your repeater do not start proceessing your data until your promise will not be resolved. On this stage you will have your data and your searchDates pipe will not cause empty-data-exception.
Here is more details about this pipe https://angular.io/docs/ts/latest/api/common/index/AsyncPipe-pipe.html
I can't figure out how to bind the fields to the component so that the fields update when i change the properties in OnDataUpdate().
The field "OtherValue" has a working two way binding to the input-field and the field for "Name" displayes "test" when the component is displayed. But when i refresh the data, none of the fields are updated to display the updated data.
The first logged value of "this.name" is undefined(???), the second is correct, but the field bound to the same property does not update.
How can the component provide the initial value for the name-field, but when the data update is trigged, the name-property is suddenly undefined?
stuff.component.ts
#Component({
moduleId: __moduleName,
selector: 'stuff',
templateUrl: 'stuff.component.html'
})
export class StuffComponent {
Name: string = "test";
OtherValue: string;
constructor(private dataservice: DataSeriesService) {
dataservice.subscribe(this.OnDataUpdate);
}
OnDataUpdate(data: any) {
console.log(this.Name);
this.Name = data.name;
this.OtherValue = data.otherValue;
console.log(this.Name);
}
stuff.component.html
<table>
<tr>
<th>Name</th>
<td>{{Name}}</td>
</tr>
<tr>
<th>Other</th>
<td>{{OtherValue}}</td>
</tr>
</table>
<input [(ngModel)]="OtherValue" />
The this context is lost if you pass it like that in the subscribe() function. You can fix this in several ways:
by using bind
constructor(private dataservice: DataSeriesService) {
dataservice.subscribe(this.OnDataUpdate.bind(this));
}
by using an anonymous arrow function wrapper
constructor(private dataservice: DataSeriesService) {
dataservice.subscribe((data : any) => {
this.OnDataUpdate(data);
});
}
change the declaration of the function
OnDataUpdate = (data: any) : void => {
console.log(this.Name);
this.Name = data.name;
this.OtherValue = data.otherValue;
console.log(this.Name);
}
Passing method references this way breaks the this reference
dataservice.subscribe(this.OnDataUpdate);
use this instead:
dataservice.subscribe((value) => this.OnDataUpdate(value));
By using ()=> (arrow function) this is retained and keeps referring to the current class instance.
You are losing this context, to keep context you can use bind.
dataservice.subscribe(this.OnDataUpdate.bind(this));