Skip to content

Commit d6fec35

Browse files
crisbetotinayuangao
authored andcommitted
feat(form-field): add support for separate label and placeholder (#8223)
* feat(form-field): add support for separate label and placeholder * Adds the ability for the user to specify both a label and placeholder text for a form field. * Renames all of the "floating placeholder" related terminology to refer to a "floating label" instead. * Aligns the input placeholder color with the Material spec. Fixes #6194. * fix: add transition to placeholder text
1 parent 65cd1a1 commit d6fec35

40 files changed

+617
-314
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
/src/lib/core/line/** @jelbourn
4242
/src/lib/core/option/** @kara @crisbeto
4343
/src/lib/core/placeholder/** @kara @mmalerba
44+
/src/lib/core/label/** @kara @mmalerba
4445
/src/lib/core/ripple/** @devversion
4546
/src/lib/core/selection/** @tinayuangao @jelbourn
4647
/src/lib/core/selection/pseudo*/** @crisbeto @jelbourn

guides/creating-a-custom-form-field-control.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,16 +211,16 @@ get empty() {
211211
}
212212
```
213213

214-
#### `shouldPlaceholderFloat`
214+
#### `shouldLabelFloat`
215215

216-
This property is used to indicate whether the placeholder should be in the floating position. We'll
216+
This property is used to indicate whether the label should be in the floating position. We'll
217217
use the same logic as `matInput` and float the placeholder when the input is focused or non-empty.
218218
Since the placeholder will be overlapping our control when when it's not floating, we should hide
219219
the `` characters when it's not floating.
220220

221221
```ts
222222
@HostBinding('class.floating')
223-
get shouldPlaceholderFloat() {
223+
get shouldLabelFloat() {
224224
return this.focused || !this.empty;
225225
}
226226
```

src/demo-app/a11y/input/input-a11y.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<section>
22
<h2>Basic input box (e.g. name field)</h2>
3-
<mat-form-field floatPlaceholder="never">
3+
<mat-form-field floatLabel="never">
44
<input matInput placeholder="First name" [(ngModel)]="firstName">
55
</mat-form-field>
6-
<mat-form-field floatPlaceholder="never">
6+
<mat-form-field floatLabel="never">
77
<input matInput placeholder="Last name" [(ngModel)]="lastName">
88
</mat-form-field>
99
</section>
@@ -34,12 +34,12 @@ <h2>Input with error message (e.g. email field)</h2>
3434

3535
<section>
3636
<h2>Input with prefix & suffix (e.g. currency converter)</h2>
37-
<mat-form-field floatPlaceholder="always">
37+
<mat-form-field floatLabel="always">
3838
<input matInput type="number" placeholder="USD" [(ngModel)]="usd">
3939
<span matPrefix>$</span>
4040
</mat-form-field>
4141
=
42-
<mat-form-field floatPlaceholder="always">
42+
<mat-form-field floatLabel="always">
4343
<input matInput type="number" placeholder="JPY" [(ngModel)]="jpy">
4444
<span matPrefix>‎¥‎</span>
4545
</mat-form-field>

src/demo-app/autocomplete/autocomplete-demo.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<div>Reactive value: {{ stateCtrl.value | json }}</div>
77
<div>Reactive dirty: {{ stateCtrl.dirty }}</div>
88

9-
<mat-form-field floatPlaceholder="never">
9+
<mat-form-field floatLabel="never">
1010
<input matInput placeholder="State" [matAutocomplete]="reactiveAuto" [formControl]="stateCtrl">
1111
<mat-autocomplete #reactiveAuto="matAutocomplete" [displayWith]="displayFn">
1212
<mat-option *ngFor="let state of reactiveStates | async" [value]="state">

src/demo-app/input/input-demo.html

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,12 +384,115 @@ <h4>Textarea</h4>
384384
</button>
385385
<div>
386386
<mat-form-field>
387-
<input matInput placeholder="delayed value" [formControl]="delayedFormControl">
387+
<input matInput placeholder="delayed value" [formControl]="delayedFormControl">
388388
</mat-form-field>
389389
</div>
390390
</mat-card-content>
391391
</mat-card>
392392

393+
394+
<mat-card class="demo-card demo-basic">
395+
<mat-toolbar color="primary">Floating labels</mat-toolbar>
396+
397+
<mat-card-content>
398+
<div>
399+
<mat-form-field>
400+
<input matInput [formControl]="placeholderTestControl">
401+
</mat-form-field>
402+
403+
<mat-form-field>
404+
<input matInput [formControl]="placeholderTestControl" placeholder="Only placeholder">
405+
</mat-form-field>
406+
407+
<mat-form-field>
408+
<mat-label>Only label</mat-label>
409+
<input matInput [formControl]="placeholderTestControl">
410+
</mat-form-field>
411+
412+
<mat-form-field>
413+
<mat-label>Label and placeholder</mat-label>
414+
<input matInput [formControl]="placeholderTestControl" placeholder="This is the placeholder">
415+
</mat-form-field>
416+
417+
<mat-form-field floatLabel="always">
418+
<mat-label>Always float</mat-label>
419+
<input matInput [formControl]="placeholderTestControl" placeholder="This is the placeholder">
420+
</mat-form-field>
421+
422+
<mat-form-field floatLabel="never">
423+
<mat-label>Never float</mat-label>
424+
<input matInput [formControl]="placeholderTestControl" placeholder="This is the placeholder">
425+
</mat-form-field>
426+
427+
<mat-form-field>
428+
<mat-label>Label and placeholder element</mat-label>
429+
<mat-placeholder>The placeholder element</mat-placeholder>
430+
<input matInput [formControl]="placeholderTestControl">
431+
</mat-form-field>
432+
</div>
433+
434+
<div>
435+
<mat-form-field>
436+
<mat-select [formControl]="placeholderTestControl">
437+
<mat-option value="Value">Value</mat-option>
438+
</mat-select>
439+
</mat-form-field>
440+
441+
<mat-form-field>
442+
<mat-select [formControl]="placeholderTestControl" placeholder="Only placeholder">
443+
<mat-option value="Value">Value</mat-option>
444+
</mat-select>
445+
</mat-form-field>
446+
447+
<mat-form-field>
448+
<mat-label>Only label</mat-label>
449+
<mat-select [formControl]="placeholderTestControl">
450+
<mat-option value="Value">Value</mat-option>
451+
</mat-select>
452+
</mat-form-field>
453+
454+
<mat-form-field>
455+
<mat-label>Label and placeholder</mat-label>
456+
<mat-select [formControl]="placeholderTestControl" placeholder="This is the placeholder">
457+
<mat-option value="Value">Value</mat-option>
458+
</mat-select>
459+
</mat-form-field>
460+
461+
<mat-form-field floatLabel="always">
462+
<mat-label>Always float</mat-label>
463+
<mat-select [formControl]="placeholderTestControl" placeholder="This is the placeholder">
464+
<mat-option value="Value">Value</mat-option>
465+
</mat-select>
466+
</mat-form-field>
467+
468+
<mat-form-field floatLabel="never">
469+
<mat-label>Never float</mat-label>
470+
<mat-select [formControl]="placeholderTestControl" placeholder="This is the placeholder">
471+
<mat-option value="Value">Value</mat-option>
472+
</mat-select>
473+
</mat-form-field>
474+
475+
<mat-form-field>
476+
<mat-label>Label and placeholder element</mat-label>
477+
<mat-placeholder>The placeholder element</mat-placeholder>
478+
<mat-select [formControl]="placeholderTestControl">
479+
<mat-option value="Value">Value</mat-option>
480+
</mat-select>
481+
</mat-form-field>
482+
</div>
483+
484+
<button
485+
mat-raised-button
486+
color="primary"
487+
(click)="togglePlaceholderTestValue()">Toggle value</button>
488+
489+
<button
490+
mat-raised-button
491+
color="primary"
492+
(click)="togglePlaceholderTestTouched()">Toggle touched</button>
493+
</mat-card-content>
494+
</mat-card>
495+
393496
<mat-card class="demo-card demo-basic">
394497
<mat-toolbar color="primary">Textarea Autosize</mat-toolbar>
395498
<mat-card-content>

src/demo-app/input/input-demo.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class InputDemo {
2929
hideRequiredMarker: boolean;
3030
ctrlDisabled = false;
3131
textareaNgModelValue: string;
32+
placeholderTestControl = new FormControl('', Validators.required);
3233

3334
name: string;
3435
errorMessageExample1: string;
@@ -73,4 +74,14 @@ export class InputDemo {
7374
return false;
7475
}
7576
};
77+
78+
togglePlaceholderTestValue() {
79+
this.placeholderTestControl.setValue(this.placeholderTestControl.value === '' ? 'Value' : '');
80+
}
81+
82+
togglePlaceholderTestTouched() {
83+
this.placeholderTestControl.touched ?
84+
this.placeholderTestControl.markAsUntouched() :
85+
this.placeholderTestControl.markAsTouched();
86+
}
7687
}

src/demo-app/select/select-demo.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<mat-card>
77
<mat-card-subtitle>ngModel</mat-card-subtitle>
88

9-
<mat-form-field [floatPlaceholder]="floatPlaceholder" [color]="drinksTheme">
9+
<mat-form-field [floatLabel]="floatLabel" [color]="drinksTheme">
1010
<mat-select placeholder="Drink" [(ngModel)]="currentDrink" [required]="drinksRequired"
1111
[disabled]="drinksDisabled" #drinkControl="ngModel">
1212
<mat-option>None</mat-option>
@@ -24,7 +24,7 @@
2424
<p> Status: {{ drinkControl.control?.status }} </p>
2525
<p>
2626
<label for="floating-placeholder">Floating placeholder:</label>
27-
<select [(ngModel)]="floatPlaceholder" id="floating-placeholder">
27+
<select [(ngModel)]="floatLabel" id="floating-placeholder">
2828
<option value="auto">Auto</option>
2929
<option value="always">Always</option>
3030
<option value="never">Never</option>

src/demo-app/select/select-demo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class SelectDemo {
2929
currentPokemonFromGroup: string;
3030
currentDigimon: string;
3131
latestChangeEvent: MatSelectChange;
32-
floatPlaceholder: string = 'auto';
32+
floatLabel: string = 'auto';
3333
foodControl = new FormControl('pizza-1');
3434
topHeightCtrl = new FormControl(0);
3535
drinksTheme = 'primary';

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
123123
/** Strategy that is used to position the panel. */
124124
private _positionStrategy: ConnectedPositionStrategy;
125125

126-
/** Whether or not the placeholder state is being overridden. */
127-
private _manuallyFloatingPlaceholder = false;
126+
/** Whether or not the label state is being overridden. */
127+
private _manuallyFloatingLabel = false;
128128

129129
/** The subscription for closing actions (some are bound to document). */
130130
private _closingActionsSubscription: Subscription;
@@ -163,7 +163,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
163163
/** Opens the autocomplete suggestion panel. */
164164
openPanel(): void {
165165
this._attachOverlay();
166-
this._floatPlaceholder();
166+
this._floatLabel();
167167
}
168168

169169
/** Closes the autocomplete suggestion panel. */
@@ -173,14 +173,14 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
173173
this._closingActionsSubscription.unsubscribe();
174174
}
175175

176-
this._resetPlaceholder();
176+
this._resetLabel();
177177

178178
if (this._panelOpen) {
179179
this.autocomplete._isOpen = this._panelOpen = false;
180180

181181
// We need to trigger change detection manually, because
182182
// `fromEvent` doesn't seem to do it at the proper time.
183-
// This ensures that the placeholder is reset when the
183+
// This ensures that the label is reset when the
184184
// user clicks outside.
185185
this._changeDetectorRef.detectChanges();
186186
}
@@ -307,33 +307,33 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
307307
_handleFocus(): void {
308308
if (!this._element.nativeElement.readOnly) {
309309
this._attachOverlay();
310-
this._floatPlaceholder(true);
310+
this._floatLabel(true);
311311
}
312312
}
313313

314314
/**
315-
* In "auto" mode, the placeholder will animate down as soon as focus is lost.
315+
* In "auto" mode, the label will animate down as soon as focus is lost.
316316
* This causes the value to jump when selecting an option with the mouse.
317-
* This method manually floats the placeholder until the panel can be closed.
318-
* @param shouldAnimate Whether the placeholder should be animated when it is floated.
317+
* This method manually floats the label until the panel can be closed.
318+
* @param shouldAnimate Whether the label should be animated when it is floated.
319319
*/
320-
private _floatPlaceholder(shouldAnimate = false): void {
321-
if (this._formField && this._formField.floatPlaceholder === 'auto') {
320+
private _floatLabel(shouldAnimate = false): void {
321+
if (this._formField && this._formField.floatLabel === 'auto') {
322322
if (shouldAnimate) {
323-
this._formField._animateAndLockPlaceholder();
323+
this._formField._animateAndLockLabel();
324324
} else {
325-
this._formField.floatPlaceholder = 'always';
325+
this._formField.floatLabel = 'always';
326326
}
327327

328-
this._manuallyFloatingPlaceholder = true;
328+
this._manuallyFloatingLabel = true;
329329
}
330330
}
331331

332-
/** If the placeholder has been manually elevated, return it to its normal state. */
333-
private _resetPlaceholder(): void {
334-
if (this._manuallyFloatingPlaceholder) {
335-
this._formField.floatPlaceholder = 'auto';
336-
this._manuallyFloatingPlaceholder = false;
332+
/** If the label has been manually elevated, return it to its normal state. */
333+
private _resetLabel(): void {
334+
if (this._manuallyFloatingLabel) {
335+
this._formField.floatLabel = 'auto';
336+
this._manuallyFloatingLabel = false;
337337
}
338338
}
339339

0 commit comments

Comments
 (0)