Skip to content

feat(select): add support for custom errorStateMatcher #6147

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

Closed
wants to merge 7 commits into from
Closed
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
4 changes: 4 additions & 0 deletions src/lib/core/_core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
@import 'option/option-theme';
@import 'option/optgroup';
@import 'option/optgroup-theme';
@import 'error/error';
@import 'error/error-theme';
@import 'selection/pseudo-checkbox/pseudo-checkbox-theme';
@import 'typography/all-typography';

Expand All @@ -25,6 +27,7 @@
@include mat-ripple();
@include mat-option();
@include mat-optgroup();
@include mat-error();
@include cdk-a11y();
@include cdk-overlay();
}
Expand All @@ -35,6 +38,7 @@
@include mat-option-theme($theme);
@include mat-optgroup-theme($theme);
@include mat-pseudo-checkbox-theme($theme);
@include mat-error-theme($theme);

// Wrapper element that provides the theme background when the
// user's content isn't inside of a `md-sidenav-container`.
Expand Down
12 changes: 7 additions & 5 deletions src/lib/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {OverlayModule} from './overlay/index';
import {A11yModule} from './a11y/index';
import {MdSelectionModule} from './selection/index';
import {MdRippleModule} from './ripple/index';
import {MdErrorModule} from './error/index';

// Re-exports of the CDK to avoid breaking changes.
export {
Expand Down Expand Up @@ -123,12 +124,11 @@ export {

// Error
export {
MdErrorModule,
MdError,
ErrorStateMatcher,
ErrorOptions,
MD_ERROR_GLOBAL_OPTIONS,
defaultErrorStateMatcher,
showOnDirtyErrorStateMatcher
} from './error/error-options';
ShowOnDirtyErrorStateMatcher,
} from './error/index';

@NgModule({
imports: [
Expand All @@ -141,6 +141,7 @@ export {
A11yModule,
MdOptionModule,
MdSelectionModule,
MdErrorModule,
],
exports: [
MdLineModule,
Expand All @@ -152,6 +153,7 @@ export {
A11yModule,
MdOptionModule,
MdSelectionModule,
MdErrorModule,
],
})
export class MdCoreModule {}
9 changes: 9 additions & 0 deletions src/lib/core/error/_error-theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import '../theming/palette';
@import '../theming/theming';


@mixin mat-error-theme($theme) {
.mat-error {
color: mat-color(map-get($theme, warn));
}
}
5 changes: 5 additions & 0 deletions src/lib/core/error/_error.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@mixin mat-error {
.mat-error {
display: block;
}
}
35 changes: 14 additions & 21 deletions src/lib/core/error/error-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,21 @@
* found in the LICENSE file at https://angular.io/license
*/

import {InjectionToken} from '@angular/core';
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
import {Injectable} from '@angular/core';
import {FormGroupDirective, NgForm, NgControl} from '@angular/forms';

/** Injection token that can be used to specify the global error options. */
export const MD_ERROR_GLOBAL_OPTIONS = new InjectionToken<ErrorOptions>('md-error-global-options');

export type ErrorStateMatcher =
(control: FormControl, form: FormGroupDirective | NgForm) => boolean;

export interface ErrorOptions {
errorStateMatcher?: ErrorStateMatcher;
}

/** Returns whether control is invalid and is either touched or is a part of a submitted form. */
export function defaultErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm) {
const isSubmitted = form && form.submitted;
return !!(control.invalid && (control.touched || isSubmitted));
/** Error state matcher that matches when a control is invalid and dirty. */
@Injectable()
export class ShowOnDirtyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean {
return control ? !!(control.invalid && (control.dirty || (form && form.submitted))) : false;
}
}

/** Returns whether control is invalid and is either dirty or is a part of a submitted form. */
export function showOnDirtyErrorStateMatcher(control: FormControl,
form: FormGroupDirective | NgForm) {
const isSubmitted = form && form.submitted;
return !!(control.invalid && (control.dirty || isSubmitted));
/** Provider that defines how form controls behave with regards to displaying error messages. */
@Injectable()
export class ErrorStateMatcher {
isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean {
return control ? !!(control.invalid && (control.touched || (form && form.submitted))) : false;
}
}
24 changes: 24 additions & 0 deletions src/lib/core/error/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, Input} from '@angular/core';

let nextUniqueId = 0;

/** Single error message to be shown underneath a form control. */
@Directive({
selector: 'md-error, mat-error',
host: {
'class': 'mat-error',
'role': 'alert',
'[attr.id]': 'id',
}
})
export class MdError {
@Input() id: string = `md-input-error-${nextUniqueId++}`;
}
23 changes: 23 additions & 0 deletions src/lib/core/error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/


import {NgModule} from '@angular/core';
import {MdError} from './error';
import {ErrorStateMatcher} from './error-options';

@NgModule({
declarations: [MdError],
exports: [MdError],
providers: [ErrorStateMatcher],
})
export class MdErrorModule {}


export * from './error';
export * from './error-options';
4 changes: 0 additions & 4 deletions src/lib/input/_input-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,6 @@
background-color: $input-underline-color-warn;
}
}

.mat-input-error {
color: $input-underline-color-warn;
}
}

// Applies a floating placeholder above the input itself.
Expand Down
6 changes: 3 additions & 3 deletions src/lib/input/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import {NgModule} from '@angular/core';
import {
MdErrorDirective,
MdHint,
MdInputContainer,
MdInputDirective,
Expand All @@ -19,11 +18,11 @@ import {
import {MdTextareaAutosize} from './autosize';
import {CommonModule} from '@angular/common';
import {PlatformModule} from '../core/platform/index';
import {MdErrorModule} from '../core/error/index';


@NgModule({
declarations: [
MdErrorDirective,
MdHint,
MdInputContainer,
MdInputDirective,
Expand All @@ -35,16 +34,17 @@ import {PlatformModule} from '../core/platform/index';
imports: [
CommonModule,
PlatformModule,
MdErrorModule,
],
exports: [
MdErrorDirective,
MdHint,
MdInputContainer,
MdInputDirective,
MdPlaceholder,
MdPrefix,
MdSuffix,
MdTextareaAutosize,
MdErrorModule,
],
})
export class MdInputModule {}
Expand Down
5 changes: 0 additions & 5 deletions src/lib/input/input-container.scss
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,3 @@ textarea.mat-input-element {
.mat-input-hint-spacer {
flex: 1 0 $mat-input-hint-min-space;
}

// Single error message displayed beneath the input.
.mat-input-error {
display: block;
}
24 changes: 8 additions & 16 deletions src/lib/input/input-container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
getMdInputContainerPlaceholderConflictError
} from './input-container-errors';
import {MD_PLACEHOLDER_GLOBAL_OPTIONS} from '../core/placeholder/placeholder-options';
import {MD_ERROR_GLOBAL_OPTIONS, showOnDirtyErrorStateMatcher} from '../core/error/error-options';
import {ErrorStateMatcher, ShowOnDirtyErrorStateMatcher} from '../core/error/error-options';

describe('MdInputContainer without forms', function () {
beforeEach(async(() => {
Expand Down Expand Up @@ -840,7 +840,7 @@ describe('MdInputContainer with forms', () => {
fixture.componentInstance.formControl.markAsTouched();
fixture.detectChanges();

let errorIds = fixture.debugElement.queryAll(By.css('.mat-input-error'))
let errorIds = fixture.debugElement.queryAll(By.css('.mat-error'))
.map(el => el.nativeElement.getAttribute('id')).join(' ');
describedBy = inputEl.getAttribute('aria-describedby');

Expand Down Expand Up @@ -896,9 +896,7 @@ describe('MdInputContainer with forms', () => {
MdInputContainerWithFormErrorMessages
],
providers: [
{
provide: MD_ERROR_GLOBAL_OPTIONS,
useValue: { errorStateMatcher: globalErrorStateMatcher } }
{ provide: ErrorStateMatcher, useValue: { isErrorState: globalErrorStateMatcher } }
]
});

Expand Down Expand Up @@ -926,12 +924,7 @@ describe('MdInputContainer with forms', () => {
declarations: [
MdInputContainerWithFormErrorMessages
],
providers: [
{
provide: MD_ERROR_GLOBAL_OPTIONS,
useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher }
}
]
providers: [{ provide: ErrorStateMatcher, useClass: ShowOnDirtyErrorStateMatcher }]
});

let fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
Expand Down Expand Up @@ -1247,7 +1240,7 @@ class MdInputContainerWithFormErrorMessages {
<md-input-container>
<input mdInput
formControlName="name"
[errorStateMatcher]="customErrorStateMatcher.bind(this)">
[errorStateMatcher]="customErrorStateMatcher">
<md-hint>Please type something</md-hint>
<md-error>This field is required</md-error>
</md-input-container>
Expand All @@ -1260,10 +1253,9 @@ class MdInputContainerWithCustomErrorStateMatcher {
});

errorState = false;

customErrorStateMatcher(): boolean {
return this.errorState;
}
customErrorStateMatcher: ErrorStateMatcher = {
isErrorState: () => this.errorState
};
}

@Component({
Expand Down
Loading