Angular's `cdRef.detectChanges` doesn't prevent checked exception? - javascript

I have a situation where I have an inner component :
#Component({
selector: 'hello',
template: `<h1>Name = {{name}}!</h1> `
})
export class HelloComponent {
#Input() name: string;
#Output() ev: EventEmitter<string> = new EventEmitter<string>();
constructor(private cdRef: ChangeDetectorRef) { }
ngOnInit() {
this.ev.emit("new name");
this.cdRef.detectChanges();
}
}
Where in the parent component :
<hello name="{{ name }}" (ev)="changeName($event)"></hello>
#Component({
selector: 'my-app',
templateUrl: './app.component.html'
})
export class AppComponent {
name = 'Angular 5';
changeName(e) {
this.name = e;
console.log(e)
}
}
So basically , when the inner component loads , it emits an event which in turn received by parent and re-set the value of its inner component.
But this code ( and I do know why) - yields an exception :
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has
changed after it was checked. Previous value: 'Angular 5'. Current
value: 'new name'.
But according to other answers in SO:
This line : this.cdRef.detectChanges(); should have caused a new detection and prevent the exception.
I already know that setTimeout()... does solve it in a hacky way.
Question:
Why doesn't detectChanges prevent the exception ?
Stackblitz

Related

Showing and Hiding a child component from parent for specific seconds

I have notification component, which is a child component and being shown in parent component after a button click. After a custom seconds of time it will be hidden. So far I have come up with this code (Stackblits).
MyCode
Parent component:
<button (click)="handleClick()">Initiate Message</button>
<app-notification [message]="message" [duration]="3000" [show]="show"></app-notification>
<app-notification [message]="message" [duration]="6000" [show]="show"></app-notification>
export class AppComponent {
message:string;
show:boolean;
handleClick(){
this.message = 'Good Morning';
this.show=true;
}
}
Child comp:
<div class="container" *ngIf="show">
{{ message }}
</div>
#Input() message: string;
#Input() duration: number;
#Input() show = false;
constructor() {}
ngOnChanges(changes: SimpleChanges) {
console.log(this.show)
setTimeout(()=>{
this.show = false;
console.log(3)
}, this.duration)
}
This works perfectly, but after 3 seconds as it is in the example, If I click the button, the child component is not being showed. What am I doing wrong. I am new to Angular, please help me.
Also I want to handle the same for both instances of the child.
You have to move the code for the timeout out of ngOnChanges.
In your component setup a setter would do the trick.
#Component({
selector: 'app-notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.css'],
})
export class NotificationComponent {
#Input() message: string;
#Input() duration: number;
#Input() set show(value: boolean) {
this._show = value;
// Or find another failsafe if the values are not set as expected
if (value && this.duration) {
this._show = true;
setTimeout(() => {
this._show = false;
}, this.duration);
}
}
get show(): boolean {
return this._show;
}
private _show = false;
}
As well as an ugly hack to avoid Angular not realizing that this value needs to be reset:
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
message: string;
show: boolean;
duration: number;
handleClick() {
this.message = 'Good Morning';
this.show = true;
setTimeout(() => (this.show = false), 1);
}
}
Using RxJS Observables would solve this problem in a nice way, but it would also be more complicated if you are new to Angular.

Angular - ngOnChanges() not getting executed on property change

#Component {
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: 'app.component.css'
}
export class App implements OnChanges, OnInit {
prop = '';
ngOnInit() {
this.prop = 'something';
}
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
}
}
I'm expecting to get the console log from ngOnChanges() when I change the prop property via ngOnInit() method. But it's not working. Any help?
The lifeCycle hook OnChanges is only triggered for an #Input() property when the value is changed in the parent component.
Edit regarding the comments :
In order to listen to a prop changes inside a component, you can change it to a Subject :
prop: Subject<string> = new Subject();
ngOnInit() {
this.prop.next('something');
this.prop.subscribe(value => console.log(value));
}
onClick(value: string) {
this.prop.next(value);
}

Change Detection with property reference

I cannot seem to figure out how to get my template to update. When I change a boolean property which is referenced in another array property, I would expect that my changes would change within the template. However, I am not seeing the changes.
When the app loads everything is loaded in its initial state (false: Login is visible and Logout is hidden), but when the isLogged boolean changes the navigation doesn't update to hide/show the correct item.
I think the issue is how Angular handles change detection on objects/arrays, but I am not sure.
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
public isLogged: boolean = false;
public navigation: INav = {
links: [
{
text: 'Login'
hidden: !this.isLogged
},
{
text: 'Logout'
hidden: this.isLogged
}
]
}
public ngOnInit(): void {
// Triggered whenever the login state changes
this.authService.loginState().subscribe(state => {
this.isLogged = state;
});
}
}
<third-party-nav [model]="navigation"></third-party-nav>
This problem is not related to angular's change detection.
The problem in your code is that you set the navigation only once when the component is created (an therefore the variable isLogged is false). Instead you should update it when the loginState changes. You can do this by using observables/rxjs:
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
public navigation: Observable<INav>;
public ngOnInit(): void {
// When the login state changes the navigation object also changes
this.navigation = this.authService.loginState().map(s => ({
links: [
{
text: 'Login',
hidden: !s
},
{
text: 'Logout',
hidden: s
}
]
}));
}
}
Then in your html you can use the async pipe to handle the observable
<third-party-nav [model]="navigation | async"></third-party-nav>

Access text (not instance of another component) with ContentChild

How can I access a string of text given within the tags of a component
<my-custom-component>THIS TEXT</my-custom-component>
Within a template, I can use ng-content, or if it is an instance of some other class I can access it within the component definition like demonstrated in these examples. However I am interested in detecting if there is a string of text there or not, which I believe would make providedText undefined. However, I am always getting undefined.
#ContentChild(Element, { static: true }) providedText: Text | undefined;
I have tried Text as the first element passed to #ContentChild. Passing any will not work (I don't know why).
StackBlitz
I am interested mostly in finding if there is a string or undefined, but am also curious why ContentChild(Text... isn't working.
Edit:
I have added a potential solution, but it seems pretty imperfect, so I hope something better comes along.
Edit 2:
I now understand that #ContentChild is not a mechanism for selecting whatever native HTML I want without wiring it up to Angular’s dependency graph with a ref, directive, etc.
I am still curious if my proposed solution below is a bad idea for any reason.
My solution for now (since I wish to capture all transcluded content) is to wrap ng-content in a containing element, then get its innerText.
#Component({
selector: "app-parent",
template: `
<span #transcludedContainerRef>
<ng-content></ng-content>
</span>
`
})
export class ParentComponent implements AfterViewInit {
#ViewChild("transcludedContainerRef", { static: false })
transcludedContainerRef: ElementRef | undefined;
buttonText: string;
ngAfterViewInit() {
const isButtonTextPresent = this.transcludedContainerRef.nativeElement
.innerText;
if (isButtonTextPresent) {
console.log(isButtonTextPresent); // successfully logs content
}else {
console.log('No text set');
}
}
}
It does feel hacky, but it works. I am holding out for something better.
it's difficult if I don't know about your <my-custom-component>
In general if your custom component it's only
<ng-content></ng-content>
You can inject in constructor the elementRef
constructor(public el:ElementRef){}
From a parent
<hello >
Start editing to see some magic happen :)
</hello>
You can use
#ViewChild(HelloComponent,{static:false}) helloComponent:HelloComponent
click()
{
console.log(this.helloComponent.el.nativeElement.innerHTML)
}
If your component has any variable -or ViewContent-, you can access this variables in a similar way
So the other way to read the inner text from the component is that child component emit the value whatever it get's as input from other component. See below:
hello.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '#angular/core';
#Component({
selector: 'hello',
template: `<h1>Hello {{name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent implements OnInit {
#Input() name: string;
#Output() innerText: EventEmitter<string> = new EventEmitter();
ngOnInit() {
this.innerText.emit(this.name);
}
}
app.component.ts
import { Component, ContentChild, AfterContentInit, OnInit } from "#angular/core";
#Component({
selector: "app-parent",
template: "content from <code>app-parent</code>"
})
export class ParentComponent implements AfterContentInit {
#ContentChild(Element, { static: true }) providedText: Text | undefined;
ngAfterContentInit() {
console.log("ngAfterContentInit Content text: ", this.providedText);
}
}
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
name = "Angular";
_innerText: string;
ngAfterContentInit() {}
get childContent(): string {
return this._innerText;
}
set childContent(text) {
this._innerText = text;
}
innerTextFn(innertext: string) {
this.childContent = innertext;
console.log('Event: ', innertext);
}
}
app.component.html
<hello name="{{ name }}" (innerText)="innerTextFn($event)"></hello>
<app-parent>This is the content text</app-parent>
Here is stackblitz url to check: https://stackblitz.com/edit/angular-bacizp
I hope this may helpful for you and if yes then accept this as correct answer.

Dynamic nested reactive form: ExpressionChangedAfterItHasBeenCheckedError

My reactive form is three component levels deep. The parent component creates a new form without any fields and passes it down to child components.
At first the outer form is valid. Later on a child component adds new form elements with validators (that fail) making the outer form invalid.
I am getting an ExpressionChangedAfterItHasBeenCheckedError error in the console. I want to fix that error.
Somehow this only happens when I add the third level of nesting. The same approach seemed to work for two levels of nesting.
Plunker: https://plnkr.co/edit/GymI5CqSACFEvhhz55l1?p=preview
Parent component
#Component({
selector: 'app-root',
template: `
myForm.valid: <b>{{myForm.valid}}</b>
<form>
<app-subform [myForm]="myForm"></app-subform>
</form>
`
})
export class AppComponent implements OnInit {
...
ngOnInit() {
this.myForm = this.formBuilder.group({});
}
}
Sub component
#Component({
selector: 'app-subform',
template: `
<app-address-form *ngFor="let addressData of addressesData;"
[addressesForm]="addressesForm">
</app-address-form>
`
})
export class SubformComponent implements OnInit {
...
addressesData = [...];
constructor() { }
ngOnInit() {
this.addressesForm = new FormArray([]);
this.myForm.addControl('addresses', this.addressesForm);
}
Child component
#Component({
selector: 'app-address-form',
template: `
<input [formControl]="addressForm.controls.addressLine1">
<input [formControl]="addressForm.controls.city">
`
})
export class AddressFormComponent implements OnInit {
...
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ]
],
city: [
this.addressData.city
]
});
this.addressesForm.push(this.addressForm);
}
}
To understand the problem you need to read Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error article.
For your particular case the problem is that you're creating a form in the AppComponent and use a {{myForm.valid}} interpolation in the DOM. It means that Angular will run create and run updateRenderer function for the AppComponent that updates DOM. Then you use the ngOnInit lifecycle hook of subcomponent to add subgroup with control to this form:
export class AddressFormComponent implements OnInit {
#Input() addressesForm;
#Input() addressData;
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ] <-----------
]
this.addressesForm.push(this.addressForm); <--------
The control becomes invalid because you don't supply initial value and you specify a required validator. Hence the entire form becomes invalid and the expression {{myForm.valid}} evaluates to false. But when Angular ran change detection for the AppComponent it evaluated to true. And that's what the error says.
One possible fix could be to mark the form as invalid in the start since you're planning to add required validator, but it seems Angular doesn't provide such method. Your best choice is probably to add controls asynchronously. In fact, this is what Angular does itself in the sources:
const resolvedPromise = Promise.resolve(null);
export class NgForm extends ControlContainer implements Form {
...
addControl(dir: NgModel): void {
// adds controls asynchronously using Promise
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
dir._control = <FormControl>container.registerControl(dir.name, dir.control);
setUpControl(dir.control, dir);
dir.control.updateValueAndValidity({emitEvent: false});
});
}
So for you case it will be:
const resolvedPromise = Promise.resolve(null);
#Component({
...
export class AddressFormComponent implements OnInit {
#Input() addressesForm;
#Input() addressData;
addressForm;
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ]
],
city: [
this.addressData.city
]
});
resolvedPromise.then(() => {
this.addressesForm.push(this.addressForm); <-------
})
}
}
Or use some variable in the AppComponent to hold form state and use it in the template:
{{formIsValid}}
export class AppComponent implements OnInit {
myForm: FormGroup;
formIsValid = false;
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.myForm = this.formBuilder.group({});
this.myForm.statusChanges((status)=>{
formIsValid = status;
})
}
}
import {ChangeDetectorRef} from '#angular/core';
....
export class SomeComponent {
form: FormGroup;
constructor(private fb: FormBuilder,
private ref: ChangeDetectorRef) {
this.form = this.fb.group({
myArray: this.fb.array([])
});
}
get myArray(): FormArray {
return this.form.controls.myArray as FormArray;
}
addGroup(): void {
const newGroup = this.fb.group({
prop1: [''],
prop2: ['']
});
this.myArray.push(newGroup);
this.ref.detectChanges();
}
}
I had the same scenario and same issue in Angular 9 and above solution works fine. I tweaked it a little bit: synchroinously adding the control without validators and adding the required validators asynchronously... Because I needed the controls immediately otherwise I got an error cannot find formControl ....
This is my solution based on the accepted answer above:
const resolvedPromise = Promise.resolve(null);
#Component({
selector: 'password-input',
templateUrl: './password-input.component.html',
styleUrls: ['./password-input.component.css']
})
export class PasswordInputComponent implements OnInit {
#Input() parentFormGroup : FormGroup;
#Input() controlName : string = 'password';
#Input() placeholder : string = 'Password';
#Input() label : string = null;
ngOnInit(): void {
this.parentFormGroup.addControl(this.controlName, new FormControl(null));
resolvedPromise.then( () => {
this.parentFormGroup.get(this.controlName).setValidators(Validators.required)
this.parentFormGroup.get(this.controlName).updateValueAndValidity();
});
}
Following the solution of Max Koretskyi, we can use async/await pattern on ngOnInit.
Here is an example:
#Component({
selector: 'my-sub-component',
templateUrl: 'my-sub-component.component.html',
})
export class MySubComponent implements OnInit {
#Input()
form: FormGroup;
async ngOnInit() {
// Avoid ExpressionChangedAfterItHasBeenCheckedError
await Promise.resolve();
this.form.removeControl('config');
this.form.addControl('config', new FormControl("", [Validator.required]));
}
}

Categories