Skip to content

Commit c29f8ca

Browse files
crisbetotinayuangao
authored andcommitted
feat(input): add directive for displaying error messages (#3560)
* feat(input): add directive for displayed error messages Adds the `md-error` directive that can be utilised to display validation errors to the user. Example: ``` <md-input-container> <input mdInput placeholder="email" [formControl]="emailFormControl"> <md-error *ngIf="emailFormControl.errors.required">This field is required</md-error> <md-error *ngIf="emailFormControl.errors.pattern"> Please enter a valid email address </md-error> </md-input-container> ``` The `md-input-container` behavior is as follows: * If there is an error and the user interacted with the input, the errors will be shown. * If there is an error and the user submitted the a form that wraps the input, the errors will be shown. * If there are errors to be shown on an input container that has `md-hint`-s, the hints will be hidden. * If an input with hints and errors becomes valid, the hint won't be displayed anymore. **Note:** At the moment, all hints will be hidden when an error is shown. This might not be intended for some cases (e.g. with a character counter). It might make sense to only hide the one in the `start` slot, but this could be addressed separately. * chore: lint and aot errors * fix: address feedback * fix: address more feedback * chore: use FormGroup properly * refactor: switch back to showing hints when valid * errror
1 parent e482cb6 commit c29f8ca

11 files changed

+391
-34
lines changed

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,51 @@
5151
</md-card-content>
5252
</md-card>
5353

54+
<md-card class="demo-card demo-basic">
55+
<md-toolbar color="primary">Error messages</md-toolbar>
56+
<md-card-content>
57+
<h4>Regular</h4>
58+
59+
<p>
60+
<md-input-container>
61+
<input mdInput placeholder="example" [(ngModel)]="errorMessageExample1" required>
62+
<md-error>This field is required</md-error>
63+
</md-input-container>
64+
65+
<md-input-container>
66+
<input mdInput placeholder="email" [formControl]="emailFormControl">
67+
<md-error *ngIf="emailFormControl.hasError('required')">
68+
This field is required
69+
</md-error>
70+
<md-error *ngIf="emailFormControl.hasError('pattern')">
71+
Please enter a valid email address
72+
</md-error>
73+
</md-input-container>
74+
</p>
75+
76+
<h4>With hint</h4>
77+
78+
<md-input-container>
79+
<input mdInput placeholder="example" [(ngModel)]="errorMessageExample2" required>
80+
<md-error>This field is required</md-error>
81+
<md-hint>Please type something here</md-hint>
82+
</md-input-container>
83+
84+
85+
<form novalidate>
86+
<h4>Inside a form</h4>
87+
88+
<md-input-container>
89+
<input mdInput name="example" placeholder="example"
90+
[(ngModel)]="errorMessageExample3" required>
91+
<md-error>This field is required</md-error>
92+
</md-input-container>
93+
94+
<button color="primary" md-raised-button>Submit</button>
95+
</form>
96+
</md-card-content>
97+
</md-card>
98+
5499
<md-card class="demo-card demo-basic">
55100
<md-toolbar color="primary">Prefix + Suffix</md-toolbar>
56101
<md-card-content>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {FormControl, Validators} from '@angular/forms';
44

55
let max = 5;
66

7+
const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
8+
79
@Component({
810
moduleId: module.id,
911
selector: 'input-demo',
@@ -17,6 +19,9 @@ export class InputDemo {
1719
ctrlDisabled = false;
1820

1921
name: string;
22+
errorMessageExample1: string;
23+
errorMessageExample2: string;
24+
errorMessageExample3: string;
2025
items: any[] = [
2126
{ value: 10 },
2227
{ value: 20 },
@@ -26,6 +31,7 @@ export class InputDemo {
2631
];
2732
rows = 8;
2833
formControl = new FormControl('hello', Validators.required);
34+
emailFormControl = new FormControl('', [Validators.required, Validators.pattern(EMAIL_REGEX)]);
2935
model = 'hello';
3036

3137
addABunch(n: number) {

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {TestBed, async, fakeAsync, tick, ComponentFixture} from '@angular/core/testing';
22
import {Component, OnDestroy, QueryList, ViewChild, ViewChildren} from '@angular/core';
33
import {By} from '@angular/platform-browser';
4+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
45
import {MdAutocompleteModule, MdAutocompleteTrigger} from './index';
56
import {OverlayContainer} from '../core/overlay/overlay-container';
67
import {MdInputModule} from '../input/index';
@@ -27,7 +28,11 @@ describe('MdAutocomplete', () => {
2728
dir = 'ltr';
2829
TestBed.configureTestingModule({
2930
imports: [
30-
MdAutocompleteModule.forRoot(), MdInputModule.forRoot(), FormsModule, ReactiveFormsModule
31+
MdAutocompleteModule.forRoot(),
32+
MdInputModule.forRoot(),
33+
FormsModule,
34+
ReactiveFormsModule,
35+
NoopAnimationsModule
3136
],
3237
declarations: [
3338
SimpleAutocomplete,

src/lib/core/compatibility/compatibility.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ export const MAT_ELEMENTS_SELECTOR = `
7070
mat-spinner,
7171
mat-tab,
7272
mat-tab-group,
73-
mat-toolbar`;
73+
mat-toolbar,
74+
mat-error`;
7475

7576
/** Selector that matches all elements that may have style collisions with AngularJS Material. */
7677
export const MD_ELEMENTS_SELECTOR = `
@@ -130,7 +131,8 @@ export const MD_ELEMENTS_SELECTOR = `
130131
md-spinner,
131132
md-tab,
132133
md-tab-group,
133-
md-toolbar`;
134+
md-toolbar,
135+
md-error`;
134136

135137
/** Directive that enforces that the `mat-` prefix cannot be used. */
136138
@Directive({selector: MAT_ELEMENTS_SELECTOR})

src/lib/input/_input-theme.scss

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
$warn: map-get($theme, warn);
99
$background: map-get($theme, background);
1010
$foreground: map-get($theme, foreground);
11-
11+
1212
// Placeholder colors. Required is used for the `*` star shown in the placeholder.
1313
$input-placeholder-color: mat-color($foreground, hint-text);
1414
$input-floating-placeholder-color: mat-color($primary);
1515
$input-required-placeholder-color: mat-color($accent);
16-
16+
1717
// Underline colors.
1818
$input-underline-color: mat-color($foreground, divider);
1919
$input-underline-color-accent: mat-color($accent);
@@ -64,7 +64,10 @@
6464
}
6565
}
6666

67-
.mat-input-container.ng-invalid.ng-touched:not(.mat-focused) {
67+
// Styling for the error state of the input container. Note that while the same can be
68+
// achieved with the ng-* classes, we use this approach in order to ensure that the same
69+
// logic is used to style the error state and to show the error messages.
70+
.mat-input-invalid {
6871
.mat-input-placeholder,
6972
.mat-placeholder-required {
7073
color: $input-underline-color-warn;
@@ -73,5 +76,13 @@
7376
.mat-input-underline {
7477
border-color: $input-underline-color-warn;
7578
}
79+
80+
.mat-input-ripple {
81+
background-color: $input-underline-color-warn;
82+
}
83+
}
84+
85+
.mat-input-error {
86+
color: $input-underline-color-warn;
7687
}
7788
}

src/lib/input/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import {NgModule, ModuleWithProviders} from '@angular/core';
2-
import {MdPlaceholder, MdInputContainer, MdHint, MdInputDirective} from './input-container';
2+
import {
3+
MdPlaceholder,
4+
MdInputContainer,
5+
MdHint,
6+
MdInputDirective,
7+
MdErrorDirective,
8+
} from './input-container';
39
import {MdTextareaAutosize} from './autosize';
410
import {CommonModule} from '@angular/common';
511
import {FormsModule} from '@angular/forms';
@@ -12,7 +18,8 @@ import {PlatformModule} from '../core/platform/index';
1218
MdInputContainer,
1319
MdHint,
1420
MdTextareaAutosize,
15-
MdInputDirective
21+
MdInputDirective,
22+
MdErrorDirective
1623
],
1724
imports: [
1825
CommonModule,
@@ -24,7 +31,8 @@ import {PlatformModule} from '../core/platform/index';
2431
MdInputContainer,
2532
MdHint,
2633
MdTextareaAutosize,
27-
MdInputDirective
34+
MdInputDirective,
35+
MdErrorDirective
2836
],
2937
})
3038
export class MdInputModule {

src/lib/input/input-container.html

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@
3636
[class.mat-warn]="dividerColor == 'warn'"></span>
3737
</div>
3838

39-
<div *ngIf="hintLabel != ''" [attr.id]="_hintLabelId" class="mat-hint">{{hintLabel}}</div>
40-
<ng-content select="md-hint, mat-hint"></ng-content>
39+
<div class="mat-input-subscript-wrapper" [ngSwitch]="_getDisplayedMessages()">
40+
<div *ngSwitchCase="'error'" [@transitionMessages]="_subscriptAnimationState">
41+
<ng-content select="md-error, mat-error"></ng-content>
42+
</div>
43+
44+
<div class="mat-input-hint-wrapper" *ngSwitchCase="'hint'"
45+
[@transitionMessages]="_subscriptAnimationState">
46+
<div *ngIf="hintLabel" [id]="_hintLabelId" class="mat-hint">{{hintLabel}}</div>
47+
<ng-content select="md-hint, mat-hint"></ng-content>
48+
</div>
49+
</div>
4150
</div>

src/lib/input/input-container.scss

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55

66
$mat-input-floating-placeholder-scale-factor: 0.75 !default;
7+
$mat-input-wrapper-spacing: 1em !default;
78

89
// Gradient for showing the dashed line when the input is disabled.
910
$mat-input-underline-disabled-background-image:
@@ -41,7 +42,7 @@ $mat-input-underline-disabled-background-image:
4142
// Global wrapper. We need to apply margin to the element for spacing, but
4243
// cannot apply it to the host element directly.
4344
.mat-input-wrapper {
44-
margin: 1em 0;
45+
margin: $mat-input-wrapper-spacing 0;
4546
// Account for the underline which has 4px of margin + 2px of border.
4647
padding-bottom: 6px;
4748
}
@@ -219,29 +220,57 @@ $mat-input-underline-disabled-background-image:
219220
}
220221
}
221222

222-
// The hint is shown below the underline. There can be more than one; one at the start
223-
// and one at the end.
224-
.mat-hint {
225-
display: block;
223+
// Wrapper for the hints and error messages. Provides positioning and text size.
224+
// Note that we're using `top` in order to allow for stacked children to flow downwards.
225+
.mat-input-subscript-wrapper {
226226
position: absolute;
227227
font-size: 75%;
228-
bottom: 0;
228+
top: 100%;
229+
width: 100%;
230+
margin-top: -$mat-input-wrapper-spacing;
231+
overflow: hidden; // prevents multi-line errors from overlapping the input
232+
}
233+
234+
// Clears the floats on the hints. This is necessary for the hint animation to work.
235+
.mat-input-hint-wrapper {
236+
&::before,
237+
&::after {
238+
content: ' ';
239+
display: table;
240+
}
241+
242+
&::after {
243+
clear: both;
244+
}
245+
}
229246

247+
// The hint is shown below the underline. There can be
248+
// more than one; one at the start and one at the end.
249+
.mat-hint {
250+
display: block;
251+
float: left;
252+
253+
// We use floats here, as opposed to flexbox, in order to make it
254+
// easier to reverse their location in rtl and to ensure that they're
255+
// aligned properly in some cases (e.g. when there is only an `end` hint).
230256
&.mat-right {
231-
right: 0;
257+
float: right;
232258
}
233259

234260
[dir='rtl'] & {
235-
right: 0;
236-
left: auto;
261+
float: right;
237262

238263
&.mat-right {
239-
right: auto;
240-
left: 0;
264+
float: left;
241265
}
242266
}
243267
}
244268

269+
// Single error message displayed beneath the input.
270+
.mat-input-error {
271+
display: block;
272+
}
273+
245274
.mat-input-prefix, .mat-input-suffix {
246275
// Prevents the prefix and suffix from stretching together with the container.
247276
width: 0.1px;

0 commit comments

Comments
 (0)