Skip to content
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(select): add support for custom errorStateMatcher #6147

Closed
wants to merge 7 commits into from

Conversation

crisbeto
Copy link
Member

  • Allows for the md-select error behavior to be configured through an @Input, as well as globally through the same provider as md-input-container.
  • Simplifies the signature of some of the error option symbols.

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Jul 30, 2017
@jelbourn
Copy link
Member

@kara and @mmalerba please take a look

@@ -7,27 +7,24 @@
*/

import {InjectionToken} from '@angular/core';
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
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 =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I was thinking about maybe refactoring some of this error state matcher stuff because @g1shin recently had to implement a context-sensitive one for the stepper (#6117) and it felt a bit messier than it needed to be... What do you think about something like this:

export interface ErrorStateMatcher {
  isErrorState(...): boolean;
} 

export class DefaultErrorStateMatcher {
  isErrorState(...) { ... }
}

export class ShowOnDirtyErrorStateMatcher {
  isErrorState(...) { ... }
}

And then in error module:

@NgModule({
  providers: [{provide: ErrorStateMatcher, useClass: DefaultErrorStateMatcher}]
})

And finally in input/select/etc:

constructor(errorStateMatcher: ErrorStateMatcher) {
  console.log(errorStateMatcher.isErrorState(...));
}

I like this because:

  1. We don't have to worry about the function being ripped off the class and losing its context
  2. We don't have to worry about falling back to the defaultErrorStateMatcher in every component since we provide that in the error module
  3. The code to inject in the constructor doesn't need a whole bunch of @Optional() @Inject(...) stuff

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM, but because the matcher is really just the function, it would look more along the lines of this:

export class ErrorOptions {
  isErrorState = (control, form) => control.invalid && (form.submitted || control.dirty);
  // more error-related config goes here in the future.
}

Also I'm not sure whether we should provide the ShowOnDirtyErrorStateMatcher by default since it's pretty basic logic that people can write themselves if they're overriding it anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 Is there other stuff we plan to put in ErrorOptions? Just curious, I can't remember why we went with that over just a function

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have anything atm, but the intention is to avoid having to add any more providers if we decide to have more options.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crisbeto I added it only because it seemed to be a common question here and on SO #4750 (comment), #4750 (comment)

@mmalerba None that I was aware of. Simply based it off of PlaceholderOptions which seemed a nicely future proof

Copy link
Contributor

@mmalerba mmalerba Jul 31, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok sounds good, I do want to keep the interface though so that components like the stepper can implement it.

interface ErrorOptions { ... }
class DefaultErrorOptions implements ErrorOptions { ... }

edit: @crisbeto I'd like to keep the show-on-dirty one because as @willshowell it was a pretty common request and it's nice to have a really easy way for people to do it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, although FWIW, you can implement a class as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waaa I didn't know that, TS you crazy

@crisbeto crisbeto requested a review from kara as a code owner August 2, 2017 18:54
@crisbeto
Copy link
Member Author

crisbeto commented Aug 2, 2017

@mmalerba I ended up splitting the ErrorOptions and MdError directive into a separate module to make the DI a little simpler and because we'll probably end up needing the MdError for MdSelect at some point anyway. Can you take another look?

*/
@Injectable()
export class ErrorOptions {
errorStateMatcher: ErrorStateMatcher = defaultErrorStateMatcher;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this method style instead? I want it to be obvious that it's safe to use this in the method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const control = this._ngControl;
const parent = this._parentFormGroup || this._parentForm;
const newState = control && this.errorStateMatcher(control.control as FormControl, parent);
const errorMatcher = this.errorStateMatcher || this._errorOptions.errorStateMatcher;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is bad because it loses the error options context. Honestly, I think we should just get rid of ErrorOptions since there are currently no other options. and make ErrorStateMatcher a class rather than a function. This was we can easily support overriding via @Input like we want to do here and the stepper can easily provide its context-dependent matcher

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how making the ErrorStateMatcher would help, considering that it would still look something like:

class ErrorStateMatcher {
  match(): boolean;
}

Otherwise I'll fix it so it doesn't lose the context.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then instead of making the Input() take a function you make it take a ErrorStateMatcher. My other reason for not wanting ErrorOptions is that things like the stepper which just cares about the matcher then has to worry about all the other options too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, let's just go back to the injection token with a plain function and we can worry about a provider if we really need it.

@crisbeto
Copy link
Member Author

crisbeto commented Aug 2, 2017

Updated again @mmalerba.

Copy link
Contributor

@mmalerba mmalerba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes, I think this will make it a lot cleaner when we go to do the ErrorStateMatcher for the stepper :)

A global error state matcher can be specified by setting the `MD_ERROR_GLOBAL_OPTIONS` provider. This applies
to all inputs. For convenience, `showOnDirtyErrorStateMatcher` is available in order to globally cause
input errors to show when the input is dirty and invalid.
A global error state matcher can be specified by setting the `ErrorOptions` provider. This applies
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorStateMatcher


```ts
@NgModule({
providers: [
{provide: MD_ERROR_GLOBAL_OPTIONS, useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher }}
{provide: ErrorOptions, useClass: ShowOnDirtyErrorStateMatcher}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorStateMatcher

});

it('should be able to override the error matching behavior via the injection token', () => {
const errorOptions: ErrorStateMatcher = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errorStateMatcher

/** Error state matcher that matches when a control is invalid and dirty. */
@Injectable()
export class ShowOnDirtyErrorStateMatcher implements ErrorStateMatcher {
match(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we call it isErrorSate? match is kind of generic and might conflict with some other interface someone is implementing

@crisbeto
Copy link
Member Author

crisbeto commented Aug 3, 2017

Addressed the feedback @mmalerba.

@mmalerba mmalerba added pr: lgtm action: merge The PR is ready for merge by the caretaker and removed pr: needs review labels Aug 3, 2017
@kara kara removed pr: lgtm action: merge The PR is ready for merge by the caretaker labels Aug 3, 2017
/** Error state matcher that matches when a control is invalid and dirty. */
@Injectable()
export class ShowOnDirtyErrorStateMatcher implements ErrorStateMatcher {
isErrorSate(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This public API has a typo - isErrorSate -> isErrorState. Should be fixed across the PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ops, good catch.

@kara kara assigned crisbeto and unassigned jelbourn, kara and mmalerba Aug 3, 2017
@crisbeto crisbeto added pr: lgtm action: merge The PR is ready for merge by the caretaker labels Aug 3, 2017
* Allows for the `md-select` error behavior to be configured through an `@Input`, as well as globally through the same provider as `md-input-container`.
* Simplifies the signature of some of the error option symbols.
@mmalerba mmalerba removed the action: merge The PR is ready for merge by the caretaker label Aug 7, 2017
@mmalerba
Copy link
Contributor

mmalerba commented Aug 7, 2017

@jelbourn would you rather move md-error into core/error/ or form-field/ as #6331 currently does?

@jelbourn
Copy link
Member

jelbourn commented Aug 8, 2017

@mmalerba form-field is good

@crisbeto
Copy link
Member Author

crisbeto commented Aug 8, 2017

In that case let's defer this PR until the form field gets in.

@mmalerba
Copy link
Contributor

I think we can resume work on this one now

crisbeto added a commit to crisbeto/material2 that referenced this pull request Sep 30, 2017
* Allows for the select's error state matcher to be overwritten through an `@Input`.
* Switches `MatSelect` over to use the same global provider for its error state as `MatInput`.

**Note:** This is a resubmit of angular#6147 that works with our latest setup and excludes a few changes.
@crisbeto
Copy link
Member Author

crisbeto commented Sep 30, 2017

@mmalerba a lot changed since we parked this PR and it'll take a while to rebase it. I've re-done it, excluded the mat-error changes and resubmitted as #7443.

@crisbeto crisbeto closed this Sep 30, 2017
crisbeto added a commit to crisbeto/material2 that referenced this pull request Sep 30, 2017
* Allows for the select's error state matcher to be overwritten through an `@Input`.
* Switches `MatSelect` over to use the same global provider for its error state as `MatInput`.

**Note:** This is a resubmit of angular#6147 that works with our latest setup and excludes a few changes.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Sep 30, 2017
* Allows for the select's error state matcher to be overwritten through an `@Input`.
* Switches `MatSelect` over to use the same global provider for its error state as `MatInput`.

**Note:** This is a resubmit of angular#6147 that works with our latest setup and excludes a few changes.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Sep 30, 2017
* Allows for the select's error state matcher to be overwritten through an `@Input`.
* Switches `MatSelect` over to use the same global provider for its error state as `MatInput`.

**Note:** This is a resubmit of angular#6147 that works with our latest setup and excludes a few changes.

Fixes angular#7419.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Oct 3, 2017
* Allows for the select's error state matcher to be overwritten through an `@Input`.
* Switches `MatSelect` over to use the same global provider for its error state as `MatInput`.

**Note:** This is a resubmit of angular#6147 that works with our latest setup and excludes a few changes.

Fixes angular#7419.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Oct 4, 2017
* Allows for the select's error state matcher to be overwritten through an `@Input`.
* Switches `MatSelect` over to use the same global provider for its error state as `MatInput`.

**Note:** This is a resubmit of angular#6147 that works with our latest setup and excludes a few changes.

Fixes angular#7419.
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 7, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes PR author has agreed to Google's Contributor License Agreement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants