10

We use the standard date picker component, coming from Angular material (v. 9.1.2) that looks like this:

<mat-form-field>
    <mat-label i18n>Date of birth</mat-label>
    <input matInput [matDatepicker]="picker" formControlName="dateOfBirth" />
    <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
    <mat-datepicker #picker></mat-datepicker>
</mat-form-field>

The dates are in ISO format, e.g. 1979-12-02. Once bound to the form and displayed, we get it back like by calling getRawValue on the whole form. However, this gets the date back as javascript Date, which is then converted to string and send to the backend in "full" ISO format, e.g. 1979-12-02TXX:00:00.000Z, which breaks the contact/API.

If we use the MatMomentDateModule instead of the MatNativeDateModule, we get back a moment js date (instead of the javascript Date), but this doesn't help with the formatting.

Is there a way to bind the raw value of the control as a string instead of a date? Preferably without wrapping the component in a ControlValueAccessor? Perhaps a custom DateAdapter?

2
  • 1
    Personally I used my own component to wrap this material datepicker where I tracked value changes to transform value emitted to form Commented Jun 15, 2020 at 17:46
  • The way I handled this situation was to create a separate hidden input as a string, and use (dateChange)="onDateChange(true, $event.value._d)" to update the hidden field as needed. It even looks like you can use Angular's built in DatePipe for this. (we use moment.js, but this should work for either DateModule). If you feel this is the right approach, but need more information, I can go ahead and add relevant code as an answer... just let me know. I think this should be enough to get you in the right direction though. Commented Jun 21, 2020 at 20:32

7 Answers 7

2

You are right, you should implement custom DateAdapter to work with short ISO date strings.
Basically you just need to create a class extending DateAdapter and implement following methods:

  abstract getYear(date: D): number;

  abstract getMonth(date: D): number;

  abstract getDate(date: D): number;

  abstract getDayOfWeek(date: D): number;

  abstract getMonthNames(style: 'long' | 'short' | 'narrow'): string[];

  abstract getDateNames(): string[];

  abstract getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[];

  abstract getYearName(date: D): string;

  abstract getFirstDayOfWeek(): number;

  abstract getNumDaysInMonth(date: D): number;

  abstract clone(date: D): D;

  abstract createDate(year: number, month: number, date: number): D;

  abstract today(): D;

  abstract parse(value: any, parseFormat: any): D | null;

  abstract format(date: D, displayFormat: any): string;

  abstract addCalendarYears(date: D, years: number): D;

  abstract addCalendarMonths(date: D, months: number): D;

  abstract addCalendarDays(date: D, days: number): D;

  abstract toIso8601(date: D): string;

  abstract isDateInstance(obj: any): boolean;

  abstract isValid(date: D): boolean;

  abstract invalid(): D;

There are two great examples provided by Angular team: MomentDateAdapter and NativeDateAdapter.
When the adapter is implemented you will need to add it to the module or component as follows:

  providers: [
    {provide: DateAdapter, useClass: YourISODateAdapter, deps: [...]}
  ],
Sign up to request clarification or add additional context in comments.

Comments

1

I've analyzed MatDatepickerInputBase source code and currently there is no option to configure what kind or format of value you would like to have in related FormControl. So based on this idea of overwriting class methods I've put this code in app.module and I obtained Date as string value in my desired format YYYY-MM-DD. String is passed to control when user enters date by hand or choses date in calendar component and if date is valid ofcourse. I use my own DateAdapter overridden class also but it is not related to this problem, because DateAdapter only formats date to display in MatDatepickerInput control by overriding parse() and format() methods.

    const customFormatDate = (date: Date) => formatDate(date, 'yyyy-MM-dd', 'en');
    
    MatDatepickerInput.prototype._registerModel = function(model: any): void {
      this._model = model;
      this._valueChangesSubscription.unsubscribe();
    
      if (this._pendingValue) {
        this._assignValue(this._pendingValue);
      }
    
      this._valueChangesSubscription = this._model.selectionChanged.subscribe(event => {
        if (this._shouldHandleChangeEvent(event)) {
          const value = this._getValueFromModel(event.selection);
          this._lastValueValid = this._isValidValue(value);
          // this._cvaOnChange(value);
          this._cvaOnChange(customFormatDate(value));
          this._onTouched();
          this._formatValue(value);
          this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
          this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
        }
      });
    };


    MatDatepickerInput.prototype._onInput =  function(value: string) {
      const lastValueWasValid = this._lastValueValid;
      let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
      this._lastValueValid = this._isValidValue(date);
      date = this._dateAdapter.getValidDateOrNull(date);
    
      if (!this._dateAdapter.sameDate(date, this.value)) {
        this._assignValue(date);
    
        //this._cvaOnChange(date);
        this._cvaOnChange(customFormatDate(date));
    
        this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
      } else {
        // Call the CVA change handler for invalid values
        // since this is what marks the control as dirty.
        if ((value === '') || value && !this.value) {
          this._cvaOnChange(value);
        }
    
        if (lastValueWasValid !== this._lastValueValid) {
          this._validatorOnChange();
        }
      }
    };

Edit may-2022:

Better solution is to extend MatDatepickerInput<D> to custom directive as:

import {Directive, ElementRef, forwardRef, Inject, Input, Optional} from '@angular/core';
import {
  // MAT_DATEPICKER_VALIDATORS,
  // MAT_DATEPICKER_VALUE_ACCESSOR,
  MatDatepickerInput,
  MatDatepickerInputEvent
} from '@angular/material/datepicker';
import {MAT_INPUT_VALUE_ACCESSOR} from '@angular/material/input';
import {numFormatDateFn} from '../staticMethods';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
import {MAT_FORM_FIELD, MatFormField} from '@angular/material/form-field';
import {NG_VALIDATORS, NG_VALUE_ACCESSOR, Validators} from '@angular/forms';
import {MatDatepickerControl, MatDatepickerPanel} from '@angular/material/datepicker/datepicker-base';


/** @docs-private */
export const MAT_DATEPICKER_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => MatDatePickerCustomDirective),
  multi: true,
};

/** @docs-private */
export const MAT_DATEPICKER_VALIDATORS: any = {
  provide: NG_VALIDATORS,
  useExisting: forwardRef(() => MatDatePickerCustomDirective),
  multi: true,
};


@Directive({
  selector: 'input[appMatDatePickerCustom]',
  providers: [
    MAT_DATEPICKER_VALUE_ACCESSOR,
    MAT_DATEPICKER_VALIDATORS,
    {provide: MAT_INPUT_VALUE_ACCESSOR, useExisting: MatDatePickerCustomDirective},
  ],
  host: {
    'class': 'mat-datepicker-input',
    '[attr.aria-haspopup]': '_datepicker ? "dialog" : null',
    '[attr.aria-owns]': '(_datepicker?.opened && _datepicker.id) || null',
    '[attr.min]': 'min ? _dateAdapter.toIso8601(min) : null',
    '[attr.max]': 'max ? _dateAdapter.toIso8601(max) : null',
    // Used by the test harness to tie this input to its calendar. We can't depend on
    // `aria-owns` for this, because it's only defined while the calendar is open.
    '[attr.data-mat-calendar]': '_datepicker ? _datepicker.id : null',
    '[disabled]': 'disabled',
    '(input)': '_onInput($event.target.value)',
    '(change)': '_onChange()',
    '(blur)': '_onBlur()',
    '(keydown)': '_onKeydown($event)',
  },
})
export class MatDatePickerCustomDirective<D> extends  MatDatepickerInput<D> {

  /** The datepicker that this input is associated with. */
  @Input()
  set appMatDatePickerCustom(datepicker: MatDatepickerPanel<MatDatepickerControl<D>, D | null, D>) {
    if (datepicker) {
      this.matDatepicker = datepicker;
      // this._datepicker = datepicker;
      // this._closedSubscription = datepicker.closedStream.subscribe(() => this._onTouched());
      // this._registerModel(datepicker.registerInput(this));
    }
  }


  constructor(
    elementRef: ElementRef<HTMLInputElement>,
    @Optional() dateAdapter: DateAdapter<D>,
    @Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats,
    @Optional() @Inject(MAT_FORM_FIELD) _formField?: MatFormField,
  ) {
    super(elementRef, dateAdapter, dateFormats);
    // this._validator = Validators.compose(super._getValidators());
  }

  //MatDatepickerInput.prototype.
  _registerModel = function(model: any): void {
    this._model = model;
    this._valueChangesSubscription.unsubscribe();

    if (this._pendingValue) {
      this._assignValue(this._pendingValue);
    }

    this._valueChangesSubscription = this._model.selectionChanged.subscribe(event => {
      if (this._shouldHandleChangeEvent(event)) {
        const value = this._getValueFromModel(event.selection);
        this._lastValueValid = this._isValidValue(value);
        // this._cvaOnChange(value);
        this._cvaOnChange(numFormatDateFn(value));
        this._onTouched();
        this._formatValue(value);
        this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
        this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
      }
    });
  };


  //MatDatepickerInput.prototype.
  _onInput =  function(value: string) {
    console.warn('custom overrided _onInput in NBOX-MODULE EXECUTED!');
    //debugger
    const lastValueWasValid = this._lastValueValid;
    let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
    this._lastValueValid = this._isValidValue(date);
    date = this._dateAdapter.getValidDateOrNull(date);

    if (!this._dateAdapter.sameDate(date, this.value)) {
      this._assignValue(date);

      //this._cvaOnChange(date);
      this._cvaOnChange(numFormatDateFn(date));

      this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
    } else {
      // Call the CVA change handler for invalid values
      // since this is what marks the control as dirty.
      if ((value === '') || value && !this.value) {
        //this._cvaOnChange(date);
        // this._cvaOnChange(customFormatDate(date));
        this._cvaOnChange(value);
      }

      if (lastValueWasValid !== this._lastValueValid) {
        this._validatorOnChange();
      }
    }
  };

}

and use it as:

<input 
    [formControl]="someControl"
    [appMatDatePickerCustom]="picker"
    type="text"
    autocomplete="off">
<mat-datepicker-toggle [for]="picker"></mat-datepicker-toggle>
<mat-datepicker xPosition="end" #picker></mat-datepicker>

Comments

0

Before sending your object to api manually stringify it like below :-

const req = JSON.stringify(reqObj, (key: any, value: any) => {
          if (JSON.parse(JSON.stringify(moment(value))) === value) {
            return moment(value).format("yyyy/MM/DD");
          } else {
            return value;
          }
        });

and send req to api. this will convert any moment date in your requet object to your desired format.

2 Comments

Yes, but it will have to be done manually for each date field after calling getRawValue, which is not my intention. I'd like to see that happening declaratively somehow.
@MilanMilanov do you want to do it only for this model object or complete application?
0

Here the type of date object had been changed and the formcontrol value had been updated by using patch value method, so it will automatically bind the data in UI when u got response from backend api. using moment.js you can find [Using Moment.js]1

HTML:
      <form [formGroup]="frmStepOne" (ngSubmit)="onSubmit()">
        <div>
                <mat-form-field>
                  <mat-label>Choose a date</mat-label>
                  <input matInput [matDatepicker]="picker" formControlName="dateValue" (dateChange)="date($event)">
                  <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
                  <mat-datepicker #picker></mat-datepicker>
                </mat-form-field>
            </div>
           <div>
             <button type="submit">Submit</button>
           </div>
         </form>
        TS:
        convertDate:String;
        constructor(private _formBuilder: FormBuilder) {   
             this.frmStepOne = this._formBuilder.group({
              dateValue:['']
            });
        date(e) {
               this.convertDate = new Date(e.target.value).toISOString().substring(0, 10);
              this.frmStepOne.get('dateValue').patchValue(this.convertDate, {
                onlyself: true
              })
            }
     onSubmit(){
          console.log(this.frmStepOne.value.dateValue)
          console.log(typeof(this.convertDate));
        }

Comments

0

Using saturn date picker with moment for parsing dates. Sorry just had time to take a snippet.

      dateChanged(event: MatDatepickerInputEvent<DateRange>) {
        if (event.value) {
          const { begin, end } = event.value;
          this.store.dispatch(
            setDateRangeFilterAction({
              begin: moment(begin).tz("UTC").toISOString(),
              end: moment(end)
                .add(1, "days")
                .toISOString()
            })
          );
        } else {
          this.store.dispatch(setDateRangeFilterAction({ begin: null, end: null }));
        }
      }


<mat-form-field>
    <input
      matInput
      class="date-input"
      placeholder="Select a date range"
      name="range-picker"
      [satDatepicker]="picker"
      [value]="creationTimeFilter$ | async"
      (dateChange)="dateChanged($event)"
    />
    <sat-datepicker-toggle matSuffix [for]="picker">
      <mat-icon matDatepickerToggleIcon>keyboard_arrow_down</mat-icon>
    </sat-datepicker-toggle>
    <sat-datepicker #picker [rangeMode]="true"></sat-datepicker>
  </mat-form-field>

Comments

0

As mentioned below, the most native way to do it, is to write a new DateAdapter.

But a shorter solution, if you use reactive forms, is to build a form validator which accepts only a valid date in a valid format, and also translates the value into the valid format if the date is valid but the format is not. example:

function dateValidator(ctrl: AbstractControl) {
  const value = ctrl.value;
  if (value || value === 0) {
    const d = new Date(value);
    if (isNaN(+d)) {
      return {invalidDate: true}
    }
    const validFormat = d.toISOString(); // Or whatever convert function
    if (value !== validFormat) {
      ctrl.setValue(validFormat);
      return {invalidFormat: true}
    }
  }
  return null;
}

If the value can be parsed to a valid date, but is not in the required format, set a new formatted value, which will call the validator again, but this time it will be valid (returns null).

Don't forget to add this validator to the date picker's form.

About custom date validators

Comments

-2

You can create HttpInterceptor then iterate through the request body to convert all the datetime properties to your desired format

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { DatePipe } from '@angular/common';
import { isArray } from 'util';
@Injectable()
export class HttpDatetimeInterceptor implements HttpInterceptor {
  constructor(private datePipe: DatePipe) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    this.convertDatetimeToStringFormate(request.body);
    return next.handle(request);
  }

  convertDatetimeToStringFormate(body) {
    for (let [key] of Object.entries(body)) {

      if (isArray(body[key])) {
        for (let prop of body[key]) {
          this.convertDatetimeToStringFormate(prop);
        }
      }

      // Check if property is date
      if (typeof body[key].getMonth === 'function') {
        body[key] = this.datePipe.transform(body[key], 'yyyy/MM/dd');
      }
    }
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.