Skip to content

feat(material/form-field-custom-control): custom phone input autoadvance #19650

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
<div [formGroup]="parts" class="example-tel-input-container">
<input
class="example-tel-input-element"
formControlName="area"
size="3"
aria-label="Area code"
(input)="_handleInput()">
<input class="example-tel-input-element"
formControlName="area" size="3"
maxLength="3"
aria-label="Area code"
(input)="_handleInput(parts.controls.area, exchange)"
#area>
<span class="example-tel-input-spacer">&ndash;</span>
<input
class="example-tel-input-element"
formControlName="exchange"
size="3"
aria-label="Exchange code"
(input)="_handleInput()">
<input class="example-tel-input-element"
formControlName="exchange"
maxLength="3"
size="3"
aria-label="Exchange code"
(input)="_handleInput(parts.controls.exchange, subscriber)"
(keyup.backspace)="autoFocusPrev(parts.controls.exchange, area)"
#exchange>
<span class="example-tel-input-spacer">&ndash;</span>
<input
class="example-tel-input-element"
formControlName="subscriber"
size="4"
aria-label="Subscriber number"
(input)="_handleInput()">
<input class="example-tel-input-element"
formControlName="subscriber"
maxLength="4"
size="4"
aria-label="Subscriber number"
(input)="_handleInput(parts.controls.subscriber)"
(keyup.backspace)="autoFocusPrev(parts.controls.subscriber, exchange)"
#subscriber>
</div>
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<mat-form-field appearance="fill">
<mat-label>Phone number</mat-label>
<example-tel-input required></example-tel-input>
<mat-icon matSuffix>phone</mat-icon>
<mat-hint>Include area code</mat-hint>
</mat-form-field>
<div [formGroup]="form">
<mat-form-field appearance="fill">
<mat-label>Phone number</mat-label>
<example-tel-input formControlName="tel" required></example-tel-input>
<mat-icon matSuffix>phone</mat-icon>
<mat-hint>Include area code</mat-hint>
</mat-form-field>
</div>
Original file line number Diff line number Diff line change
@@ -1,37 +1,65 @@
import {FocusMonitor} from '@angular/cdk/a11y';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {Component, ElementRef, Input, OnDestroy, Optional, Self} from '@angular/core';
import {FormBuilder, FormGroup, ControlValueAccessor, NgControl, Validators} from '@angular/forms';
import {
Component,
ElementRef,
Input,
OnDestroy,
Optional,
Self,
ViewChild
} from '@angular/core';
import {
FormBuilder,
FormGroup,
ControlValueAccessor,
NgControl,
Validators,
FormControl,
AbstractControl
} from '@angular/forms';
import {MatFormFieldControl} from '@angular/material/form-field';
import {Subject} from 'rxjs';

/** @title Form field with custom telephone number input control. */
@Component({
selector: 'form-field-custom-control-example',
templateUrl: 'form-field-custom-control-example.html',
styleUrls: ['form-field-custom-control-example.css'],
styleUrls: ['form-field-custom-control-example.css']
})
export class FormFieldCustomControlExample {}
export class FormFieldCustomControlExample {
form: FormGroup = new FormGroup({
tel: new FormControl(new MyTel('', '', ''))
});
}

/** Data structure for holding telephone number. */
export class MyTel {
constructor(public area: string, public exchange: string, public subscriber: string) {}
constructor(
public area: string,
public exchange: string,
public subscriber: string
) {}
}

/** Custom `MatFormFieldControl` for telephone number input. */
@Component({
selector: 'example-tel-input',
templateUrl: 'example-tel-input-example.html',
styleUrls: ['example-tel-input-example.css'],
providers: [{provide: MatFormFieldControl, useExisting: MyTelInput}],
providers: [{ provide: MatFormFieldControl, useExisting: MyTelInput }],
host: {
'[class.example-floating]': 'shouldLabelFloat',
'[id]': 'id',
'[attr.aria-describedby]': 'describedBy',
'[attr.aria-describedby]': 'describedBy'
}
})
export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyTel>, OnDestroy {
export class MyTelInput
implements ControlValueAccessor, MatFormFieldControl<MyTel>, OnDestroy {
static nextId = 0;
@ViewChild('area') areaInput: HTMLInputElement;
@ViewChild('exchange') exchangeInput: HTMLInputElement;
@ViewChild('subscriber') subscriberInput: HTMLInputElement;

parts: FormGroup;
stateChanges = new Subject<void>();
Expand All @@ -44,31 +72,41 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
onTouched = () => {};

get empty() {
const {value: {area, exchange, subscriber}} = this.parts;
const {
value: { area, exchange, subscriber }
} = this.parts;

return !area && !exchange && !subscriber;
}

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

@Input()
get placeholder(): string { return this._placeholder; }
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; }
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; }
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = coerceBooleanProperty(value);
this._disabled ? this.parts.disable() : this.parts.enable();
Expand All @@ -79,27 +117,38 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
@Input()
get value(): MyTel | null {
if (this.parts.valid) {
const {value: {area, exchange, subscriber}} = this.parts;
const {
value: { area, exchange, subscriber }
} = this.parts;
return new MyTel(area, exchange, subscriber);
}
return null;
}
set value(tel: MyTel | null) {
const {area, exchange, subscriber} = tel || new MyTel('', '', '');
this.parts.setValue({area, exchange, subscriber});
const { area, exchange, subscriber } = tel || new MyTel('', '', '');
this.parts.setValue({ area, exchange, subscriber });
this.stateChanges.next();
}

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

@Optional() @Self() public ngControl: NgControl
) {
this.parts = formBuilder.group({
area: [null, [Validators.required, Validators.minLength(3), Validators.maxLength(3)]],
exchange: [null, [Validators.required, Validators.minLength(3), Validators.maxLength(3)]],
subscriber: [null, [Validators.required, Validators.minLength(4), Validators.maxLength(4)]],
area: [
null,
[Validators.required, Validators.minLength(3), Validators.maxLength(3)]
],
exchange: [
null,
[Validators.required, Validators.minLength(3), Validators.maxLength(3)]
],
subscriber: [
null,
[Validators.required, Validators.minLength(4), Validators.maxLength(4)]
]
});

_focusMonitor.monitor(_elementRef, true).subscribe(origin => {
Expand All @@ -115,6 +164,18 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
}
}

autoFocusNext(control: AbstractControl, nextElement?: HTMLInputElement): void {
if (!control.errors && nextElement) {
this._focusMonitor.focusVia(nextElement, 'program');
}
}

autoFocusPrev(control: AbstractControl, prevElement: HTMLInputElement): void {
if (control.value.length < 1) {
this._focusMonitor.focusVia(prevElement, 'program');
}
}

ngOnDestroy() {
this.stateChanges.complete();
this._focusMonitor.stopMonitoring(this._elementRef);
Expand All @@ -125,8 +186,14 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
}

onContainerClick(event: MouseEvent) {
if ((event.target as Element).tagName.toLowerCase() != 'input') {
this._elementRef.nativeElement.querySelector('input')!.focus();
if (this.parts.controls.subscriber.valid) {
this._focusMonitor.focusVia(this.subscriberInput, 'program');
} else if (this.parts.controls.exchange.valid) {
this._focusMonitor.focusVia(this.subscriberInput, 'program');
} else if (this.parts.controls.area.valid) {
this._focusMonitor.focusVia(this.exchangeInput, 'program');
} else {
this._focusMonitor.focusVia(this.areaInput, 'program');
}
}

Expand All @@ -146,7 +213,8 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
this.disabled = isDisabled;
}

_handleInput(): void {
_handleInput(control: AbstractControl, nextElement?: HTMLInputElement): void {
this.autoFocusNext(control, nextElement);
this.onChange(this.value);
}

Expand Down