mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
This commit is contained in:
committed by
tinayuangao
parent
e482cb6821
commit
c29f8ca44a
@@ -51,6 +51,51 @@
|
||||
</md-card-content>
|
||||
</md-card>
|
||||
|
||||
<md-card class="demo-card demo-basic">
|
||||
<md-toolbar color="primary">Error messages</md-toolbar>
|
||||
<md-card-content>
|
||||
<h4>Regular</h4>
|
||||
|
||||
<p>
|
||||
<md-input-container>
|
||||
<input mdInput placeholder="example" [(ngModel)]="errorMessageExample1" required>
|
||||
<md-error>This field is required</md-error>
|
||||
</md-input-container>
|
||||
|
||||
<md-input-container>
|
||||
<input mdInput placeholder="email" [formControl]="emailFormControl">
|
||||
<md-error *ngIf="emailFormControl.hasError('required')">
|
||||
This field is required
|
||||
</md-error>
|
||||
<md-error *ngIf="emailFormControl.hasError('pattern')">
|
||||
Please enter a valid email address
|
||||
</md-error>
|
||||
</md-input-container>
|
||||
</p>
|
||||
|
||||
<h4>With hint</h4>
|
||||
|
||||
<md-input-container>
|
||||
<input mdInput placeholder="example" [(ngModel)]="errorMessageExample2" required>
|
||||
<md-error>This field is required</md-error>
|
||||
<md-hint>Please type something here</md-hint>
|
||||
</md-input-container>
|
||||
|
||||
|
||||
<form novalidate>
|
||||
<h4>Inside a form</h4>
|
||||
|
||||
<md-input-container>
|
||||
<input mdInput name="example" placeholder="example"
|
||||
[(ngModel)]="errorMessageExample3" required>
|
||||
<md-error>This field is required</md-error>
|
||||
</md-input-container>
|
||||
|
||||
<button color="primary" md-raised-button>Submit</button>
|
||||
</form>
|
||||
</md-card-content>
|
||||
</md-card>
|
||||
|
||||
<md-card class="demo-card demo-basic">
|
||||
<md-toolbar color="primary">Prefix + Suffix</md-toolbar>
|
||||
<md-card-content>
|
||||
|
||||
@@ -4,6 +4,8 @@ import {FormControl, Validators} from '@angular/forms';
|
||||
|
||||
let max = 5;
|
||||
|
||||
const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'input-demo',
|
||||
@@ -17,6 +19,9 @@ export class InputDemo {
|
||||
ctrlDisabled = false;
|
||||
|
||||
name: string;
|
||||
errorMessageExample1: string;
|
||||
errorMessageExample2: string;
|
||||
errorMessageExample3: string;
|
||||
items: any[] = [
|
||||
{ value: 10 },
|
||||
{ value: 20 },
|
||||
@@ -26,6 +31,7 @@ export class InputDemo {
|
||||
];
|
||||
rows = 8;
|
||||
formControl = new FormControl('hello', Validators.required);
|
||||
emailFormControl = new FormControl('', [Validators.required, Validators.pattern(EMAIL_REGEX)]);
|
||||
model = 'hello';
|
||||
|
||||
addABunch(n: number) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {TestBed, async, fakeAsync, tick, ComponentFixture} from '@angular/core/testing';
|
||||
import {Component, OnDestroy, QueryList, ViewChild, ViewChildren} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MdAutocompleteModule, MdAutocompleteTrigger} from './index';
|
||||
import {OverlayContainer} from '../core/overlay/overlay-container';
|
||||
import {MdInputModule} from '../input/index';
|
||||
@@ -27,7 +28,11 @@ describe('MdAutocomplete', () => {
|
||||
dir = 'ltr';
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MdAutocompleteModule.forRoot(), MdInputModule.forRoot(), FormsModule, ReactiveFormsModule
|
||||
MdAutocompleteModule.forRoot(),
|
||||
MdInputModule.forRoot(),
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
declarations: [
|
||||
SimpleAutocomplete,
|
||||
|
||||
@@ -70,7 +70,8 @@ export const MAT_ELEMENTS_SELECTOR = `
|
||||
mat-spinner,
|
||||
mat-tab,
|
||||
mat-tab-group,
|
||||
mat-toolbar`;
|
||||
mat-toolbar,
|
||||
mat-error`;
|
||||
|
||||
/** Selector that matches all elements that may have style collisions with AngularJS Material. */
|
||||
export const MD_ELEMENTS_SELECTOR = `
|
||||
@@ -130,7 +131,8 @@ export const MD_ELEMENTS_SELECTOR = `
|
||||
md-spinner,
|
||||
md-tab,
|
||||
md-tab-group,
|
||||
md-toolbar`;
|
||||
md-toolbar,
|
||||
md-error`;
|
||||
|
||||
/** Directive that enforces that the `mat-` prefix cannot be used. */
|
||||
@Directive({selector: MAT_ELEMENTS_SELECTOR})
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
$warn: map-get($theme, warn);
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
|
||||
|
||||
// Placeholder colors. Required is used for the `*` star shown in the placeholder.
|
||||
$input-placeholder-color: mat-color($foreground, hint-text);
|
||||
$input-floating-placeholder-color: mat-color($primary);
|
||||
$input-required-placeholder-color: mat-color($accent);
|
||||
|
||||
|
||||
// Underline colors.
|
||||
$input-underline-color: mat-color($foreground, divider);
|
||||
$input-underline-color-accent: mat-color($accent);
|
||||
@@ -64,7 +64,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mat-input-container.ng-invalid.ng-touched:not(.mat-focused) {
|
||||
// Styling for the error state of the input container. Note that while the same can be
|
||||
// achieved with the ng-* classes, we use this approach in order to ensure that the same
|
||||
// logic is used to style the error state and to show the error messages.
|
||||
.mat-input-invalid {
|
||||
.mat-input-placeholder,
|
||||
.mat-placeholder-required {
|
||||
color: $input-underline-color-warn;
|
||||
@@ -73,5 +76,13 @@
|
||||
.mat-input-underline {
|
||||
border-color: $input-underline-color-warn;
|
||||
}
|
||||
|
||||
.mat-input-ripple {
|
||||
background-color: $input-underline-color-warn;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-input-error {
|
||||
color: $input-underline-color-warn;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -1,5 +1,11 @@
|
||||
import {NgModule, ModuleWithProviders} from '@angular/core';
|
||||
import {MdPlaceholder, MdInputContainer, MdHint, MdInputDirective} from './input-container';
|
||||
import {
|
||||
MdPlaceholder,
|
||||
MdInputContainer,
|
||||
MdHint,
|
||||
MdInputDirective,
|
||||
MdErrorDirective,
|
||||
} from './input-container';
|
||||
import {MdTextareaAutosize} from './autosize';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
@@ -12,7 +18,8 @@ import {PlatformModule} from '../core/platform/index';
|
||||
MdInputContainer,
|
||||
MdHint,
|
||||
MdTextareaAutosize,
|
||||
MdInputDirective
|
||||
MdInputDirective,
|
||||
MdErrorDirective
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -24,7 +31,8 @@ import {PlatformModule} from '../core/platform/index';
|
||||
MdInputContainer,
|
||||
MdHint,
|
||||
MdTextareaAutosize,
|
||||
MdInputDirective
|
||||
MdInputDirective,
|
||||
MdErrorDirective
|
||||
],
|
||||
})
|
||||
export class MdInputModule {
|
||||
|
||||
@@ -36,6 +36,15 @@
|
||||
[class.mat-warn]="dividerColor == 'warn'"></span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hintLabel != ''" [attr.id]="_hintLabelId" class="mat-hint">{{hintLabel}}</div>
|
||||
<ng-content select="md-hint, mat-hint"></ng-content>
|
||||
<div class="mat-input-subscript-wrapper" [ngSwitch]="_getDisplayedMessages()">
|
||||
<div *ngSwitchCase="'error'" [@transitionMessages]="_subscriptAnimationState">
|
||||
<ng-content select="md-error, mat-error"></ng-content>
|
||||
</div>
|
||||
|
||||
<div class="mat-input-hint-wrapper" *ngSwitchCase="'hint'"
|
||||
[@transitionMessages]="_subscriptAnimationState">
|
||||
<div *ngIf="hintLabel" [id]="_hintLabelId" class="mat-hint">{{hintLabel}}</div>
|
||||
<ng-content select="md-hint, mat-hint"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
|
||||
$mat-input-floating-placeholder-scale-factor: 0.75 !default;
|
||||
$mat-input-wrapper-spacing: 1em !default;
|
||||
|
||||
// Gradient for showing the dashed line when the input is disabled.
|
||||
$mat-input-underline-disabled-background-image:
|
||||
@@ -41,7 +42,7 @@ $mat-input-underline-disabled-background-image:
|
||||
// Global wrapper. We need to apply margin to the element for spacing, but
|
||||
// cannot apply it to the host element directly.
|
||||
.mat-input-wrapper {
|
||||
margin: 1em 0;
|
||||
margin: $mat-input-wrapper-spacing 0;
|
||||
// Account for the underline which has 4px of margin + 2px of border.
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
@@ -219,29 +220,57 @@ $mat-input-underline-disabled-background-image:
|
||||
}
|
||||
}
|
||||
|
||||
// The hint is shown below the underline. There can be more than one; one at the start
|
||||
// and one at the end.
|
||||
.mat-hint {
|
||||
display: block;
|
||||
// Wrapper for the hints and error messages. Provides positioning and text size.
|
||||
// Note that we're using `top` in order to allow for stacked children to flow downwards.
|
||||
.mat-input-subscript-wrapper {
|
||||
position: absolute;
|
||||
font-size: 75%;
|
||||
bottom: 0;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
margin-top: -$mat-input-wrapper-spacing;
|
||||
overflow: hidden; // prevents multi-line errors from overlapping the input
|
||||
}
|
||||
|
||||
// Clears the floats on the hints. This is necessary for the hint animation to work.
|
||||
.mat-input-hint-wrapper {
|
||||
&::before,
|
||||
&::after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
|
||||
&::after {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
// The hint is shown below the underline. There can be
|
||||
// more than one; one at the start and one at the end.
|
||||
.mat-hint {
|
||||
display: block;
|
||||
float: left;
|
||||
|
||||
// We use floats here, as opposed to flexbox, in order to make it
|
||||
// easier to reverse their location in rtl and to ensure that they're
|
||||
// aligned properly in some cases (e.g. when there is only an `end` hint).
|
||||
&.mat-right {
|
||||
right: 0;
|
||||
float: right;
|
||||
}
|
||||
|
||||
[dir='rtl'] & {
|
||||
right: 0;
|
||||
left: auto;
|
||||
float: right;
|
||||
|
||||
&.mat-right {
|
||||
right: auto;
|
||||
left: 0;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Single error message displayed beneath the input.
|
||||
.mat-input-error {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mat-input-prefix, .mat-input-suffix {
|
||||
// Prevents the prefix and suffix from stretching together with the container.
|
||||
width: 0.1px;
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import {async, TestBed, inject} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {FormsModule, ReactiveFormsModule, FormControl} from '@angular/forms';
|
||||
import {async, TestBed, inject, ComponentFixture} from '@angular/core/testing';
|
||||
import {Component, ViewChild} from '@angular/core';
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FormControl,
|
||||
NgForm,
|
||||
Validators,
|
||||
FormGroupDirective,
|
||||
FormGroup,
|
||||
} from '@angular/forms';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MdInputModule} from './index';
|
||||
import {MdInputContainer, MdInputDirective} from './input-container';
|
||||
import {Platform} from '../core/platform/platform';
|
||||
import {PlatformModule} from '../core/platform/index';
|
||||
import {wrappedErrorMessage} from '../core/testing/wrapped-error-message';
|
||||
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
|
||||
import {
|
||||
MdInputContainerMissingMdInputError,
|
||||
MdInputContainerPlaceholderConflictError,
|
||||
@@ -21,7 +31,8 @@ describe('MdInputContainer', function () {
|
||||
MdInputModule.forRoot(),
|
||||
PlatformModule.forRoot(),
|
||||
FormsModule,
|
||||
ReactiveFormsModule
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
declarations: [
|
||||
MdInputContainerPlaceholderRequiredTestComponent,
|
||||
@@ -50,7 +61,9 @@ describe('MdInputContainer', function () {
|
||||
MdInputContainerMissingMdInputTestController,
|
||||
MdInputContainerMultipleHintTestController,
|
||||
MdInputContainerMultipleHintMixedTestController,
|
||||
MdInputContainerWithDynamicPlaceholder
|
||||
MdInputContainerWithDynamicPlaceholder,
|
||||
MdInputContainerWithFormErrorMessages,
|
||||
MdInputContainerWithFormGroupErrorMessages
|
||||
],
|
||||
});
|
||||
|
||||
@@ -551,6 +564,127 @@ describe('MdInputContainer', function () {
|
||||
expect(labelEl.classList).not.toContain('mat-float');
|
||||
});
|
||||
|
||||
describe('error messages', () => {
|
||||
let fixture: ComponentFixture<MdInputContainerWithFormErrorMessages>;
|
||||
let testComponent: MdInputContainerWithFormErrorMessages;
|
||||
let containerEl: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
|
||||
fixture.detectChanges();
|
||||
testComponent = fixture.componentInstance;
|
||||
containerEl = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
|
||||
});
|
||||
|
||||
it('should not show any errors if the user has not interacted', () => {
|
||||
expect(testComponent.formControl.untouched).toBe(true, 'Expected untouched form control');
|
||||
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
|
||||
});
|
||||
|
||||
it('should display an error message when the input is touched and invalid', async(() => {
|
||||
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
|
||||
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
|
||||
|
||||
testComponent.formControl.markAsTouched();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(containerEl.classList)
|
||||
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
|
||||
expect(containerEl.querySelectorAll('md-error').length)
|
||||
.toBe(1, 'Expected one error message to have been rendered.');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display an error message when the parent form is submitted', async(() => {
|
||||
expect(testComponent.form.submitted).toBe(false, 'Expected form not to have been submitted');
|
||||
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
|
||||
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
|
||||
|
||||
dispatchFakeEvent(fixture.debugElement.query(By.css('form')).nativeElement, 'submit');
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(testComponent.form.submitted).toBe(true, 'Expected form to have been submitted');
|
||||
expect(containerEl.classList)
|
||||
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
|
||||
expect(containerEl.querySelectorAll('md-error').length)
|
||||
.toBe(1, 'Expected one error message to have been rendered.');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display an error message when the parent form group is submitted', async(() => {
|
||||
fixture.destroy();
|
||||
|
||||
let groupFixture = TestBed.createComponent(MdInputContainerWithFormGroupErrorMessages);
|
||||
let component: MdInputContainerWithFormGroupErrorMessages;
|
||||
|
||||
groupFixture.detectChanges();
|
||||
component = groupFixture.componentInstance;
|
||||
containerEl = groupFixture.debugElement.query(By.css('md-input-container')).nativeElement;
|
||||
|
||||
expect(component.formGroup.invalid).toBe(true, 'Expected form control to be invalid');
|
||||
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
|
||||
expect(component.formGroupDirective.submitted)
|
||||
.toBe(false, 'Expected form not to have been submitted');
|
||||
|
||||
dispatchFakeEvent(groupFixture.debugElement.query(By.css('form')).nativeElement, 'submit');
|
||||
groupFixture.detectChanges();
|
||||
|
||||
groupFixture.whenStable().then(() => {
|
||||
expect(component.formGroupDirective.submitted)
|
||||
.toBe(true, 'Expected form to have been submitted');
|
||||
expect(containerEl.classList)
|
||||
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
|
||||
expect(containerEl.querySelectorAll('md-error').length)
|
||||
.toBe(1, 'Expected one error message to have been rendered.');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should hide the errors and show the hints once the input becomes valid', async(() => {
|
||||
testComponent.formControl.markAsTouched();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(containerEl.classList)
|
||||
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
|
||||
expect(containerEl.querySelectorAll('md-error').length)
|
||||
.toBe(1, 'Expected one error message to have been rendered.');
|
||||
expect(containerEl.querySelectorAll('md-hint').length)
|
||||
.toBe(0, 'Expected no hints to be shown.');
|
||||
|
||||
testComponent.formControl.setValue('something');
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(containerEl.classList).not.toContain('mat-input-invalid',
|
||||
'Expected container not to have the invalid class when valid.');
|
||||
expect(containerEl.querySelectorAll('md-error').length)
|
||||
.toBe(0, 'Expected no error messages when the input is valid.');
|
||||
expect(containerEl.querySelectorAll('md-hint').length)
|
||||
.toBe(1, 'Expected one hint to be shown once the input is valid.');
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not hide the hint if there are no error messages', async(() => {
|
||||
testComponent.renderError = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(containerEl.querySelectorAll('md-hint').length)
|
||||
.toBe(1, 'Expected one hint to be shown on load.');
|
||||
|
||||
testComponent.formControl.markAsTouched();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(containerEl.querySelectorAll('md-hint').length)
|
||||
.toBe(1, 'Expected one hint to still be shown.');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@Component({
|
||||
@@ -775,3 +909,39 @@ class MdTextareaWithBindings {
|
||||
template: `<md-input-container><input></md-input-container>`
|
||||
})
|
||||
class MdInputContainerMissingMdInputTestController {}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<form #form="ngForm" novalidate>
|
||||
<md-input-container>
|
||||
<input mdInput [formControl]="formControl">
|
||||
<md-hint>Please type something</md-hint>
|
||||
<md-error *ngIf="renderError">This field is required</md-error>
|
||||
</md-input-container>
|
||||
</form>
|
||||
`
|
||||
})
|
||||
class MdInputContainerWithFormErrorMessages {
|
||||
@ViewChild('form') form: NgForm;
|
||||
formControl = new FormControl('', Validators.required);
|
||||
renderError = true;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<form [formGroup]="formGroup" novalidate>
|
||||
<md-input-container>
|
||||
<input mdInput formControlName="name">
|
||||
<md-hint>Please type something</md-hint>
|
||||
<md-error>This field is required</md-error>
|
||||
</md-input-container>
|
||||
</form>
|
||||
`
|
||||
})
|
||||
class MdInputContainerWithFormGroupErrorMessages {
|
||||
@ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
|
||||
formGroup = new FormGroup({
|
||||
name: new FormControl('', Validators.required)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
AfterContentInit,
|
||||
Component,
|
||||
ContentChild,
|
||||
ContentChildren,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Optional,
|
||||
Output,
|
||||
QueryList,
|
||||
EventEmitter,
|
||||
Renderer,
|
||||
ChangeDetectorRef,
|
||||
ViewEncapsulation,
|
||||
Self,
|
||||
ViewEncapsulation
|
||||
QueryList,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import {coerceBooleanProperty} from '../core';
|
||||
import {NgControl} from '@angular/forms';
|
||||
import {NgControl, NgForm, FormGroupDirective} from '@angular/forms';
|
||||
import {getSupportedInputTypes} from '../core/platform/features';
|
||||
import {
|
||||
MdInputContainerDuplicatedHintError,
|
||||
@@ -72,6 +81,14 @@ export class MdHint {
|
||||
@Input() id: string = `md-input-hint-${nextUniqueId++}`;
|
||||
}
|
||||
|
||||
/** Directive, used to display a single error message under the input. */
|
||||
@Directive({
|
||||
selector: 'md-error, mat-error',
|
||||
host: {
|
||||
'[class.mat-input-error]': 'true'
|
||||
}
|
||||
})
|
||||
export class MdErrorDirective { }
|
||||
|
||||
/** The input directive, used to mark the input that `MdInputContainer` is wrapping. */
|
||||
@Directive({
|
||||
@@ -235,10 +252,20 @@ export class MdInputDirective {
|
||||
selector: 'md-input-container, mat-input-container',
|
||||
templateUrl: 'input-container.html',
|
||||
styleUrls: ['input-container.css'],
|
||||
animations: [
|
||||
trigger('transitionMessages', [
|
||||
state('enter', style({ opacity: 1, transform: 'translateY(0%)' })),
|
||||
transition('void => enter', [
|
||||
style({ opacity: 0, transform: 'translateY(-100%)' }),
|
||||
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')
|
||||
])
|
||||
])
|
||||
],
|
||||
host: {
|
||||
// Remove align attribute to prevent it from interfering with layout.
|
||||
'[attr.align]': 'null',
|
||||
'[class.mat-input-container]': 'true',
|
||||
'[class.mat-input-invalid]': '_isErrorState()',
|
||||
'[class.mat-focused]': '_mdInputChild.focused',
|
||||
'[class.ng-untouched]': '_shouldForward("untouched")',
|
||||
'[class.ng-touched]': '_shouldForward("touched")',
|
||||
@@ -251,7 +278,7 @@ export class MdInputDirective {
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class MdInputContainer implements AfterContentInit {
|
||||
export class MdInputContainer implements AfterViewInit, AfterContentInit {
|
||||
/** Alignment of the input container's content. */
|
||||
@Input() align: 'start' | 'end' = 'start';
|
||||
|
||||
@@ -264,6 +291,9 @@ export class MdInputContainer implements AfterContentInit {
|
||||
/** Whether the placeholder can float or not. */
|
||||
get _canPlaceholderFloat() { return this._floatPlaceholder !== 'never'; }
|
||||
|
||||
/** State of the md-hint and md-error animations. */
|
||||
_subscriptAnimationState: string = '';
|
||||
|
||||
/** Text for the input hint. */
|
||||
@Input()
|
||||
get hintLabel() { return this._hintLabel; }
|
||||
@@ -288,8 +318,15 @@ export class MdInputContainer implements AfterContentInit {
|
||||
|
||||
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
|
||||
|
||||
@ContentChildren(MdErrorDirective) _errorChildren: QueryList<MdErrorDirective>;
|
||||
|
||||
@ContentChildren(MdHint) _hintChildren: QueryList<MdHint>;
|
||||
|
||||
constructor(
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
@Optional() private _parentForm: NgForm,
|
||||
@Optional() private _parentFormGroup: FormGroupDirective) { }
|
||||
|
||||
ngAfterContentInit() {
|
||||
if (!this._mdInputChild) {
|
||||
throw new MdInputContainerMissingMdInputError();
|
||||
@@ -303,6 +340,12 @@ export class MdInputContainer implements AfterContentInit {
|
||||
this._mdInputChild._placeholderChange.subscribe(() => this._validatePlaceholders());
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// Avoid animations on load.
|
||||
this._subscriptAnimationState = 'enter';
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
/** Determines whether a class from the NgControl should be forwarded to the host element. */
|
||||
_shouldForward(prop: string): boolean {
|
||||
let control = this._mdInputChild ? this._mdInputChild._ngControl : null;
|
||||
@@ -315,6 +358,22 @@ export class MdInputContainer implements AfterContentInit {
|
||||
/** Focuses the underlying input. */
|
||||
_focusInput() { this._mdInputChild.focus(); }
|
||||
|
||||
/** Whether the input container is in an error state. */
|
||||
_isErrorState(): boolean {
|
||||
const control = this._mdInputChild._ngControl;
|
||||
const isInvalid = control && control.invalid;
|
||||
const isTouched = control && control.touched;
|
||||
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
|
||||
(this._parentForm && this._parentForm.submitted);
|
||||
|
||||
return !!(isInvalid && (isTouched || isSubmitted));
|
||||
}
|
||||
|
||||
/** Determines whether to display hints or errors. */
|
||||
_getDisplayedMessages(): 'error' | 'hint' {
|
||||
return (this._errorChildren.length > 0 && this._isErrorState()) ? 'error' : 'hint';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there is only one placeholder (either `input` attribute or child element with the
|
||||
* `md-placeholder` attribute.
|
||||
|
||||
+15
-2
@@ -1,4 +1,4 @@
|
||||
`<md-input-container>` is a wrapper for native `input` and `textarea` elements. This container
|
||||
`<md-input-container>` is a wrapper for native `input` and `textarea` elements. This container
|
||||
applies Material Design styles and behavior while still allowing direct access to the underlying
|
||||
native element.
|
||||
|
||||
@@ -14,7 +14,7 @@ elements inside `md-input-container` as well. This includes Angular directives s
|
||||
|
||||
The only limitations are that the `type` attribute can only be one of the values supported by
|
||||
`md-input-container` and the native element cannot specify a `placeholder` attribute if the
|
||||
`md-input-container` also contains a `md-placeholder` element.
|
||||
`md-input-container` also contains a `md-placeholder` element.
|
||||
|
||||
### Supported `input` types
|
||||
|
||||
@@ -33,6 +33,19 @@ be used with `md-input-container`:
|
||||
* url
|
||||
* week
|
||||
|
||||
### Error messages
|
||||
|
||||
Error messages can be shown beneath an input by specifying `md-error` elements inside the
|
||||
`md-input-container`. Errors are hidden by default and will be displayed on invalid inputs after
|
||||
the user has interacted with the element or the parent form has been submitted. In addition,
|
||||
whenever errors are displayed, the container's `md-hint` labels will be hidden.
|
||||
|
||||
If an input element can have more than one error state, it is up to the consumer to toggle which
|
||||
messages should be displayed. This can be done with CSS, `ngIf` or `ngSwitch`.
|
||||
|
||||
Note that, while multiple error messages can be displayed at the same time, it is recommended to
|
||||
only show one at a time.
|
||||
|
||||
### Placeholder
|
||||
|
||||
A placeholder is an indicative text displayed in the input zone when the input does not contain
|
||||
|
||||
Reference in New Issue
Block a user