Error when using property that relies on ViewChildren - javascript

I have created a custom component that contains a form <address></address>. And I have a parent component that has an array of these:
#ViewChildren(AddressComponent) addressComponents: QueryList<AddressComponent>;
So the parent can contain a collection of these elements and the user can add and remove them based on the number of addresses they will be entering.
The parent also has a button to proceed after the user has entered all desired addresses. However, the <address> component must be filled out correctly so I have a public getter on the <address> component:
get valid(): boolen {
return this._form.valid;
}
Back to the button on the parent. It needs to be disabled if any of the <address> components are invalid. So I wrote the following:
get allValid() {
return this.addressComponents && this.addressComponents.toArray().every(component => component.valid);
}
And in the parent template:
<button [disabled]="!allValid" (click)="nextPage()">Proceed</button>
But angular doesn't like this because addressComponents are not defined in the parent until ngAfterViewInit lifecycle event. And since it immediately runs ngOnViewInit() I get two different values for the expression check which causes the error. (At least that's what I think is going on).
How do I use a property in my template that depends on ngAfterViewInit? Or what is the best way to inform my parent that all of its children are valid?
The Error Message:
Expression has changed after it was checked. Previous value: 'false'.
Current value: 'true'
Update:
So I console.loged the return value of allValid and noticed the first time it was undefined. This was to be expected as this.addressComponents are undefined until ngAfterInit. The next log it was true and this was surprising as I didn't have any <address> components on the page (yet). I am using mock data (all valid, though) in ngOnInit of the parent component to create a component. I did learn that ([].every... returns true on an empty array). So the third call to the console.log was returning false. Again, I am a little surprised because all my data is valid. On the 4th log it was returning true which is what I expected. So I'm assuming this final value being returned is what Angular disliked.
Anyway, I was able to sort of solve this. I don't know if I'm actually fixing the problem or just suppressing the error. I do not like this solution so I am going to keep the question open for a better solution.
get allValid() {
return this.addressComponents && this.addressComponents.length > 0 && this.addressComponents().toArray().every(component => component.valid);
}

So what I think is happening:
The first wave of change detection gets you a false for your function, then your parent component finds out this information after the view is instantiated (then returns true). In "dev" mode, Angular runs change detection twice to ensure that changes don't happen AFTER change detection (as change detection should detect all of the changes, of course!)
According to the answer found here:
Angular2 - Expression has changed after it was checked - Binding to div width with resize events
using AfterViewInit can cause these issues, as it may run after the change detection has completed.
Wrapping your assignment in a timeout will fix this, as it will wait a tick before setting the value.
ngAfterViewInit(){
setTimeout(_ => this.allValid = this.addressComponents && this.addressComponents.toArray().every(component => component.valid));
}
Due to these reasons, I would not use a getter on a template variable like that, as the view initializing may change the value after change detection has finished.

If I understand, you'll probably need to come up with a way in the parent to track how many instances of the child there are, and the child will need an EventEmitter that informs the parent when it's valid or becomes invalid.
So in the parent you could use an array to track how many address instances there are..
Parent Component
addressForms: Array<any> = [{ valid: false }];
addAddressForm() {
this.addressForms.push({ valid: false ));
}
checkValid() {
// somehow loop through the addressForms, make sure all valid
let allValid: boolean = false;
for (var i = this.addressForms.length - 1; i >= 0; i--) {
if (this.addressForms[i].value && allValid === false)
allValid = true;
}
return allValid;
}
Parent Template
<div *ngFor="let form of addressForms; let i = index">
<address (valid)="form.valid = true" (invalid)="form.valid = false"></address>
</div>
<button [disabled]="checkValid()">Next</button>
Address Component
#Output() valid: EventEmitter<any> = new EventEmitter();
#Output() invalid: EventEmitter<any> = new EventEmitter();
isValid: boolean = false;
check() {
// call this check on field blurs and stuff
if ("it's valid now" && !this.isValid) {
this.isValid = true;
this.valid.emit(null);
}
if ("it's not valid anymore" && this.isValid) {
this.isValid = false;
this.invalid.emit(null);
}
}
That's the basic idea anyway, with some holes that are obvious enough to fill in. Hope that has some relevancy with what you're doing and I understood the question to begin with. Good luck!

I faced the same issue when have been using that realy handy pattern :( Only short way I found atm to solve it is the next kind of a hack:
#ViewChild(DetailsFormComponent) detailsForm: DetailsFormComponent;
isInitialized = false;
get isContinueBtnEnabled(): boolean {
return this.isInitialized && this.detailsForm?.isValid();
}
and
ngAfterViewInit() {
setTimeout(() => { // required
this.isInitialized = true;
});
}

Related

How do I wait for users to click on certain buttons in component, then return a value from that in a function depending on which button was pressed?

I have an overlay component that appears when a user clicks on certain things in my page, and in this overlay it gives a warning and 2 buttons, one for yes and the other for no. What I want is to create a function that'll serve this component, and then it will wait for the user to respond, and subsequently return true or false based on what button was pressed. This boolean result can then be used to further progress to other code.
This is what I have tried already. It uses promises rather than rxjs observables.
A component will call this function to bring the overlay from the service, eg this.service.promptUser().then(res => if (res === true) { doSomething() }).
In the service:
didContinue: boolean = null;
async promptUser() {
this.showOverlay.next(true) //BehaviourSubject when true brings the popup
await waitForUser();
const decision = this.didContinue;
this.closeOverlay(); //sets didContinue back to null
return decision
}
The didContinue is a property inside of the service to indicate whether they have clicked yes or no using a boolean. Otherwise it will remain null. The click events from the overlay component will set the property didContinue to true or false.
The waitForUser function to wait for the user's input:
async waitForUser() {
while (this.didContinue === null) {setTimeout(() => {}, 50};
}
Currently it'll get stuck at the waitForUser() function but the popup will have not rendered at that stage, so the user can't input anything, the didContinue property will never change, and the application will freeze.
Please do send it forward if you know of an existing solution, I miss a lot of things with my google-foo. I am currently still new to Angular.
Create a service that will handle the result
inject service in your component where you want to use it like this
constructor(public popService: popService) { } // create functions accordingly in service
Call service method with the help of constructor on click() event like this
<button (click)="popService.success()">Done
<button (click)="popService.cancel()">Done
i hope this will help, let me know if you need further help :-)

Angular ngOnChanges in child doesn't trigger after subscribe manipulates data

I have a problem with ngOnChanges not being fired for a child component after data is being manipulated in parent via subscribe method.
Basically my main component looks like this:
public imageGroups: IImageGroup[];
public status$: Observable<ModelStatusesModel>;
public ngOnChanges(changes: SimpleChanges): void {
if (Boolean(this.treeData)) {
this.imageGroups = this.treeDataService.convertImageGroups(this.treeData);
this.status$ = this.updateStatus();
this.status$.subscribe((status: ModelStatusesModel) => {
this.treeDataService.updateStatus(this.imageGroups, status); // this function makes changes to this.imageGroups
console.log('subscribe happened');
});
}
}
HTML:
...
<ul class="treeview-nodes-wrapper">
<treeview-branch *ngFor="let group of (imageGroups)"
[group]="group"></treeview-branch>
</ul>
...
The branch has also ngOnChnages:
public ngOnChanges(): void {
this._currentNodeDisabled = this.group.isDisabled;
console.log(this.group); //returns isDisables as true
console.log(this.group.isDisabled); //returns false
console.log(this._currentNodeDisabled); //returns false
}
When I run my application this is the result in the console:
{ ... isDisabled: true ...},
false
false
subscribe happened
I was also trying to surround the call inside my subscription in a ngZone.run but without any success. Do you have any idea how to tell angular that ngOnChanges in the child triggered?
EDIT: What works for me is creating a property in the parent component (public change = false;) then toggling this property inside my subscribe method and giving it as an input to my children elements. That gets picked up as a change. Even though this solves my problem, it looks more like a very hacky way of writing the code.
This is a result of ngOnChanges being triggered when an input value changes, and with objects that are passed by reference the modification of their properties does not explicitly change the thing being passed down. The solution is often times to do a clone, ...spread operator, or reassign to the property that is being passed down in the parent's [input].
Your solution of having an additional input that changes to a new value to trigger ngOnChanges works as well, it's a bit of a workaround but that's web dev. just remember if you set the property to true then true again it won't trigger, so a count variable may work better (it's kinda hacky).
Doing a clone via JSON.parse solved my problem in a cleaner way than toggling a variable :
...
this.status$.subscribe((status: ModelStatusesModel) => {
this.treeDataService.updateStatus(this.imageGroups, status);
triggerChange();
});
...
private triggerChange() {
this.imageGroups = JSON.parse(JSON.stringify(this.imageGroups));
}

My angular 7 web component #Input<boolean> doesn't work when I bind a false value

I have setup an application which exports web components from angular 7 using angular custom-elements package.
Everything works fine. I am able to bind anything, arrays, objects, strings from javascript using the element instance:
const table = document.createElement('my-table-element');
table.someInputProp = 'Works Great';
table.otherInput = ['my', 'working', 'array'];
The problem comes when I try to bind to a literal false value:
table.myBooleanInput = false
This doesnt change anything on the component #Input myBooleanInput = true
The value is still true for ever. No matter how many times it changes to false.
I'am able to bind it as a truthy value which it renders just fine. The problem is only when using literal false.
Here is a working reproduction of this issue:
https://stackblitz.com/edit/angular-elements-official-example-q4ueqr
Thanks in advance.
PS: If I use the web components from within another angular app, the binding works fine.
I'm actually having this same issue. Was debugging it and there seems to be some code the #angular/elements (I'm using version 7.2.15) package which skips over setting properties on initialize when they evaluate to false.
/** Set any stored initial inputs on the component's properties. */
ComponentNgElementStrategy.prototype.initializeInputs = function () {
var _this = this;
this.componentFactory.inputs.forEach(function (_a) {
var propName = _a.propName;
var initialValue = _this.initialInputValues.get(propName);
if (initialValue) {
_this.setInputValue(propName, initialValue);
}
else {
// Keep track of inputs that were not initialized in case we need to know this for
// calling ngOnChanges with SimpleChanges
_this.uninitializedInputs.add(propName);
}
});
this.initialInputValues.clear();
};
As a workaround you could convert the flag to a string, wrap it in an object, or look at the element attributes from within the component.

Changing ContentChildren models on QueryList.changes

Suppose I have a parent component with #ContentChildren(Child) children. Suppose that each Child has an index field within its component class. I'd like to keep these index fields up-to-date when the parent's children change, doing something as follows:
this.children.changes.subscribe(() => {
this.children.forEach((child, index) => {
child.index = index;
})
});
However, when I attempt to do this, I get an "ExpressionChangedAfter..." error, I guess due to the fact that this index update is occurring outside of a change cycle. Here's a stackblitz demonstrating this error: https://stackblitz.com/edit/angular-brjjrl.
How can I work around this? One obvious way is to simply bind the index in the template. A second obvious way is to just call detectChanges() for each child when you update its index. Suppose I can't do either of these approaches, is there another approach?
As stated, the error comes from the value changing after the change cycle has evaluated <div>{{index}}</div>.
More specifically, the view is using your local component variable index to assign 0... which is then changed as a new item is pushed to the array... your subscription sets the true index for the previous item only after, it has been created and added to the DOM with an index value of 0.
The setTimout or .pipe(delay(0)) (these are essentially the same thing) work because it keeps the change linked to the change cycle that this.model.push({}) occurred in... where without it, the change cycle is already complete, and the 0 from the previous cycle is changed on the new/next cycle when the button is clicked.
Set a duration of 500 ms to the setTimeout approach and you will see what it is truly doing.
ngAfterContentInit() {
this.foos.changes.pipe(delay(0)).subscribe(() => {
this.foos.forEach((foo, index) => {
setTimeout(() => {
foo.index = index;
}, 500)
});
});
}
It does indeed allow the value to be set after the element is rendered on
the DOM while avoiding the error however, you will not have the value
available in the component during the constructor or ngOnInit if
you need it.
The following in FooComponent will always result in 0 with the setTimeout solution.
ngOnInit(){
console.log(this.index)
}
Passing the index as an input like below, will make the value
available during the constructor or ngOnInit of FooComponent
You mention not wanting to bind to the index in the template, but it unfortunately would be the only way to pass the index value prior to the element being rendered on the DOM with a default value of 0 in your example.
You can accept an input for the index inside of the FooComponent
export class FooComponent {
// index: number = 0;
#Input('index') _index:number;
Then pass the index from your loop to the input
<foo *ngFor="let foo of model; let i = index" [index]="i"></foo>
Then use the input in the view
selector: 'foo',
template: `<div>{{_index}}</div>`,
This would allow you to manage the index at the app.component level via the *ngFor, and pass it into the new element on the DOM as it is rendered... essentially avoiding the need to assign the index to the component variable, and also ensuring the true index is provided when the change cycle needs it, at the time of render / class initialization.
Stackblitz
https://stackblitz.com/edit/angular-ozfpsr?embed=1&file=src/app/app.component.html
One way is update the index value using a Macro-Task. This is essentially a setTimeout, but bear with me.
This makes your subscription from your StackBlitz look like this:
ngAfterContentInit() {
this.foos.changes.subscribe(() => {
// Macro-Task
setTimeout(() => {
this.foos.forEach((foo, index) => {
foo.index = index;
});
}, 0);
});
}
Here is a working StackBlitz.
So the javascript event loop is coming into play. The reason for the "ExpressionChangedAfter..." error is highlighting the fact that changes are being made to other components which essentially mean that another cycle of change detection should run otherwise you can get inconsistent results in the UI. That's something to avoid.
What this boils down to is that if we want to update something, but we know it shouldn't cause other side-effects, we can schedule something in the Macro-Task queue. When the change detection process is finished, only then will the next task in the queue be executed.
Resources
The whole event loop is there in javascript because there is only a single-thread to play with, so it's useful to be aware of what's going on.
This article from Always Be Coding explains the Javascript Event Loop much better, and goes into the details of the micro/macro queues.
For a bit more depth and running code samples, I found the post from Jake Archibald very good: Tasks, microtasks, queues and schedules
The problem here is that you are changing something after the view generation process is further modifying the data it is trying to display in the first place. The ideal place to change would be in the life-cycle hook before the view is displayed, but another issue arises here i.e., this.foos is undefined when these hooks are called as QueryList is only populated before ngAfterContentInit.
Unfortunately, there aren't many options left at this point. #matt-tester detailed explanation of micro/macro task is a very helpful resource to understand why the hacky setTimeout works.
But the solution to an Observable is using more observables/operators (pun intended), so piping a delay operator is a cleaner version in my opinion, as setTimeout is encapsulated within it.
ngAfterContentInit() {
this.foos.changes.pipe(delay(0)).subscribe(() => {
this.foos.forEach((foo, index) => {
foo.index = index;
});
});
}
here is the working version
use below code, to make that changes in the next cycle
this.foos.changes.subscribe(() => {
setTimeout(() => {
this.foos.forEach((foo, index) => {
foo.index = index;
});
});
});
I really don't know the kind of application, but to avoid playing with ordered indexes , it is often a good idea to use uid's as index.
Like this, there is no need to renumber indexes when you add or remove components since they are unique.
You maintain only a list of uids in the parent.
another solution that may solve your problem , by dynamically creating your components and thus maintain a list of these childs components in the parent .
regarding the example you provided on stackblitz (https://stackblitz.com/edit/angular-bxrn1e) , it can be easily solved without monitoring changes :
replace with the following code :
app.component.html
<hello [(model)]="model">
<foo *ngFor="let foo of model;let i=index" [index]="i"></foo>
</hello>
hello.component.ts
remove changes monitoring
added foocomponent index parameter
import { ContentChildren, ChangeDetectorRef, Component, Input, Host, Inject, forwardRef, QueryList } from '#angular/core';
#Component({
selector: 'foo',
template: `<div>{{index}}</div>`,
})
export class FooComponent {
#Input() index: number = 0;
constructor(#Host() #Inject(forwardRef(()=>HelloComponent)) private hello) {}
getIndex() {
if (this.hello.foos) {
return this.hello.foos.toArray().indexOf(this);
}
return -1;
}
}
#Component({
selector: 'hello',
template: `<ng-content></ng-content>
<button (click)="addModel()">add model</button>`,
})
export class HelloComponent {
#Input() model = [];
#ContentChildren(FooComponent) foos: QueryList<FooComponent>;
constructor(private cdr: ChangeDetectorRef) {}
addModel() {
this.model.push({});
}
}
I forked this working implementation : https://stackblitz.com/edit/angular-uwad8c

Angular 2 bind input to function call

Is it acceptable to bind #Input() property of child component to a function call of parent component, for example:
<navigation
[hasNextCategory]="hasNextCategory()"
[hasPreviousCategory]="hasPreviousCategory()"
(nextClicked)="nextCategory()"
(previousClicked)="previousCategory()"
(submitClicked)="submit()"
</navigation>
This seems to work, but I wonder how. Are those inputs re-evaluated when event is fired from component, or what drives the input binding?
Sure. The function is called every time change detection runs and assigns the result of the function call to the input property.
You get an exception in devMode when 2 successive calls return different values. like
hasNextValue() {
return {};
}
Exception: Expression has changed ...
It is discouraged to bind to functions. Rather assign the result to a property and bind to this property.
If you know what you are doing it's fine though.
update
so returning true / false according to some internal state is not allowed? Strange that my navigation still works
This is actually allowed. If your state changes because of some event (click, timeout, ...) then Angular change detection expect changes. If Angular change detection calls the method twice (as it does in devMode) without any event happening in between, then it doesn't expect changes and throws the exception mentioned above. What Angular doesn't like is when change detection itself causes changes.
Below example would also cause an exception because change detection itself would modify the components state (this.someState = !this.someState;)
which is not allowed.
someState:boolean = false;
hasNextValue() {
this.someState = !this.someState;
return this.someState;
}
Two successive calls would return false and true even when no event happened in between.
This example would work fine though
someState:boolean = false;
#HostListener('click') {
this.someState = !this.someState;
}
hasNextValue() {
return this.someState;
}
because two successive calls (without any event in between) would return the same value.

Categories

Resources