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 )
}
}