Skip to content

Commit b8fba33

Browse files
author
Wildhammer
authored
docs(material/form-field-custom-control): custom phone input autoadvance (#19650)
* feat(material/form-field-custom-control): custom phone input autoadvance Automatically focus the next input when done with the previous one. Allow autofocusing backward when pressing backspace. onContainerClick now focuses on the last filled input on click. Using maxLength on the inputs to not allow more characters than the size of the input. Fixes #13195 * style(material/form-field-custom-control): styling update for phone number input Updates styling for form-field-custom-control-example component, multi-line elements in template due to them getting long, argument spacing. Fixes #13195 * style(material/form-field-custom-control): removes unnecessary !! from autoFocusNext * Merge remote-tracking branch 'upstream/master' into phone-input-auto-advance
1 parent 4d9745e commit b8fba33

File tree

3 files changed

+122
-48
lines changed

3 files changed

+122
-48
lines changed
Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
<div [formGroup]="parts" class="example-tel-input-container">
2-
<input
3-
class="example-tel-input-element"
4-
formControlName="area"
5-
size="3"
6-
aria-label="Area code"
7-
(input)="_handleInput()">
2+
<input class="example-tel-input-element"
3+
formControlName="area" size="3"
4+
maxLength="3"
5+
aria-label="Area code"
6+
(input)="_handleInput(parts.controls.area, exchange)"
7+
#area>
88
<span class="example-tel-input-spacer">&ndash;</span>
9-
<input
10-
class="example-tel-input-element"
11-
formControlName="exchange"
12-
size="3"
13-
aria-label="Exchange code"
14-
(input)="_handleInput()">
9+
<input class="example-tel-input-element"
10+
formControlName="exchange"
11+
maxLength="3"
12+
size="3"
13+
aria-label="Exchange code"
14+
(input)="_handleInput(parts.controls.exchange, subscriber)"
15+
(keyup.backspace)="autoFocusPrev(parts.controls.exchange, area)"
16+
#exchange>
1517
<span class="example-tel-input-spacer">&ndash;</span>
16-
<input
17-
class="example-tel-input-element"
18-
formControlName="subscriber"
19-
size="4"
20-
aria-label="Subscriber number"
21-
(input)="_handleInput()">
18+
<input class="example-tel-input-element"
19+
formControlName="subscriber"
20+
maxLength="4"
21+
size="4"
22+
aria-label="Subscriber number"
23+
(input)="_handleInput(parts.controls.subscriber)"
24+
(keyup.backspace)="autoFocusPrev(parts.controls.subscriber, exchange)"
25+
#subscriber>
2226
</div>
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
<mat-form-field appearance="fill">
2-
<mat-label>Phone number</mat-label>
3-
<example-tel-input required></example-tel-input>
4-
<mat-icon matSuffix>phone</mat-icon>
5-
<mat-hint>Include area code</mat-hint>
6-
</mat-form-field>
1+
<div [formGroup]="form">
2+
<mat-form-field appearance="fill">
3+
<mat-label>Phone number</mat-label>
4+
<example-tel-input formControlName="tel" required></example-tel-input>
5+
<mat-icon matSuffix>phone</mat-icon>
6+
<mat-hint>Include area code</mat-hint>
7+
</mat-form-field>
8+
</div>

src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,65 @@
11
import {FocusMonitor} from '@angular/cdk/a11y';
22
import {coerceBooleanProperty} from '@angular/cdk/coercion';
3-
import {Component, ElementRef, Input, OnDestroy, Optional, Self} from '@angular/core';
4-
import {FormBuilder, FormGroup, ControlValueAccessor, NgControl, Validators} from '@angular/forms';
3+
import {
4+
Component,
5+
ElementRef,
6+
Input,
7+
OnDestroy,
8+
Optional,
9+
Self,
10+
ViewChild
11+
} from '@angular/core';
12+
import {
13+
FormBuilder,
14+
FormGroup,
15+
ControlValueAccessor,
16+
NgControl,
17+
Validators,
18+
FormControl,
19+
AbstractControl
20+
} from '@angular/forms';
521
import {MatFormFieldControl} from '@angular/material/form-field';
622
import {Subject} from 'rxjs';
723

824
/** @title Form field with custom telephone number input control. */
925
@Component({
1026
selector: 'form-field-custom-control-example',
1127
templateUrl: 'form-field-custom-control-example.html',
12-
styleUrls: ['form-field-custom-control-example.css'],
28+
styleUrls: ['form-field-custom-control-example.css']
1329
})
14-
export class FormFieldCustomControlExample {}
30+
export class FormFieldCustomControlExample {
31+
form: FormGroup = new FormGroup({
32+
tel: new FormControl(new MyTel('', '', ''))
33+
});
34+
}
1535

1636
/** Data structure for holding telephone number. */
1737
export class MyTel {
18-
constructor(public area: string, public exchange: string, public subscriber: string) {}
38+
constructor(
39+
public area: string,
40+
public exchange: string,
41+
public subscriber: string
42+
) {}
1943
}
2044

2145
/** Custom `MatFormFieldControl` for telephone number input. */
2246
@Component({
2347
selector: 'example-tel-input',
2448
templateUrl: 'example-tel-input-example.html',
2549
styleUrls: ['example-tel-input-example.css'],
26-
providers: [{provide: MatFormFieldControl, useExisting: MyTelInput}],
50+
providers: [{ provide: MatFormFieldControl, useExisting: MyTelInput }],
2751
host: {
2852
'[class.example-floating]': 'shouldLabelFloat',
2953
'[id]': 'id',
30-
'[attr.aria-describedby]': 'describedBy',
54+
'[attr.aria-describedby]': 'describedBy'
3155
}
3256
})
33-
export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyTel>, OnDestroy {
57+
export class MyTelInput
58+
implements ControlValueAccessor, MatFormFieldControl<MyTel>, OnDestroy {
3459
static nextId = 0;
60+
@ViewChild('area') areaInput: HTMLInputElement;
61+
@ViewChild('exchange') exchangeInput: HTMLInputElement;
62+
@ViewChild('subscriber') subscriberInput: HTMLInputElement;
3563

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

4674
get empty() {
47-
const {value: {area, exchange, subscriber}} = this.parts;
75+
const {
76+
value: { area, exchange, subscriber }
77+
} = this.parts;
4878

4979
return !area && !exchange && !subscriber;
5080
}
5181

52-
get shouldLabelFloat() { return this.focused || !this.empty; }
82+
get shouldLabelFloat() {
83+
return this.focused || !this.empty;
84+
}
5385

5486
@Input()
55-
get placeholder(): string { return this._placeholder; }
87+
get placeholder(): string {
88+
return this._placeholder;
89+
}
5690
set placeholder(value: string) {
5791
this._placeholder = value;
5892
this.stateChanges.next();
5993
}
6094
private _placeholder: string;
6195

6296
@Input()
63-
get required(): boolean { return this._required; }
97+
get required(): boolean {
98+
return this._required;
99+
}
64100
set required(value: boolean) {
65101
this._required = coerceBooleanProperty(value);
66102
this.stateChanges.next();
67103
}
68104
private _required = false;
69105

70106
@Input()
71-
get disabled(): boolean { return this._disabled; }
107+
get disabled(): boolean {
108+
return this._disabled;
109+
}
72110
set disabled(value: boolean) {
73111
this._disabled = coerceBooleanProperty(value);
74112
this._disabled ? this.parts.disable() : this.parts.enable();
@@ -79,27 +117,38 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
79117
@Input()
80118
get value(): MyTel | null {
81119
if (this.parts.valid) {
82-
const {value: {area, exchange, subscriber}} = this.parts;
120+
const {
121+
value: { area, exchange, subscriber }
122+
} = this.parts;
83123
return new MyTel(area, exchange, subscriber);
84124
}
85125
return null;
86126
}
87127
set value(tel: MyTel | null) {
88-
const {area, exchange, subscriber} = tel || new MyTel('', '', '');
89-
this.parts.setValue({area, exchange, subscriber});
128+
const { area, exchange, subscriber } = tel || new MyTel('', '', '');
129+
this.parts.setValue({ area, exchange, subscriber });
90130
this.stateChanges.next();
91131
}
92132

93133
constructor(
94134
formBuilder: FormBuilder,
95135
private _focusMonitor: FocusMonitor,
96136
private _elementRef: ElementRef<HTMLElement>,
97-
@Optional() @Self() public ngControl: NgControl) {
98-
137+
@Optional() @Self() public ngControl: NgControl
138+
) {
99139
this.parts = formBuilder.group({
100-
area: [null, [Validators.required, Validators.minLength(3), Validators.maxLength(3)]],
101-
exchange: [null, [Validators.required, Validators.minLength(3), Validators.maxLength(3)]],
102-
subscriber: [null, [Validators.required, Validators.minLength(4), Validators.maxLength(4)]],
140+
area: [
141+
null,
142+
[Validators.required, Validators.minLength(3), Validators.maxLength(3)]
143+
],
144+
exchange: [
145+
null,
146+
[Validators.required, Validators.minLength(3), Validators.maxLength(3)]
147+
],
148+
subscriber: [
149+
null,
150+
[Validators.required, Validators.minLength(4), Validators.maxLength(4)]
151+
]
103152
});
104153

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

167+
autoFocusNext(control: AbstractControl, nextElement?: HTMLInputElement): void {
168+
if (!control.errors && nextElement) {
169+
this._focusMonitor.focusVia(nextElement, 'program');
170+
}
171+
}
172+
173+
autoFocusPrev(control: AbstractControl, prevElement: HTMLInputElement): void {
174+
if (control.value.length < 1) {
175+
this._focusMonitor.focusVia(prevElement, 'program');
176+
}
177+
}
178+
118179
ngOnDestroy() {
119180
this.stateChanges.complete();
120181
this._focusMonitor.stopMonitoring(this._elementRef);
@@ -125,8 +186,14 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl<MyT
125186
}
126187

127188
onContainerClick(event: MouseEvent) {
128-
if ((event.target as Element).tagName.toLowerCase() != 'input') {
129-
this._elementRef.nativeElement.querySelector('input')!.focus();
189+
if (this.parts.controls.subscriber.valid) {
190+
this._focusMonitor.focusVia(this.subscriberInput, 'program');
191+
} else if (this.parts.controls.exchange.valid) {
192+
this._focusMonitor.focusVia(this.subscriberInput, 'program');
193+
} else if (this.parts.controls.area.valid) {
194+
this._focusMonitor.focusVia(this.exchangeInput, 'program');
195+
} else {
196+
this._focusMonitor.focusVia(this.areaInput, 'program');
130197
}
131198
}
132199

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

149-
_handleInput(): void {
216+
_handleInput(control: AbstractControl, nextElement?: HTMLInputElement): void {
217+
this.autoFocusNext(control, nextElement);
150218
this.onChange(this.value);
151219
}
152220

0 commit comments

Comments
 (0)