Angularで和暦の日付入力機能を作成する

input-date.component.html

<div role="group" [formGroup]="form">
  <mat-form-field appearance="fill">
    <mat-label>Input Date</mat-label>
    <span matPrefix>{{era}} </span>
    <multi-field-area formControlName="element" [items]="items" [check]="check" (event)="updateValue($event)" required #elm></multi-field-area>
    <input matInput [(value)]="value" [matDatepicker]="picker1" style="display: none;" (dateChange)="change($event.target.value)" required>
    <mat-datepicker-toggle matSuffix [for]="picker1"></mat-datepicker-toggle>
    <mat-datepicker #picker1></mat-datepicker>
    <mat-hint>input date with era</mat-hint>
  </mat-form-field>
</div>

input-date.component.ts

import { MultiFieldInput} from './multi-field-control';
import { Component, ViewChild } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { DateAdapter } from '@angular/material/core';
import { JpDateAdapter } from './jp-date-adapter';

@Component({
  selector: 'app-input-date',
  templateUrl: './input-date.component.html',
  styleUrls: ['./input-date.component.css'],
  providers: [
    {provide: DateAdapter, useClass: JpDateAdapter}
  ]
})
export class InputDateComponent {
  @ViewChild('elm') public element: MultiFieldInput;
  items = [
    {size:2},
    {size:2},
    {size:2},
  ]
  form: FormGroup = new FormGroup({
    element: new FormControl(this.items.map(x => '')),
  });
  value;
  era;

  change(date:Date) {
    let era = this.getEra(date)
    this.era = era.name
    this.form.patchValue({element:[
      era.year, 
      this.pad(date.getMonth()+1, '0', 2), 
      this.pad(date.getDate(), '0', 2)
    ]})
  }

  check(values:string[]):boolean {
    if (!values) return true
    let tmp = `${2018+Number(values[0])}/${values[1]}/${values[2]}`
    let date = new Date(tmp)
    return isNaN(date.getTime())
  } 

  updateValue(e) {
    if (e != null) {
      let tmp = `${2018+Number(e[0])}/${e[1]}/${e[2]}`
      let date = new Date(tmp)
      if (this.isValid(date)) {
        this.value = date
      } else {
        this.value = ''
      }
    } else {
      this.value = ''
    }
  }

  isValid(date) {
    return !isNaN(date.getTime())
  }

  getEra(date) {
    const year = date.getFullYear();
    if (year > 2018) {
      return {name:'令和', year: this.pad(year - 2018, '0', 2)}
    } else if (year > 1988) {
      return {name:'平成', year: this.pad(year - 1988, '0', 2)}
    } else if (year > 1925) {
      return {name:'昭和', year: this.pad(year - 1925, '0', 2)}
    } else if (year > 1911) {
      return {name:'大正', year: this.pad(year - 1911, '0', 2)}
    } else if (year > 1867) {
      return {name:'明治', year: this.pad(year - 1867, '0', 2)}
    }
    return {name:'西暦', year: this.pad(year, '0', 4)}
  }

  pad(base, chr, num) {
    return ( chr.repeat(num) + base ).slice( -num )
  }
}

multi-field-control.html

<div role="group" class="example-tel-input-container"
     [formGroup]="parts"
     [attr.aria-labelledby]="_formField?.getLabelId()">
       <ng-container *ngFor="let item of items; let i = index">
              <span *ngIf="i!=0" class="example-tel-input-spacer">/</span>
              <input class="example-tel-input-element"
                     [formControlName]="controlName(i)"
                     [maxLength]="item.size"
                     [size]="item.size"
                     (blur)="_handleBlur(parts.controls[controlName(i)], i)"
                     (input)="_handleInput(parts.controls[controlName(i)], Math.min(i+1, items.length-1))"
                     (keyup.backspace)="autoFocusPrev(parts.controls[controlName(i)], Math.max(i-1, 0))"
                     (keypress)="numberOnly($event)"
                     #inputs>
       </ng-container>
</div>

multi-field-control.ts

import {FocusMonitor} from '@angular/cdk/a11y';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {
  Component,
  ElementRef,
  Inject,
  Input,
  OnInit,
  OnDestroy,
  Optional,
  Self,
  Output,
  EventEmitter,
  ViewChildren,
  QueryList
} from '@angular/core';
import {
  AbstractControl,
  ControlValueAccessor,
  FormBuilder,
  FormGroup,
  NgControl,
  Validators
} from '@angular/forms';
import {MAT_FORM_FIELD, MatFormField, MatFormFieldControl} from '@angular/material/form-field';
import {Subject} from 'rxjs';

@Component({
  selector: 'multi-field-area',
  templateUrl: 'multi-field-control.html',
  styleUrls: ['example-tel-input-example.css'],
  providers: [{ provide: MatFormFieldControl, useExisting: MultiFieldInput }],
  host: {
    '[class.example-floating]': 'shouldLabelFloat',
    '[id]': 'id',
  }
})
export class MultiFieldInput
  implements ControlValueAccessor, MatFormFieldControl<string[]>, OnDestroy, OnInit {
  static nextId = 0;
  parts: FormGroup;
  stateChanges = new Subject<void>();
  focused = false;
  controlType = 'multi-field-input';
  id = `multi-field-input-${MultiFieldInput.nextId++}`;
  onChange = (_: any) => {};
  onTouched = () => {};

  get empty() {
    this.parts
    return Object.values(this.parts.value).map(x => !x).reduce((l,r) => l && r)
  }

  get shouldLabelFloat() {
    return this.focused || !this.empty;
  }

  @Input('aria-describedby') userAriaDescribedBy: string;

  @Input()
  get placeholder(): string {
    return this._placeholder;
  }
  set placeholder(value: string) {
    this._placeholder = value;
    this.stateChanges.next();
  }
  private _placeholder: string;

  @Input()
  get required(): boolean {
    return this._required;
  }
  set required(value: boolean) {
    this._required = coerceBooleanProperty(value);
    this.stateChanges.next();
  }
  private _required = false;

  @Input()
  get disabled(): boolean {
    return this._disabled;
  }
  set disabled(value: boolean) {
    this._disabled = coerceBooleanProperty(value);
    this._disabled ? this.parts.disable() : this.parts.enable();
    this.stateChanges.next();
  }
  private _disabled = false;

  @Input()
  get value(): string[] | null {
    if (this.parts.valid) {
        return Object.values(this.parts.value);
    }
    return null;
  }
  set value(element: string[] | null) {
    const fields = element || [];
    console.log(fields)
    const obj = {};
    fields.forEach((x, i) => obj[this.controlName(i)]=x)
    if (this.parts) this.parts.patchValue(obj);
    this.stateChanges.next();
  }

  @Input() check:(val:string[]) => boolean = () => false
  get errorState(): boolean {
    const flg = this.check(this.value)
    return (this.parts.invalid || flg) && this.parts.dirty;
  }

  get Math() {
    return Math;
  }

  controlName(i) {
    return `field${Number(i)+1}`
  }

  @ViewChildren('inputs') inputs: QueryList<HTMLInputElement>;
  @Output() event = new EventEmitter<string[]>();

  @Input() items;

  constructor(
    private formBuilder: FormBuilder,
    private _focusMonitor: FocusMonitor,
    private _elementRef: ElementRef<HTMLElement>,
    @Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField,
    @Optional() @Self() public ngControl: NgControl) {

    _focusMonitor.monitor(_elementRef, true).subscribe(origin => {
      if (this.focused && !origin) {
        this.onTouched();
      }
      this.focused = !!origin;
      this.stateChanges.next();
    });

    if (this.ngControl != null) {
      this.ngControl.valueAccessor = this;
    }
  }

  ngOnInit() {
    const obj = {}
    this.items.forEach((e, i) => {
      obj[this.controlName(i)] = [ null, [Validators.required, Validators.minLength(e.size), Validators.maxLength(e.size)] ]
    })
    this.parts = this.formBuilder.group(obj);
  }

  autoFocusNext(control: AbstractControl, next?: number): void {
    if (!control.errors && next) {
      this._focusMonitor.focusVia(this.inputs.find((e,i) => i == next), 'program');
    }
  }

  autoFocusPrev(control: AbstractControl, prev: number): void {
    if (control.value.length < 1) {
      this._focusMonitor.focusVia(this.inputs.find((e,i) => i == prev), 'program');
    }
  }

  ngOnDestroy() {
    this.stateChanges.complete();
    this._focusMonitor.stopMonitoring(this._elementRef);
  }

  setDescribedByIds(ids: string[]) {
    const controlElement = this._elementRef.nativeElement
      .querySelector('.example-tel-input-container')!;
    controlElement.setAttribute('aria-describedby', ids.join(' '));
  }

  onContainerClick() {
    Object.values(this.parts.controls).some((e, i) => {
      if (!e.valid) {
        this._focusMonitor.focusVia(this.inputs.toArray()[i], 'program');
        return true;
      }
    })
  }

  writeValue(element: string[] | null): void {
    this.value = element;
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  _handleInput(control: AbstractControl, next?: number): void {
    this.autoFocusNext(control, next);
    this.onChange(this.value);
    this.event.emit(this.value);
  }

  _handleBlur(target, index) {
    if (target.errors && target.errors.minlength && target.errors.minlength.requiredLength) {
      const obj = {}
      obj[this.controlName(index)] = this.pad(target.value, '0', target.errors.minlength.requiredLength)
      this.parts.patchValue(obj)
      this.event.emit(this.value);
    }
  }

  pad(base, chr, num) {
    return ( chr.repeat(num) + base ).slice( -num )
  }

  numberOnly(event): boolean {
    const charCode = (event.which) ? event.which : event.keyCode;
    if (charCode > 31 && (charCode < 48 || charCode > 57)) {
      return false;
    }
    return true;
  }

  static ngAcceptInputType_disabled: boolean | string | null | undefined;
  static ngAcceptInputType_required: boolean | string | null | undefined;
}

jp-date-adapter.ts

import { NativeDateAdapter } from '@angular/material/core';

// https://github.com/angular/components/blob/master/src/material/core/datetime/native-date-adapter.ts
// https://github.com/angular/components/blob/master/src/material/datepicker/calendar.ts
// https://github.com/angular/components/blob/master/src/material/core/datetime/native-date-formats.ts
export class JpDateAdapter extends NativeDateAdapter {
    format(date: Date, displayFormat: any): string {
      if (displayFormat.year && displayFormat.month && !displayFormat.day) {
        return `${this.getYearName(date)}${this.pad(date.getMonth()+1,'0',2)}月`
      }
      return super.format(date, displayFormat)
    }

    getYearName(date: Date): string {
      const era = this.getEra(date)
      return `${era.name}${era.year}年`
    }

    getDateNames(): string[] {
      const dateNames: string[] = [];
      for (let i = 0; i < 31; i++) {
        dateNames[i] = String(i + 1);
      }
      return dateNames;
    }

    getEra(date) {
      const year = date.getFullYear();
      if (year > 2018) {
        return {name:'令和', year: this.pad(year - 2018, '0', 2)}
      } else if (year > 1988) {
        return {name:'平成', year: this.pad(year - 1988, '0', 2)}
      } else if (year > 1925) {
        return {name:'昭和', year: this.pad(year - 1925, '0', 2)}
      } else if (year > 1911) {
        return {name:'大正', year: this.pad(year - 1911, '0', 2)}
      } else if (year > 1867) {
        return {name:'明治', year: this.pad(year - 1867, '0', 2)}
      }
      return {name:'西暦', year: this.pad(year, '0', 4)}
    }

    pad(base, chr, num) {
      return ( chr.repeat(num) + base ).slice( -num )
    }
  }
タイトルとURLをコピーしました