I've had an ongoing problem with the emitting of a date in Angular Material Datepicker. I am working with an application where we have JSON files in Contentstack. The JSON files have the controls for an Angular Reactive form as well as an email template for each one. In a few of the templates is a control that is a date. The idea with the JSON files is that the Reactive form populates the email template in the JSON file and then emits it to the view. I am using MatDatepicker in the form for selecting the date. The issue I had been struggling with was that even though the form would show a date as, say, 08/31/2019, when the email preview showed in the view the date read as Thurs Aug 31 2019 00:00 etc. I wanted it to show in the email as 8/31/2019. I knew the problem had to do with formatting the date between it's selection and its emission.
I figured out a solution to that problem. We were invoking a method called onChange() every time a change was made. The method simply checked if the form was valid, and if it did, the EventEmitter emit was set to true, otherwise to false. I found if I looped the the formGroup keys, pulled out the ones with Date in the formControlName, subscribed to valueChanges on them, and then did the date formatting on each change, the email preview would show the date correctly.
Here is the problem though. With that in place, when you click a date on the Datepicker calendar, the calendar no longer disappears. You have to click outside the calendar to get it to disappear. Meanwhile, calls keep being made, even after you've made the calendar disappear, until you finally hit the call stack limit.
Based on other questions and answers here, I tried adding a Subscription object and using it with the valueChanges and then unsubscribing to it in ngOnDestroy. That did nothing to solve the problem. Can anyone tell me how to shut off the calls once the value change has been detected and formatted?
Here is the component TypeScript file:
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ContentStackService } from 'src/app/services/content-stack.service';
import { Customer } from 'src/app/models/customer.model';
import { AdditionalEmailTemplateDefintionResponse, Control, TemplateDefinition } from 'src/app/models/additional-email-template-definition.model';
import { formatDate } from '@angular/common';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-ssp-send-additional-email-template-detail',
templateUrl: './ssp-additional-email-template-detail.component.html',
styleUrls: ['./ssp-additional-email-template-detail.component.scss'],
})
export class SspSendAdditionalEmailTemplateDetailComponent implements OnInit, OnDestroy {
@Input() customer : Customer;
@Input() selectedTemplateId:string;
@Input() formGroup: FormGroup;
@Input() productType: string;
@Input() stateCode: string;
@Output() onTemplateDetailedFilled = new EventEmitter();
@Output() templateDefinitionEmitter = new EventEmitter<TemplateDefinition>();
customerInfoLookUp = new Map<string, string>();
public templateDefinition!: TemplateDefinition;
public subscr: Subscription;
constructor(
private contentStackService: ContentStackService,
private fb: FormBuilder) {}
ngOnInit(): void {
console.log('*****SspSendAdditionalEmailTemplateDetailComponent****');
console.log('input customer', this.customer);
// Build customer lookup used for dynamic form population.
this.buildCustomerLookup();
this.contentStackService.getAdditionalEmailTemplateDefinition(this.selectedTemplateId)
.subscribe((formData: AdditionalEmailTemplateDefintionResponse) => {
console.log('getAdditionalEmailTemplateDefinition', formData);
this.templateDefinition = formData.template_definition;
this.createForm(formData.template_definition.controls);
this.templateDefinitionEmitter.emit(this.templateDefinition);
});
}
createForm(controls: Control[]) {
for (const control of controls) {
const validatorsToAdd = [];
for (const [key, value] of Object.entries(control.validators)) {
switch (key) {
case 'min':
validatorsToAdd.push(Validators.min(value));
break;
case 'max':
validatorsToAdd.push(Validators.max(value));
break;
case 'required':
if (value) {
validatorsToAdd.push(Validators.required);
}
break;
case 'requiredTrue':
if (value) {
validatorsToAdd.push(Validators.requiredTrue);
}
break;
case 'email':
if (value) {
validatorsToAdd.push(Validators.email);
}
break;
case 'minLength':
validatorsToAdd.push(Validators.minLength(value));
break;
case 'maxLength':
validatorsToAdd.push(Validators.maxLength(value));
break;
case 'pattern':
validatorsToAdd.push(Validators.pattern(value));
break;
case 'nullValidator':
if (value) {
validatorsToAdd.push(Validators.nullValidator);
}
break;
default:
break;
}
}
if (control.type === 'date') {
this.formGroup.addControl(
control.name,
this.fb.control(new Date(), validatorsToAdd),
);
} else {
console.log('control.value', control);
if (control.path) {
const pathSplit = control.path.split('.');
if (pathSplit[0] === 'customer') {
control.value = this.customerInfoLookUp.get(pathSplit[1]);
}
}
this.formGroup.addControl(
control.name,
this.fb.control(control.value, validatorsToAdd),
);
this.onChange();
}
}
}
buildCustomerLookup() {
this.customerInfoLookUp.set('firstName',
this.customer.person.personNameList[0].firstName);
this.customerInfoLookUp.set('lastName', this.customer.person.personNameList[0].lastName);
}
onChange() {
Object.keys(this.formGroup.controls).forEach(key => {
if ((key).includes('Date')) {
this.subscr = this.formGroup.get(key).valueChanges.subscribe(x => {
console.log('Value of ' + key + ' changed to ' + x);
this.formGroup.get(key).setValue(formatDate(x, 'MM/dd/YYYY', 'en-US'));
});
}
});
if (this.formGroup.valid) {
this.onTemplateDetailedFilled.emit(true);
} else {
this.onTemplateDetailedFilled.emit(false);
}
}
ngOnDestroy(): void {
this.subscr.unsubscribe();
}
}
Here is the HTML for the component:
<ng-container [formGroup]="formGroup" *ngFor="let control of templateDefinition?.controls" class="ssp-template-detail-full-width">
<!-- input type text-->
<mat-form-field *ngIf="[
'text',
'password',
'email',
'number',
'search',
'tel',
'url'
].includes(control.type)" class="ssp-template-detail-full-width">
<mat-label>{{control.label}}</mat-label>
<input matInput [id]="control.name" [formControlName]="control.name" [type]="control.type" (change)="onChange()">
</mat-form-field>
<!-- input type textarea -->
<mat-form-field *ngIf="[
'textarea'
].includes(control.type)" class="ssp-template-detail-full-width">
<mat-label>{{control.label}}</mat-label>
<textarea matInput [id]="control.name" [formControlName]="control.name" (change)="onChange()"></textarea>
</mat-form-field>
<!-- checkbox-->
<p *ngIf="[
'checkbox'
].includes(control.type)" class="ssp-template-detail-full-width">
<mat-checkbox [id]="control.name" [formControlName]="control.name" (change)="onChange()">{{control.label}}</mat-checkbox>
</p>
<!-- dropdown-->
<mat-form-field *ngIf="[
'dropdown'
].includes(control.type)" class="ssp-template-detail-full-width">
<mat-label>{{control.label}}</mat-label>
<mat-select [id]="control.name" [formControlName]="control.name" (change)="onChange()">
<mat-option *ngFor="let option of control.options" [value]="option.value">
{{option.option}}
</mat-option>
</mat-select>
</mat-form-field>
<!-- date -->
<mat-form-field *ngIf="[
'date'
].includes(control.type)" class="ssp-template-detail-full-width">
<mat-label>{{control.label}}</mat-label>
<input matInput [matDatepicker]="picker" [id]="control.name" [formControlName]="control.name" (change)="onChange()">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
<mat-hint>MM/DD/YYYY</mat-hint>
</mat-form-field>
<!-- input type text-->
<mat-form-field *ngIf="[
'time'
].includes(control.type)" class="ssp-template-detail-full-width">
<mat-label>{{control.label}}</mat-label>
<input matInput [id]="control.name" [formControlName]="control.name" type="text" (change)="onChange()">
</mat-form-field>
</ng-container>