I am new to Angular and am running into an issue with the constructor on a child component being called twice, and the second time it is called it is clearing the properties set the first time.
This is the parent component:
#Component({
selector: 'parent-item',
templateUrl: '...',
providers: [ItemService]
})
export class ParentItemComponent {
public parentItemId;
public model: ParentItem;
constructor(itemService: ItemService, elm: ElementRef) {
this.parentItemId = elm.nativeElement.getAttribute('parentItemId');
itemService.getParentItem(this.parentItemId).subscribe(data => this.model = data);
}
}
And in the template the child component is referenced:
<child-items [parentItemId]="parentItemId">Loading...</<child-items>
This is the child component:
#Component({
selector: 'child-items',
templateUrl: '...',
providers: [ItemService]
})
export class ChildItemsComponent {
#Input() public parentItemId: number;
public items: Observable<ChildItem[]>;
constructor(private itemService: ItemService) {
console.log("constructor");
}
ngOnInit() {
if (this.parentItemId) {
this.items = this.itemService.getChildItems(this.parentItemId);
}
else {
console.log("Parent Id not set!");
}
}
}
And finally the child component template:
<tr *ngFor="let item of items | async">
<td>...</td>
</tr>
The child components constructor is being called twice, and the second time it is called the parentItemId is set to null and the items property is cleared. If I hardcode the parentId instead of using the input the data is being properly received and displayed in the template, but using the input value the template shows no results.
I have created a plunker which shows the same behavior here: http://embed.plnkr.co/xaJtfNgbWCUPap2RCJUA/
Your problem is that in the app.module you bootstrap both parent and child component:
bootstrap: [ ParentItemComponent, ChildItemsComponent ]
It has to be
bootstrap: [ ParentItemComponent]
child-items is not closed properly. Probably because of this error
This
<child-items [parentItemId]="parentItemId">Loading...</<child-items>
should be:
<child-items [parentItemId]="parentItemId">Loading...</child-items>
Related
I've implemented a child component to render a table based on a list provided via #Input(). The data is loaded via http, however the UI (child component) is not updated unless I wave my mouse over the screen. I've seen people post about implementing ngOnChanges() in my child, but I thought Angular was supposed to do this by default? Am I missing something? Why would the UI not update with this?
Child code looks something like this:
child.component.ts
#Component({
selector: 'child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss'],
})
export class ChildComponent implements {
#Input() data: any[] = [];
constructor() {}
}
child.component.html
<table>
<tr *ngFor="let item of data"><td>{{ item }}</td></tr>
</table>
Parent code that uses the component looks something like this:
parent.component.ts
#Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss'],
})
export class ParentComponent implements OnInit {
data: string[] = [];
constructor(private endpointService: EndpointService) {}
ngOnInit() {
// response is a string array like: ['hello', 'world']
this.endpointService.loadData().subscribe((response) => {
this.data = response;
});
}
}
parent.component.html
<child [data]="data"></child>
============================= EDIT ==================================
I verified that it only fails to load when updating inside of the subscribe callback (if I set a static array, it loads just fine).
So it looks like I'm able to resolve this by running changeDetectorRef.detectChanges() in the parent component, but this feels hackish like I shouldn't have to do this. Is this a good way to resolve this? Or does this indicate something wrong with my implementation?
parent.component.ts
#Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss'],
})
export class ParentComponent implements OnInit {
data: string[] = [];
constructor(private endpointService: EndpointService,
private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit() {
// response is a string array like: ['hello', 'world']
this.endpointService.loadData().subscribe((response) => {
this.data = response;
this.changeDetectorRef.detectChanges();
});
}
}
You can also try to force change detection by forcing the value reference update via, for example, the spread operator:
this.endpointService.loadData().subscribe((response) => {
this.data = [...response];
});
hummm well .. when the component is rendered as first time it will show with the empty array becouse the api call stills happening and needs the onchanges method in child component in order to listen the complete api call and the list will re render
Seems that you have some other errors in template expressions which force the whole template to fail. Here's a stackblitz I've created and everything works: https://stackblitz.com/edit/angular-ivy-w2ptbb?file=src%2Fapp%2Fhello.component.ts
Do you have maybe some errors in console?
I replaced the service with a static string array and it worked well. I think there is problem with the observable subscription.
child.component.ts
import { Component, OnInit,Input } from '#angular/core';
#Component({
selector: 'child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
#Input() data: any[] = [];
constructor() { }
ngOnInit() {
}
}
parent.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.css'],
})
export class ParentComponent implements OnInit {
data: string[] = [];
constructor() {}
ngOnInit() {
this.data = ['hello', 'world','aaa','ddd'];
}
}
I'd like to be able to access the SearchResults component, (when it has been clicked), in the root component (AppComponent) as I'm looking to set different properties on the SearchResults component such as;
I'd like to set an attribute on the SearchResults component so that it shows the "close" text
Also, I'd to set the click event on the SearchResults to redirect elsewhere or actually enable it as a multi-select so that it stays selected until a user proceeds to the next step for example.
I'm trying to make the SearchResults and SearchResult components as re-usable as possible so we're able to state in the parent component which would include the <app-searchresults> selector what action we'd like our SearchResults components to actually be when they are clicked.
The only way I can really see doing this is using EventEmitter to pass the event up once through the SearchResult component then onto the parent component and then a Service to hold selected values but I'm still stuck around enabling the SearchResults component as either a component which redirects when clicked or stays selected? Is this actually possible or do I need to create a different SearchResults component for each different state I'd like?!
export class AppComponent {
#ViewChildren(SearchresultComponent) components: QueryList<SearchresultComponent>;
name = 'Angular';
ngAfterViewInit() {
this.components.changes.subscribe((r) => { console.log(r) });
}
}
SearchResults.ts
#Component({
selector: 'app-searchresults',
templateUrl: './searchresults.component.html',
styleUrls: ['./searchresults.component.css']
})
export class SearchresultsComponent implements OnInit {
#ViewChildren(SearchresultComponent) components: QueryList<SearchresultComponent>;
constructor() { }
ngOnInit() {
}
}
SearchResults.html
<h1>Search Results<h1>
<app-searchresult result ="first"></app-searchresult>
<app-searchresult result ="second"></app-searchresult>
<app-searchresult result ="third"></app-searchresult>
SearchResult.ts
#Component({
selector: 'app-searchresult',
templateUrl: './searchresult.component.html',
styleUrls: ['./searchresult.component.css']
})
export class SearchresultComponent implements OnInit {
#Input()
result: string;
isSelected: boolean;
constructor() { }
ngOnInit() {
}
toggleClickedState(){
if(!this.isSelected){
this.isSelected = !this.isSelected;
}
}
}
SearchResult.html
<div>
<p (click)=toggleClickedState() [ngClass]="isSelected? 'selected' : '' "> Search Result : {{result}}</p>
<p *ngIf="isSelected" class="cross" (click)="isSelected = false;">close</p>
<div>
I've included a link to structure of an app that references the above;
https://stackblitz.com/edit/angular-cjhovx
In my app-root component I have router-outlet in container with some styles.
I have route:
{
path: 'some-path',
component: ChildContainer,
data: { variable:'variable' },
}
And I can to get variable in ChildContainer, but I need it in AppRoot. So, from documentation I can get it from child, but if I do this in AppRoot constructor:
const state: RouterState = router.routerState;
const root: ActivatedRoute = state.root;
const child = root.firstChild;
and console.log(root, child) - child is null, and root contains correct child (invoke property getter).
So, how can I get variable in AppRoot?
You may tap into activate event to get reference of instantiated component inside the router outlet.
Check This SO question
#Component({
selector: 'my-app',
template: `<h3 class="title">Basic Angular 2</h3>
<router-outlet (activate)="onActivate($event)" ></router-outlet>
`
})
export class AppComponent {
constructor(){}
onActivate(componentRef){
componentRef.sayhello();
}
}
#Component({
selector: 'my-app',
template: `<h3 class="title">Dashboard</h3>
`
})
export class DashboardComponent {
constructor(){}
sayhello(){
console.log('hello!!');
}
}
Here is the Plunker!!
Update
expose ActivatedRoute as a public property and once you have the routed component reference, subscribe to data,
onActivate(componentRef){
componentRef.route.data.subsribe(data => {
console.log(data);
});
}
In my application I have Home as root component and another generic component named as list which i'm rendering inside Home.
I want to pass data as property to my list component which is coming from XMLHttpRequest.
home.ts
import {Component} from 'angular2/core';
import {DashboardService} from '../../services/dashboard';
import {List} from '../contact/list';
#Component({
selector: 'home',
template:
`
<h3>Home</h3>
<List type="{{type}}"></List>
`
providers: [DashboardService],
directives: [List],
})
export class Home {
private _type: any;
constructor(private _dashboardService: DashboardService) {
this._dashboardService.typeToDisplay()
.subscribe((type) => {
this._type = type;
});
}
}
List.ts
#Component({
selector: 'List',
properties: ['type'],
template: `
<h2>list</h3>
`,
providers: [DashboardService]
})
export class List {
private type: any;
constructor(#Attribute('type') type:string) {
this.type = type;
console.log(type);
}
}
I'm getting string data from typeToDisplay() method its an Http request & assigning to type variable. but when I passed as property to list component I'm getting null in List constructor.
I tried too but i'm getting "type" string same way.
Hope my question is Clear.
This syntax
<List type="{{type}}"></List>
is setting a property not an attribute.
To set an attribute use either
<List attr.type="{{type}}"></List>
or
<List [attr.type]="type"></List>
If you just want to have the value available in List use
#Input() type: any;
instead of the attribute injection.
This way the value is not availabe yet inside the constructor, only in ngOnInit() or later.
Background
Suppose I have some parent component, call it MatchList, that presents a list of Hero objects, among other things. Each Hero object has properties that are shown in some table. Now suppose I also have a button for each Hero that updates the route, loads a new view, and shows more details.
Before
http://heroic.com/match-list
After
http://heroic.com/hero-84
Problem
My problem essential is this: I want to call the router's navigate() method from a button in my MatchList template, but I receive the following error when I attempt to do so:
EXCEPTION: Error during evaluation of "click"BrowserDomAdapter.logError # ...
angular2.dev.js:21835 ORIGINAL EXCEPTION: TypeError: l_context.setPath is not a function...
angular2.dev.js:21835 TypeError: l_context.setPath is not a function at ...
In other words It looks like I cannot reference the parent component's router methods in the child template.
So, what is the correct and best way in Angular 2 for a child component access the methods of the parent component ( or 'context')?
I'd prefer if the solution was something cleaner than
class parent {
child: Child;
constructor(...) {
...
this.child.parent = this;
}
}
Sample Code
EDIT
I changed my template button to
(^click)="setPath(match.match_id)"
I am not longer receiving an error message, but nothing happens - I don't even get a console log confirming the click.
Snippets of what I have so far.
//Parent
#Component({
selector: 'dota-app',
directives: [Home, MatchesView, ROUTER_DIRECTIVES],
templateUrl: 'AppView.html'
})
#RouteConfig([
{ path: '/', component: Home, as: 'Home' },
{ path: '/matches', component: MatchesView, as: 'Matches' },
{ path: '/match-details', component: MatchDetailsView, as: 'MatchDetails'}
])
export class RootDotaComponent {
router: Router;
constructor(router: Router) {
this.router = router;
}
public setPath(linkParams: any[]|string): void {
if (typeof linkParams === "string")
linkParams = [linkParams];
this.router.navigate(<any[]>linkParams);
}
}
}
//Child
#Component({
selector: 'matches-view',
providers: [DotaRestDao],
})
#View({
templateUrl: './components/MatchesView/MatchesView.html',
directives: [CORE_DIRECTIVES]
})
export class MatchesView {
public result;
private dataService: DotaRestDao;
constructor(dataService: DotaRestDao) {
this.result = { matches: [] };
this.dataService = dataService;
this.dataService.getData({
baseUrl: DotaRestDao.MATCH_HISTORY_BASE
}).subscribe(
res => this.result = res.result,
err => console.log("something wrongable", err),
() => console.log('completed')
);
}
}
//Template
<table class="table">
...
<button (click)="setPath(match.match_id)">Match Detail Route</button>
</table>
In the context of this question, namely calling a parent router, the answer, it turns out, is trivial. See this plunker for details.
The main takeaway is that giving a router to a child component a la
class ChildComponent {
constructor(router: Router) {
...
}
}
does not create a new router, it merely extends the existing router of the parent component. Thus, the need to a reference to the parent object is obviated. Just call the methods of the childRouter and everything works as expected.