-
Notifications
You must be signed in to change notification settings - Fork 6.7k
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
feat(input): add custom error state matcher #4750
feat(input): add custom error state matcher #4750
Conversation
I'm wondering if this should be a more generally configurable concept; |
@kara should also weigh in as this touches on forms |
56b9846
to
5a270f3
Compare
5a270f3
to
f22a77a
Compare
1f38a53
to
d06612a
Compare
@jelbourn since you last reviewed, I've made the following changes:
I'm far from married to # 3, but wanted to explore other global options. Also if this isn't the right approach, that's fine, but I'd like to keep the conversation moving. |
src/lib/core/error/error-options.ts
Outdated
export const MD_ERROR_GLOBAL_OPTIONS = | ||
new InjectionToken<() => boolean>('md-error-global-options'); | ||
|
||
export type ErrorStateMatcherType = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just ErrorStateMatcher
is fine
src/lib/core/error/error-options.ts
Outdated
|
||
/** Injection token that can be used to specify the global error options. */ | ||
export const MD_ERROR_GLOBAL_OPTIONS = | ||
new InjectionToken<() => boolean>('md-error-global-options'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generic type should be ErrorOptions
@@ -706,6 +709,116 @@ describe('MdInputContainer', function () { | |||
}); | |||
})); | |||
|
|||
it('should display an error message when a custom error matcher returns true', async(() => { | |||
fixture.destroy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we just put this in a different describe block or something? its weird that we destroy the original fixture here
src/lib/core/error/error-options.ts
Outdated
|
||
export interface ErrorOptions { | ||
errorStateMatcher?: ErrorStateMatcherType; | ||
showOnDirty?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this is structured kind of weirdly, how about if we remove the showOnDirty
option and instead just add a couple implementations that the user can choose from (e.g. DefaultErrorStateMatcher
, ShowOnDirtyErrorStateMatcher
)
|
||
customFixture.whenStable().then(() => { | ||
expect(containerEl.querySelectorAll('md-error').length) | ||
.toBe(0, 'Expected no error messages after being touched.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: indent continuations by 4
src/lib/input/input-container.ts
Outdated
|
||
// Force setter to be called in case id was not specified. | ||
this.id = this.id; | ||
|
||
this._errorOptions = errorOptions ? errorOptions : {}; | ||
this.errorStateMatcher = this._errorOptions.errorStateMatcher || undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you create the DefaultErrorStateMatcher
class I suggested earlier this can be || new DefaultErrorStateMatcher()
d06612a
to
84841d0
Compare
84841d0
to
b0d69cc
Compare
@mmalerba I've addressed your comments! Please advise if I misinterpreted any of your class implementation suggestions. |
src/lib/core/error/error-options.ts
Outdated
|
||
/** 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'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think this will fit on 1 line
src/lib/core/error/error-options.ts
Outdated
errorStateMatcher?: ErrorStateMatcher; | ||
} | ||
|
||
export class DefaultErrorStateMatcher { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh sorry, I don't know why I said class, I just meant functions defaultErrorStateMatcher
<form #form="ngForm" novalidate> | ||
<md-input-container> | ||
<input mdInput | ||
[formControl]="formControl" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: indent continuation by 4
@mmalerba done! The convenience function isInvalid && (isDirty || isSubmitted) But I'm curious if the expected behavior might be, isInvalid && (isDirty || isTouched || isSubmitted) |
src/demo-app/input/input-demo.ts
Outdated
@@ -43,4 +44,11 @@ export class InputDemo { | |||
this.items.push({ value: ++max }); | |||
} | |||
} | |||
|
|||
customErrorStateMatcher(c: NgControl): boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, it's a bit odd to pass through the directive because any relevant properties just fall through directly to its FormControl
. I think here it would make sense to expose a FormControl
instance.
src/lib/input/input-container.ts
Outdated
(this._parentForm && this._parentForm.submitted); | ||
|
||
return !!(isInvalid && (isTouched || isSubmitted)); | ||
return this.errorStateMatcher(control, this._parentFormGroup, this._parentForm); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should be passing through this._ngControl.control
here. In other words, the FormControl
instance rather than the NgControl
directive. I don't think most people are familiar with NgControls
, and probably aren't familiar with the subtle distinction between the directive and the FormControl
itself. While most properties are mirrored on the directive, a few aren't, and I foresee that being confusing.
src/lib/core/error/error-options.ts
Outdated
} | ||
|
||
export function showOnDirtyErrorStateMatcher(control: NgControl, formGroup: FormGroupDirective, | ||
form: NgForm): boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should be surfacing both FormGroupDirective
and NgForm
here. They are mutually exclusive, so one of these args will always be null. We shouldn't put it on the user to check for the existence of one or the other every time. Given that they both implement the Form
interface, it might make more sense to use the Form
type here and only pass through the existing parent to this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with the sentiment, but I'm not clear on how it would work. The Form
interface doesn't have the submitted
property, which is the main (if not only) reason we need it. I see two solutions
- Use the union
FormGroupDirective | NgForm
export function showOnDirtyErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm) {
const isInvalid = control.invalid;
const isDirty = control.dirty;
const isSubmitted = form && form.submitted;
return !!(isInvalid && (isDirty || isSubmitted));
}
- Just pass
submitted
and don't expose eitherFormGroupDirective
orNgForm
export function showOnDirtyErrorStateMatcher(control: FormControl, submitted: boolean) {
const isInvalid = control.invalid;
const isDirty = control.dirty;
return !!(isInvalid && (isDirty || submitted));
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, the fact that the submitted
property is missing from Form
interface seems like an oversight (will look into it). Given that fact, I prefer #1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kara were you ever able to check into why submitted
is missing from the Form
interface?
@@ -1020,6 +1136,29 @@ class MdInputContainerWithFormErrorMessages { | |||
|
|||
@Component({ | |||
template: ` | |||
<form #form="ngForm" novalidate> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove novalidate
? This is added by default by the NgNoValidate
directive in @angular/forms
.
<form #form="ngForm" novalidate> | ||
<md-input-container> | ||
<input mdInput | ||
[formControl]="formControl" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NgForm
comes with the FormsModule
and formControl
comes from the ReactiveFormsModule
. It's not really great to mix them, and the point of using formControl
is to have a standalone control without a parent.
If you want a more "idiomatic" test case, I'd suggest switching to formGroup
on the form tag and then use formControlName
rather than formControl
.
83954c5
to
9bc13bb
Compare
@kara comments addressed. Could you take another look? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good, just a few more little things. @mmalerba Are you finished reviewing?
src/lib/core/error/error-options.ts
Outdated
|
||
export function defaultErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm) { | ||
const isInvalid = control.invalid; | ||
const isTouched = control.touched; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: these const assignments seem to be adding bloat, given how short control.touched
already is. I'm thinking it's easier to scan this function if you just inline (same on line 32).
const isSubmitted = form && form.submitted;
return !!(control.invalid && (control.touched || isSubmitted));
@@ -749,6 +751,121 @@ describe('MdInputContainer', function () { | |||
|
|||
}); | |||
|
|||
describe('custom error behavior', () => { | |||
it('should display an error message when a custom error matcher returns true', async(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we're just using reactive form directives now, these tests don't need to be async. You should be able to safely remove the async
and whenStable
calls.
LGTM, waiting on @mmalerba to do a last pass. |
src/lib/core/error/error-options.ts
Outdated
errorStateMatcher?: ErrorStateMatcher; | ||
} | ||
|
||
export function defaultErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docs
src/lib/core/error/error-options.ts
Outdated
return !!(control.invalid && (control.touched || isSubmitted)); | ||
} | ||
|
||
export function showOnDirtyErrorStateMatcher(control: FormControl, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docs
* feat(input): Add custom error state matcher * Address comments * Address comments pt. 2 * Use FormControl and only one of incompatible form options * Remove unnecesary async tests and const declarations * Add jsdoc comments to error state matchers
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
Fixes #4232
cc @mmalerba