I am facing a situation I don't understand. I have a parent component (app.component in the example) that gets its data from an API as an observable
This data is then passed down to the child (hello.component) using the async pipe.
That child then receives the input, but when ngOnInit runs in the child, the input is null.
I don't understand why, and I don't know how to make it so that the input is the actual returned value from the API instead. The call to detectChanges() in app.component was a desperate attempt to trigger change detection in the child but that doesn't re-run ngOnInit so it's kinda moot. I left it there because that's how the actual code I'm working with looked like.
I know this code is terrible.I didn't write it. Unfortunately, the component I'm working with is like that and I can't refactor it because, you guessed it, there are no unit tests. I'm working on cleaning it all up, but for now I have to reuse that component the way it is.
Stackblitz: https://stackblitz.com/edit/angular-ivy-rw1xte?devtoolsheight=33&file=src/app/hello.component.ts
// app.component.ts
import { ChangeDetectorRef, Component, VERSION } from "#angular/core";
import { interval, Observable, of } from "rxjs";
import { mapTo, tap } from "rxjs/operators";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
public name$: Observable<any> = of({});
constructor(private cdRef: ChangeDetectorRef) {}
public ngOnInit() {
this.name$ = interval(3000).pipe(
mapTo(() => {
first: "Eunice";
}),
tap(() => this.cdRef.detectChanges())
);
}
}
<!-- app.component.html -->
<hello [name]="name$ | async"></hello>
<p>
Start editing to see some magic happen :)
</p>
// hello.component.ts
import { Component, Input } from "#angular/core";
#Component({
selector: "hello",
template: `
<div *ngIf="name">
<h1>Hello {{ this.name.first }}!</h1>
<h1>{{ greetings }}</h1>
</div>
`,
styles: [
`
h1 {
font-family: Lato;
}
`
]
})
export class HelloComponent {
#Input() name: { first?: string }
public greetings: string = "";
public firstName: string = "";
public async ngOnInit() {
console.log('name:',this.name); // name: null
if (this.name) {
// this conditional is always false because this.name is always null
// and so this never runs.
console.log("got name");
this.firstName = this.name.first || "fallback";
this.greetings = await new Promise(resolve =>
resolve("how do you do, ${firstName}?")
);
}
}
}
this.name$ = interval(3000).pipe(
mapTo({
first: "Eunice"
}),
tap(() => this.cdRef.detectChanges())
);
Please fix it like the above. mapTo doesn't need to accept a function. It didnt make an error for you? :)
Also, the name input is null right when the hello component is mounted.
You need to check the name in ngOnChanges or you need to mount the hello component only when the name is available.
For example:
<hello *ngIf="name$ | async as name" [name]="name"></hello>
Or
public async ngOnChanges() {
console.log('name:', this.name); // name: null
if (this.name) {
...
}
}
Related
I'm making a request with fetch to the reqres api users in app.component, then i share data to his child component (hello.component) via Input. I get the correct user names in child template, I'm trying to print the users in console but i get an empty array. It's there a way to 'await' the response of another component? i guess this is an asynchronous issue. Here is the link: https://stackblitz.com/edit/angular-ivy-b3m1kp?file=src%2Fapp%2Fhello.component.ts
Thanks in advance.
app.component:
import { Component, VERSION, OnInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
public usuarios;
constructor(){
this.usuarios = [];
}
ngOnInit(){
this.getUsers();
}
getUsers(){
fetch('https://reqres.in/api/users')
.then(data => data.json())
.then(users => {
this.usuarios = users.data;
console.log(this.usuarios);
});
}
}
hello.component:
import { Component, Input, OnInit } from '#angular/core';
#Component({
selector: 'hello',
template: `<h1 *ngFor="let user of usuarios">Hello {{user.first_name}} </h1>`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent{
#Input() usuarios;
ngOnInit(){
console.log(this.usuarios);
}
}
app.component.html:
<hello [usuarios]="usuarios"></hello>
As the fetch operation is asynchronous, usuarios array would be empty upon initialization for the child. To detect the value changes move logic which will use the fetched results to ngOnChanges.
Like this:
ngOnChanges(changes: SimpleChanges) {
const { previousValue, currentValue } = changes.usuarios;
if (previousValue !== currentValue) {
console.log(this.usuarios);
}
}
Having a condition to check if the value has changed inside ngOnChanges is essential, otherwise the logic will be constantly triggered.
<hello *ngIf="usuarios.length>0" [usuarios]="usuarios"></hello>
<p>
Start editing to see some magic happen :)
</p>
I'm new to Angular 7 and I'd like to know if I'm on the right path.
I have an 'alert' component that just displays a boostrap alert box on the page at the top.
I want to be able to call this alert and display it from any component.
I'm pretty sure I need a service that I can call to pass a message and then have the alert component subscribe to the service to listen for incoming messages?
So far I can call the service and pass it a 'message' I just don't know how to subscribe/listen (I think that's the right terminology) in the alert component to listen for incoming messages to display.
ex. LoginComponent
constructor(public authService: AuthService, private router: Router, private alert: AlertService) {}
login() {
this.authService.login(this.model).subscribe(next => {
this.alert.success('Logged in successfully');
}, error => {
this.alert.failure('Log in failed');
}, () => {
// do something else
});
}
and then here is my service
ex. AlertService
import {
Injectable
} from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class AlertService {
constructor() {}
success(message: string) {
// do something here?
}
error(message: string) {
// do something here?
}
}
and then I have my AlertComponent, but not sure how I would subscribe/listen for incoming messages ti display from the AlertService.
ex. AlertComponent.ts
export class AlertComponent implements OnInit {
dismissible = true;
alerts: any[];
constructor() { }
ngOnInit() {
this.add();
}
// do something here to subscribe/listen to incoming messages from the service??
add(): void {
this.alerts.push({
type: 'info',
msg: `This alert will be closed in 5 seconds (added: ${new Date().toLocaleTimeString()})`,
timeout: 5000
});
}
}
and the html
<div *ngFor="let alert of alerts">
<alert [type]="alert.type" [dismissible]="dismissible" [dismissOnTimeout]="alert.timeout">{{ alert.msg }}</alert>
</div>
You can also read Angular Dependency Injection.
To have injectable service at disposal in some component you must put it constructor and let Angular DI to provide it: Costructor of AlertComponent shoud have:
constructor ( private/proteced alertService:AlertService) {
alertService.subsribe ((par)=> {
this.add(par);
...})
}
You have preaty a lot to learn. This is just lazy made example becouse overwrite observable every time. It's not an perfect code but shows a little bit how Observables work.
Alert Service:
import {
Injectable
} from '#angular/core';
import { Observable, of } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class AlertService {
alerts: Observable<any>
constructor() { }
success(message: any) {
this.alerts = of(message)
}
error(message: string) {
this.alerts = of(message)
}
}
Allert component where alert showns:
export class AlertComponent implements OnInit {
dismissible = true;
// just inject service
constructor(public alerts$: AlertService) { }
ngOnInit() {
}
}
Template:
<div *ngIf="alerts$ | async as alerts"> <!-- | async is an pipe it will subscribe for you. importat for observables to first be in *ngIf then in *ngFor loops-->
<ng-container *ngFor="let item of alerts">
<alert[type]="alert.type"[dismissible]="dismissible" [dismissOnTimeout]="alert.timeout"> {{ item }}</alert>
</ng-container>
</div>
Command triggering alert in any component You want:
login() {
this.authService.login(this.model).subscribe(next => {
this.alert.success({ type: 'info', timeout: '5000', msg: "Success!"});
}, error => {
this.alert.failure({ type: 'info', timeout: '5000', msg: "Success!"}); // `this function u can delete meend failure just succes refactor to 'open'`
}, () => {
// do something else
});
}
About services You need to remember to provide them in app.module.ts or any other module like providers: [AlertService] So application will know that this is an service. And you inject them eny where You wat by class constructor(). When injecting you need allways set an scope for them like 'private public or protected' Or You will end up with regular Variable in type or service class.
About Observables:
There are endless Observables and when you subscribe to them You need to unsubscribe read it abot it some where on internet. | async Pipe will do it for You if variable is an endless loop.
I have a child TestComponent component as follows:
import { Component, OnInit, Input } from '#angular/core';
import { ApiService } from '../../../api.service';
#Component({
selector: 'app-test',
templateUrl: './test.component.html'
})
export class TestComponent implements OnInit {
constructor(private apiService: ApiService) { }
testDisplayMessage = 'No data to show';
ngOnInit() {
}
getMessage(param: string) {
this.callingTest = true;
this.apiService.getTest( param ).subscribe( data => {
this.setTestDisplayMessage( data );
this.callingTest = false;
}, err => {
console.log( JSON.stringify( err ) );
this.setTestDisplayMessage( 'Failed to get data' );
this.callingTest = false;
} );
}
setTestDisplayMessage( message: string ) {
this.testDisplayMessage = message;
}
}
contents of test.component.html
<p style="padding: 10px;">{{ testDisplayMessage }}</p>
Use in parent componet :
Trigger JS Code in parent component on button click,
import { TestComponent } from './test/test.component';
....
.....
#Component({
providers: [ TestComponent ],
templateUrl: 'parent.component.html'
})
export class ParentComponent implements OnInit {
...
constructor(private testComponent: TestComponent) { }
...
// Button on parent template triggers this method
getMessage() {
this.testComponent.getMessage('Hello');
}
...
}
Html tag added in parent component,
<app-test></app-test>
When I debugged above code trigger point, call to setTestDisplayMessage() happens the field testDisplayMessage in TestComponent gets changed but UI shows the old message 'No data to show', why is the message on change does not reflect on UI template? Or this is not the way it is supposed to get used? Shall I use #Input
Update:
Based on the pointers given in the following answers as well as comment sections, I changed my component as #ViewChild so in above parent component instead of passing the child component as an argument to constructor I declared it as child component using #ViewChild, so code changes as follows,
Earlier wrong code
constructor(private testComponent: TestComponent) { }
Solution
#ViewChild(TestComponent)
testComponent: TestComponent;
I found this article useful.
Use #ViewChild()
In html file:
<app-test #childComp></app-test>
In parent component.ts file
import { Component, OnInit, ViewChild } from '#angular/core';
....
.....
#Component( {
templateUrl: 'parent.component.html'
} )
export class ParentComponent implements OnInit {
#viewChild('childComp') childComp: any;
constructor() { }
...
// Button on parent template triggers this method
getMessage() {
this.childComp.getMessage('Hello');
}
...
}
Update:
Based on the pointers given in the following answers as well as comment sections, I changed my component as #ViewChild so in above parent component instead of passing the child component as an argument to constructor I declared it as child component using #ViewChild, so code changes as follows,
Earlier wrong code
constructor(private testComponent: TestComponent) { }
Solution
#ViewChild(TestComponent)
testComponent: TestComponent;
I found this article useful.
definitely use #Input() but on set method
#Input()
set someProperty(value) {
// do some code
}
now every time you pass new value here, code will run
basically, your approach is wrong, please use Input() or Services to share data between components.
however, if you want to make ur code work, the below may work
import change detector
constructor(private cdRef: ChangeDetectorRef) {
}
note: import reference ->
import { ChangeDetectorRef } from '#angular/core';
execute detect change after the value is updated
setTestDisplayMessage( message: string ) {
this.testDisplayMessage = message;
this.cdRef.detectChanges();
}
I hope this helps
I am getting many errors at the dev tools console when adding a service into my component but the code still working but I want to get rid of from these errors
This's the service:
getPagesData(pageSlug: string): Observable<any[]> {
return this._http.get<any[]>(`${environment.apiUrl}wp/v2/pages/?slug=${pageSlug}`);
}
This is the component:
import { Component, OnInit } from '#angular/core';
import { DataService } from 'src/app/services/data.service';
#Component({
selector: 'app-membership',
templateUrl: './membership.page.html',
styleUrls: ['./membership.page.scss'],
})
export class MembershipPage implements OnInit {
public pageContent: any = {};
public content: string;
constructor(
private _data: DataService
) { }
ngOnInit() {
this._data.getPagesData('memberships')
.subscribe(
page => this.pageContent = page[0]
)
}
getContent(): string {
return this.pageContent.content.rendered.replace(/\[(.+?)\]/g, "");
}
}
What cause the errors is the getContent() method! it says that is the .rendered is an undefined property but it doses defined on the API!
I have searched on that problem and most of the solutions I found it's about using the symbol ? at HTML template but I can't use that in the component itself.
If you are calling getContent() in the HTML/template, you can most likely avoid this error by either:
Making pageContent initially null and using *ngIf to only display the content once it has asynchronously resolved:
Component:
public pageContent: any = null;
Template:
<div *ngIf="pageContent">{{getContent()}}</div>
Or you could instead RxJS operators such as map() and the async pipe:
Component:
import { Component, OnInit } from '#angular/core';
import { DataService } from 'src/app/services/data.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
#Component({
selector: 'app-membership',
templateUrl: './membership.page.html',
styleUrls: ['./membership.page.scss'],
})
export class MembershipPage implements OnInit {
public pageContent: Observable<string>;
public content: string;
constructor(private _data: DataService) { }
ngOnInit() {
this.pageContent = this._data.getPagesData('memberships')
.pipe(
map(page => page[0].content.rendered.replace(/\[(.+?)\]/g, ""))
);
}
}
Template:
<div>{{pageContent | async}}</div>
That being said, you should probably have additional checks to ensure each sub-property is available prior to accessing it, but usually this type of error is because you are attempting to access the contents before they have resolved.
Hopefully that helps!
Yes, you cannot use ? Elvis (Safe navigation) operator in the component itself because it is designed for view part only.
But you can add some check in the component too to avoid such errors like -
getContent(): string {
const dataToReturn = this.pageContent && this.pageContent.content && this.pageContent.content.rendered.replace(/\[(.+?)\]/g, "");
return dataToReturn
}
.rendered is an undefined property
Also, This error may produce you have defined pageContent = {} so on {} neither content nor rendered exist , may be that is also the reason to exist such errors.
Angular recommend to strongly typecast your data before use.
I'm just starting with Angular2, reading the official docs. However, I have not found specific details about how and when the binding happens, and things don't seem to work as I expected.
I have a simple child component
#Component({
selector: 'dummy',
template: `
<div>{{data}}</div>
`
})
export class Dummy {
#Input() data;
}
and a root component
#Component({
selector: 'main',
template: `
<h1>hello</h1>
<dummy [data]="data"></dummy>
`
})
export class MainComponent {
data: string = "initial text";
ngOnInit() {
setTimeout(this.initData, 5000);
}
initData() {
this.data = "new text";
}
}
I would expect the text shown by the child component to change after 5 seconds, however it doesn't. What am I doing wrong? Does the documentation explain when and under what conditions bound values are initialized and updated?
you're losing context of this. at the time when setTimeout callback runs, this doesn't point to the component anymore. you might want to check a bit about javascript this problem.
try:
setTimeout(()=>{
this.data = "new text";
},5000);
You forgot to import the OnInit interface and make your component implement it.
import { Component, OnInit } from '#angular/core';
And then
export class MainComponent implements OnInit { ... }