-
-
Notifications
You must be signed in to change notification settings - Fork 30
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: support showing errors when a control is marked as touched #114
base: master
Are you sure you want to change the base?
feat: support showing errors when a control is marked as touched #114
Conversation
|
@NetanelBasal Looking forward to know your thoughts. Took it a step further and since configs to change the default behavior are a few now, I thought it would be good to explain a bit better how it works, so I added a |
@@ -43,7 +45,7 @@ const errorTailorClass = 'error-tailor-has-error'; | |||
'[formControlName]:not([controlErrorsIgnore]), [formControl]:not([controlErrorsIgnore]), [formGroup]:not([controlErrorsIgnore]), [formGroupName]:not([controlErrorsIgnore]), [formArrayName]:not([controlErrorsIgnore]), [ngModel]:not([controlErrorsIgnore])', | |||
exportAs: 'errorTailor', | |||
}) | |||
export class ControlErrorsDirective implements OnInit, OnDestroy { | |||
export class ControlErrorsDirective implements OnInit, OnDestroy, DoCheck { | |||
@Input('controlErrors') customErrors: ErrorsMap = {}; |
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.
What do u think about converting the inputs to signal inputs?
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.
Would be nice indeed! How about doing it in a separate PR, though? I believe we would also need to update the Angular version. input
seems to be exported from v17.2.0 on, unless I'm missing something.
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.
Sure
}); | ||
} | ||
|
||
ngDoCheck(): void { |
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.
Let's use afterRender hook and only invoke it when this.mergedConfig.controlErrorsOn.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.
What about monkey patch ngControl methods instead?
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 considered monkey patching since it would be the most performant solution (called the least amount of times), but I think it's not the safest solution.
First of all we'd need to monkey patch all methods that might change the touched
property, so control.markAsTouched()
, control.markAsUntouched()
, formGroup.markAllAsTouched()
. We also could miss methods introduced in new versions of Angular that change that property, for example formGroup.markAllAsTouched()
was introduced in v8.
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.
Sure, I can look into the afterRender
solution. Do you think it would be more performant?
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.
Yes, because it'll only do it if this.mergedConfig.controlErrorsOn.touched is true
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.
Back with some results. I compared these three implementations:
ngDoCheck
(as in the current PR):ngDoCheck(): void { if (this.mergedConfig.controlErrorsOn.touched) { this.touchedChanges$.next(this.control.touched); } }
afterRender
:and then callif (this.mergedConfig.controlErrorsOn.touched) { afterRender( () => { this.touchedChanges$.next(this.control.touched); }, { injector: this.injector }, ); }
this.changeDetectorRef.detectChanges()
MutationObserver
:if (this.mergedConfig.controlErrorsOn.touched) { const observer = new MutationObserver((mutations: MutationRecord[]) => { mutations.forEach((mutation) => { const isTouched = (mutation.target as HTMLElement).classList.contains('ng-touched'); this.touchedChanges$.next(isTouched); }); }); observer.observe(this.host, { attributeFilter: ['class'], }); }
For each implementation I placed a console.count('rerender')
in ngDoCheck()
to count how many rerenders were performed (only for the name form control). I tested by loading the page, then after one second I called markAsTouched()
and then after another second I called markAsUntouched()
. If I didn't do any mistake, these are the results:
-
ngDoCheck
: 5 rerenders -
afterRender
: 7 rerenders -
MutationObserver
: 8 rerenders
Honestly I don't understand why the MutationObserver
implementation would cause 3 more rerenders. If you want I can push the code to a separate branch on my fork if you want to take a look, let me know.
If these results are correct then maybe it's better to go with the ngDoCheck
implementation? As I see it right now the comparison is between:
ngDoCheck
solution: accessing thethis.mergedConfig.controlErrorsOn.touched
on each change detection (doesn't do anything else if false)- other solutions: causing more rerenders, which will definitely run more code since the whole Angular's change detection logic will be set into motion.
What do you think? Had some fun digging into this matter 😄 but if you feel strongly that we should go for the other solutions I can implement the one you prefer.
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.
Let's go with afterRender but only call detect changes if the prev !== current
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.
Sure, going to call detectChanges
in the changesOnTouched$
which has distinctUntilChanged
, so will only run when it actually changes.
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.
Mmm, I'm a bit puzzled. I'm pretty sure I successfully tested the afterRender
solution earlier, but now when trying to implement it again I get this error if I call change detection:
Error: NG0102: A new render operation began before the previous operation ended. Did you trigger change detection from afterRender or afterNextRender?
which didn't show up before… Seems a timing issue, because if I call detectChanges()
in a setTimeout
with just 10ms of delay the error doesn't throw and it works fine.
I created a separate branch with the change, care to have a 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.
I'm very busy at work currently. Try to comment some code and see where is the issue (You can maybe begin with setError)
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: #64
Currently errors are not shown if a control is marked as
touched
programmatically viacontrol.markAsTouched()
orformGroup.markAllAsTouched()
.What is the new behavior?
When setting
controlErrorsOn: { touched: true }
in the config or when setting the[controlErrorsOnTouched]="true"
input on a form control, errors will be show when the control is marked as touched (control.touched === true
) and will be hidden if the control becomes pristine again after beingtouched
.Does this PR introduce a breaking change?