1

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>
apex2022
  • 777
  • 2
  • 8
  • 28

2 Answers2

1
export class SspSendAdditionalEmailTemplateDetailComponent ...
  private subscriptionList = new Subscription(); // Instantiate it immediately
  ...
  onChange() {
    Object.keys(this.formGroup.controls).forEach(key => {
      if ((key).includes('Date')) {
        // This is why I call it subscriptionList, you are adding subscriptions to the list
        this.subscriptionList.add(
          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'), {emitEvent: false});
          })
        );
      }
    });
    if (this.formGroup.valid) {
      this.onTemplateDetailedFilled.emit(true);
    } else {
      this.onTemplateDetailedFilled.emit(false);
    }
  }

  ngOnDestroy(): void {
    // Then you tell the entire list to unsubscribe.
    // If a sub in the list is already unsubscribed, it silently passes.
    this.subscriptionList.unsubscribe();
  }
  • This should work for an unlimited number of subscriptions in the subscriptionList as well. It sucks having to manually clean up the subscriptions this way but it is very clean and efficient. – Myles Morrone Aug 20 '23 at 01:12
  • That did not work. Still zooming up in calls until call stack exceeded. – apex2022 Aug 20 '23 at 15:23
  • Wait a minute, you a doing form.valueChanges.sub(_ => form.setValue()). Which means, every time the form changes, it sets the value, which then detects a change, which then sets the value...that isn't an unsubscribe issue, you coded an infinite loop by the looks of it. Try changing your setValue to include emitEvent false, exampled below: this.formGroup.get(key).setValue(formatDate(x, 'MM/dd/YYYY', 'en-US'), {emitEvent: false}); I also updated my answer above to reflect that change. If that still doesn't work, try making a stackblitz and I can tweak your code manually. – Myles Morrone Aug 21 '23 at 21:12
  • Hi, as you can see from my posted answer to my question, the solution turned out not to involve subscription, but I do want you to know that I really appreciate your suggestions for my problem. Thanks for taking the time to help me! – apex2022 Aug 23 '23 at 14:33
1

Well, I found an answer to my problem, and it surprised me. The answer was not to use a subscription at all, but to do something completely different.

The subscription idea was based on the premise that I had to alter the format of the Date before it was emitted per the Typescript file. With the line this.onTemplateDetailedFilled.emit(true); the entire template is emitted to another component. It turns out what I really needed to do was to do the formatting in that component's ngOnInit. I had been under the impression that once the template was emitted it would show per that other component's template and it would be too late to change it. I guess I just didn't have an understanding of how ngOnInit works.

apex2022
  • 777
  • 2
  • 8
  • 28