How to detect values changes after form builder and patchValue called? - html

I have this create and edit screen both in separated component. So in order to pump data in edit screen, I need to use patchValue and patch data with all the responses get back from API.
So after patching all values I need to disable the Update button for better user experiences. Then whenever users trigger changes on the form , I will enable the Update button again.
The problem now is I wish to disable the button in form.valueChanges but seems failed because patchValue will trigger valueChanges once so is there anyway I can disable my button after all data been patched?
The boolean that control the button is isFormNoChanges stated below
constructor(
private readonly router: Router,
private readonly formBuilder: FormBuilder,
private readonly agentService: AgentService,
) {
this.agentService.getAgentMLRDetails().then((data: AgentList) => {
this.agentDetails = data;
this.injectValue(this.agentDetails);
})
}
ngOnInit() {
this.onChanges();
}
injectValue(data: AgentMLRList) {
this.mlrForm.patchValue({
entity: data.entityCode,
agentCode: data.agentCode,
productCode: data.product.productCode,
coverageCode: data.coverage.coverageCode,
startDate: data.startDate,
endDate: data.endDate
})
}
onChanges() {
this.mlrForm.valueChanges
.subscribe(data => {
this.isFormNoChanges = false
})
}

Hey do this and use skip operator as below :-
this.mlrForm.valueChanges
.pipe(skip(1)).subscribe(data => {
this.isFormNoChanges = false
})
or use the solution i mentioned in comments above.
or use patchValue like
patchValue({
entity: data.entityCode,
agentCode: data.agentCode,
productCode: data.product.productCode,
coverageCode: data.coverage.coverageCode,
startDate: data.startDate,
endDate: data.endDate
}, { onlySelf: true, emitEvent: false })

Related

How can i show a spinner while waiting for data from a get call inside my ngOnInit ? (Angular 15)

i'm struggling on something that should be pretty easy.
I'm trying to render a spinner, whenever a get call is ongoing, so instead of displaying an empty screen, i can use the spinner.
I thought of using two separate div, controlled by two ngIf, related to the same bool flag. Of course if one is *ngIf="flag", the other one is *ngIf="!flag".
I edit the value, inside the 'subscribe' of the my get call, but unfortunately, the bool (although it changes), does not affect the html (probably because how angular works, and lifecycle of the components).
Do you know how can i do this ?
In my data service component i have a really simple http get to fill my variable 'products : Product[]', and it works.
In my component shop.ts i have
#Component({
selector: 'app-shop',
templateUrl: './shop.component.html',
styleUrls: ['./shop.component.css'],
})
export class ShopComponent {
constructor(public ds: DataService) {}
/* Variables */
products: Product[] = [];
isDataLoaded: boolean = false;
/* With this get call, we get all the products informations, and we save'em
into products */
ngOnInit() {
this.ds.getProducts().subscribe((resp) => {
this.products = resp as Product[];
this.isDataLoaded = true;
}
});
}
In the component html i just have
<div *ngIf="!isDataLoaded">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="isDataLoaded">
Data is loaded
</div>
I do this all the time. Here is the approach I use.
Store the result in a subscription and set it equal to the request like so:
#Component({
selector: 'app-shop',
templateUrl: './shop.component.html',
styleUrls: ['./shop.component.css'],
})
export class ShopComponent {
constructor(public ds: DataService) {}
products: Product[] = [];
isDataLoaded$: Subscription;
ngOnInit() {
this.isDataLoaded$ = this.ds.getProducts().subscribe((resp) =>
this.products = resp as Product[];
);
}
}
Then in your template, check if the subscription exists and is not closed:
<mat-spinner *ngIf="isDataLoaded$ && !isDataLoaded$.closed"></mat-spinner>
<div *ngIf="isDataLoaded$ && isDataLoaded$.closed">
Data is loaded
</div>
Problems with your original approach
If that request fails, your isDataLoaded variable will never update since you don't have an error block. Also, once you set that variable to true, it stays true. What happens if the user fires that request again? You need to also reset it back to false before each request so the spinner shows up.
Here is an improved version of your original code, although I do not recommend going with this approach.
ngOnInit() {
this.isDataLoaded = false;
this.ds.getProducts().subscribe((resp) => {
this.products = resp;
this.isDataLoaded = true;
}, error => {
...
this.isDataLoaded = true;
});
}
Can you show how you implemented the getProduts method?
I tried to replicate your project, like this:
constructor(public ds: DataService) {}
/* Variables */
products: Product[] = [];
isDataLoaded: boolean = false;
/* With this get call, we get all the products informations, and we save'em
into products */
ngOnInit() {
this.ds.getProducts()
.subscribe((resp) => {
this.products = resp;
this.isDataLoaded = true;
});
}
And I implemented the Data Service like this:
constructor(private http: HttpClient) {}
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>('API');
}
And it works. Maybe it works for you too, but the data are loaded so fast that you don't see the spinner.
import { finalize } from 'rxjs';
...
this.ds.getProducts()
.pipe(finalize(() => this.isDataLoaded = true))
.subscribe((resp) => {
this.products = resp as Product[];
});

Angular: ExpressionChangedAfterItHasBeenCheckedError when trying to disable button

I use mat-dialog to edit details of my profile page. I'm getting an ExpressionChangedAfterItHasBeenCheckedError when I click the 'Edit age' button and the dialog window pops up.
I decided to extract the styling of all edit dialogs into a single edit.component:
edit.component.html
<div class="navigation-control">
<mat-icon (click)="onCancelButtonClicked()"
class="close-button">close</mat-icon>
</div>
<div class="content-main">
<ng-content select=".content-main"></ng-content>
</div>
<div class="content-bot">
<button mat-raised-button
(click)="onCancelButtonClicked()">Cancel</button>
<button mat-raised-button
(click)="onActionButtonClicked()"
[lnDisableButton]="actionButtonDisabled">{{actionButtonValue}}</button>
</div>
edit.component.ts
#Component({ selector: 'ln-edit', ... })
export class EditComponent {
#Input() actionButtonValue: string;
#Input() actionButtonDisabled: boolean;
#Output() cancelButtonClicked = new EventEmitter<void>();
#Output() actionButtonClicked = new EventEmitter<void>();
onCancelButtonClicked() {
this.cancelButtonClicked.emit();
}
onActionButtonClicked() {
this.actionButtonClicked.emit();
}
}
To avoid the ExpressionChangedAfterItHasBeenCheckedError when trying to disable buttons and controls, I used this snippet. But that didn't solve this issue.
disable-button.directive.ts
#Directive({ selector: '[lnDisableButton]' })
export class DisableButtonDirective {
#Input('lnDisableButton') isDisabled = false;
#HostBinding('attr.disabled')
get disabled() { return this.isDisabled; }
}
The following is the contents of a mat-dialog window. This gets instantiated when I click the 'Edit age' button. When I remove the [actionButtonDisabled]="actionButtonDisabled", the error goes away, but obivously I need that line to make the functionality disable the button.
age-edit.component.html
<ln-edit [actionButtonValue]="actionButtonValue"
[actionButtonDisabled]="actionButtonDisabled"
(cancelButtonClicked)="onCancelButtonClicked()"
(actionButtonClicked)="onActionButtonClicked()">
<form [formGroup]="ageForm"
class="content-main">
<ln-datepicker formControlName="birthday"
[appearance]="'standard'"
[label]="'Birthday'"
class="form-field">
</ln-datepicker>
</form>
</ln-edit>
I handle the disabling/enabling the button in the 'ts' part of the mat-dialog popup.
age-edit.component.ts
#Component({ selector: 'ln-age-edit', ... })
export class AgeEditComponent implements OnInit, OnDestroy {
ageForm: FormGroup;
private initialFormValue: any;
actionButtonDisabled = true;
private unsubscribe = new Subject<void>();
constructor(
private editPhotoDialogRef: MatDialogRef<AgeEditComponent>,
private fb: FormBuilder,
#Inject(MAT_DIALOG_DATA) public dialogData: Date) { }
ngOnInit() {
this.initializeAgeForm();
this.loadDataToAgeForm(this.dialogData);
this.trackFormDistinct();
}
private initializeAgeForm(): void {
this.ageForm = this.fb.group({
birthday: null,
});
}
loadDataToAgeForm(birthday: Date | null): void {
if (!birthday) { return; }
this.ageForm.setValue({ birthday });
this.initialFormValue = this.ageForm.value;
}
get birthdayAC() { return this.ageForm.get('birthday') as AbstractControl; }
get actionButtonValue(): string {
return this.birthdayAC.value ? 'Update age' : 'Add age';
}
onCancelButtonClicked(): void {
this.editPhotoDialogRef.close();
}
onActionButtonClicked(): void {
this.editPhotoDialogRef.close({ ... });
}
trackFormDistinct(): void {
this.ageForm.valueChanges.pipe(
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(val => {
(this.formValueNotDistinct(this.ageForm.value, this.initialFormValue)
|| this.birthdayAC.value === null)
? this.actionButtonDisabled = true
: this.actionButtonDisabled = false;
});
}
ngOnDestroy() { ... }
}
I suspect this has something to do with content projection, but I'm not sure.
(...or perhaps with my custom 'ln-datepicker'?)
Any ideas?
Thanks.
From what I can tell, the problem resides in trackFormDistinct() method:
trackFormDistinct(): void {
this.ageForm.valueChanges.pipe(
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(val => {
(this.formValueNotDistinct(this.ageForm.value, this.initialFormValue)
|| this.birthdayAC.value === null)
? this.actionButtonDisabled = true
: this.actionButtonDisabled = false;
});
}
Looks like because of this.ageForm.valueChanges, will have different values in the 2 change detection cycles. I think this.ageForm.valueChanges emits due to <ln-datepicker>.
In a tree of form controls, if one node calls setValue, all its ancestors will have to be updated. I've written more about how Angular Forms work in this article.
I'm thinking of 2 alternatives:
skip the first emission of ageForm since it indicates the initialization of the form control tree, so this is irrelevant to the logic inside subscribe's callback.
this.ageForm.valueChanges.pipe(
skip(1),
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(/* .... */)
initialize actionButtonDisabled with false, since the error complains that it switched from true to false
actionButtonDisabled = false;

Cannot Bind Dynamic Data in Component Angular 8

Error when component loading dynamic
DynamicBuilderComponent.ngfactory.js:198 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ng-pristine: true'. Current value: 'ng-pristine: false'.
Problem
after binding json in select2data to select2 component Angular throw exception.
component code
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'select2',
Imported changeDetection in component.
template: `
<div [formGroup]="form">
<ng-container>
<ng-select2
[data]="select2data"
[options]="options"
[width]="500"
[formControlName]="field.code"
(keyup)="changed($event.target.value)">
</ng-select2>
</ng-container>
</div>`
})
select2 component class
export class Select2Component implements OnInit {
#Input() field: any = {};
#Input() form: FormGroup;
public exampleData: Array<Select2OptionData>;
public options: Options;
public value: string[];
select2data: any;
public selected: string;
constructor(public cl: Services,private cd: ChangeDetectorRef) {
this.options = {
width: '258',
multiple: true,
tags: false
};
}
Problem Area After Binding subscribe data in ng select2 component
changed(search: any) {
//call service pass search text to service
return this.cl.searchFunc(search).subscribe(
res1 =>
this.select2data = res1.data;
this.cd.markForCheck(); // marks path
}
}
},
error => {
console.log('error = ', error);
});
}
}
i tried to print this.select2data in console.log its return me json.
Vendor.js
function expressionChangedAfterItHasBeenCheckedError(context, oldValue, currValue, isFirstCheck) {
var msg = "ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '" + oldValue + "'. Current value: '" + currValue + "'.";
if (isFirstCheck) {
msg +=
" It seems like the view has been created after its parent and its children have been dirty checked." +
" Has it been created in a change detection hook ?";
}
return viewDebugError(msg, context);
}
Great Article
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
Reference
Expression ___ has changed after it was checked
any suggestion is most welcome.
I believe that you put your component select2 inside another component which contains a form which you then pass to select2 for create another <form> tag, is that correct? I mean do you have something like that?
<form [formGroup]="form">
<!-- Some code -->
<select2 [field]="something" [form]="form"></select2>
</form>
If so, then your select2 component SHOULD NOT contain re-declaration of form, it should not contain anything related to forms at all. It should be a form control. Please read a post by Netanel Basal on how to create custom form controls. You will need to create ControlValueAccessor for your select2 and wire it up to Angular forms through a custom provider.
The issue you're facing is that since you include form object twice in the DOM data changes are propagated twice as well and you run into issues. There should be only one reference to a specific instance of FormGroup in your templates.
Solution that worked
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'select2',
export class Select2Component implements OnInit {
constructor(public cl: Services,private cd: ChangeDetectorRef) {
this.options = {
width: '258',
multiple: true,
tags: false
};
}
Binding function
changed(search: any) {
//call service pass search text to service
return this.cl.searchFunc(search).subscribe(
res1 =>
this.select2data = res1.data;
this.cd.markForCheck(); // marks path
this.cd.detectChanges();
}
}
},
error => {
console.log('error = ', error);
});
}

Angular 4 Need to refresh page in order to show data

I'm running in the same problem. I try to display the sex of the user inside my navbar component. I call for my service to get me the user object and then I try to set my 'gender' for use in HTML. Problem is I need to refresh the page in order to display the gender.. Any help please? :)
export class NavbarComponent implements OnInit {
title = 'navbar';
userIsLoggedIn: boolean;
user: User;
currentUser: Parent;
gender: string;
constructor(private authenticationService: AuthenticationService, private router: Router, private parentService: ParentService) {
authenticationService.userIsloggedIn.subscribe(isLoggedIn => {
this.userIsLoggedIn = isLoggedIn;
this.user = authenticationService.getUser();
});
}
ngOnInit(): void {
this.user = this.authenticationService.getUser();
this.userIsLoggedIn = this.user != undefined;
this.getParentFromUserEmail(this.user.email);
this.getSex();
}
private getParentFromUserEmail(email: string) {
this.parentService.getByEmail(email).map(
(response) => this.currentUser = response).subscribe(data => {
this.gender = data.type;
});
}
getSex() {
return this.gender;
}
}
HTML CODE
<div class="sidebar-account-content">
<h3>{{user?.firstname}} {{user?.lastname}}</h3>
<p *ngIf="getSex()">Test</p>
<p *ngIf="gender === 'F'">Father</p>
<p *ngIf="gender === 'M'">Mother</p>
</div>
I suppose that your "getUser" is a asynchronous call, therefore you not have the data of the user when call it. I suppose you must make some like
ngOnInit(): void {
authenticationService.getUser().then((user)=>{
this.user = user;
this.userIsLoggedIn = this.user != undefined;
this.getParentFromUserEmail(this.user.email);
this.getSex();
}
});
}
Why would you want to create a two way binding on a method? You can just, in your template say
<p *ngIf="gender">Test</p>
Then you can just edit the gender in your component.ts file in order to change it on the template. No need for a getter.
I fixed the problem, I changed my return types from my service to promises. Also when our app launched we route instantly to login page since its a secure platform. The app component rendered the navbar which it shouldn't so everything from there was full of bugs. Thanks for the help, cheers.

How to make Angular 2 render HTML template after a promise in the component is resolved?

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