Skip to content

Commit

Permalink
fix(slide-toggle): prevent error when disabling while focused (#12325)
Browse files Browse the repository at this point in the history
Fixes Angular throwing an `ExpressionChangedAfterItHasBeenCheckedError` when disabling the slide-toggle while the component has been focused.

Fixes #12323
  • Loading branch information
devversion authored and jelbourn committed Aug 1, 2018
1 parent c4cacce commit e273a7a
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 6 deletions.
36 changes: 31 additions & 5 deletions src/lib/slide-toggle/slide-toggle.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import {MutationObserverFactory} from '@angular/cdk/observers';
import {dispatchFakeEvent} from '@angular/cdk/testing';
import {Component} from '@angular/core';
import {ComponentFixture, fakeAsync, flushMicrotasks, TestBed, tick} from '@angular/core/testing';
import {
ComponentFixture,
fakeAsync,
flush,
flushMicrotasks,
TestBed,
tick,
} from '@angular/core/testing';
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
import {defaultRippleAnimationConfig} from '@angular/material/core';
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
Expand Down Expand Up @@ -773,7 +780,7 @@ describe('MatSlideToggle with forms', () => {
expect(slideToggleElement.classList).toContain('mat-checked');
}));

it('should have the correct control state initially and after interaction', () => {
it('should have the correct control state initially and after interaction', fakeAsync(() => {
// The control should start off valid, pristine, and untouched.
expect(slideToggleModel.valid).toBe(true);
expect(slideToggleModel.pristine).toBe(true);
Expand All @@ -795,13 +802,31 @@ describe('MatSlideToggle with forms', () => {
// also turn touched.
dispatchFakeEvent(inputElement, 'blur');
fixture.detectChanges();
flushMicrotasks();

expect(slideToggleModel.valid).toBe(true);
expect(slideToggleModel.pristine).toBe(false);
expect(slideToggleModel.touched).toBe(true);
});
}));

it('should not throw an error when disabling while focused', fakeAsync(() => {
expect(() => {
// Focus the input element because after disabling, the `blur` event should automatically
// fire and not result in a changed after checked exception. Related: #12323
inputElement.focus();

// Flush the two nested timeouts from the FocusMonitor that are being created on `focus`.
flush();

slideToggle.disabled = true;
fixture.detectChanges();
flushMicrotasks();
}).not.toThrow();
}));

it('should not set the control to touched when changing the state programmatically',
fakeAsync(() => {

it('should not set the control to touched when changing the state programmatically', () => {
// The control should start off with being untouched.
expect(slideToggleModel.touched).toBe(false);

Expand All @@ -815,10 +840,11 @@ describe('MatSlideToggle with forms', () => {
// also turn touched.
dispatchFakeEvent(inputElement, 'blur');
fixture.detectChanges();
flushMicrotasks();

expect(slideToggleModel.touched).toBe(true);
expect(slideToggleElement.classList).toContain('mat-checked');
});
}));

it('should not set the control to touched when changing the model', fakeAsync(() => {
// The control should start off with being untouched.
Expand Down
7 changes: 6 additions & 1 deletion src/lib/slide-toggle/slide-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,12 @@ export class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestro
// For keyboard focus show a persistent ripple as focus indicator.
this._focusRipple = this._ripple.launch(0, 0, {persistent: true});
} else if (!focusOrigin) {
this.onTouched();
// When a focused element becomes disabled, the browser *immediately* fires a blur event.
// Angular does not expect events to be raised during change detection, so any state change
// (such as a form control's 'ng-touched') will cause a changed-after-checked error.
// See https://github.com/angular/angular/issues/17793. To work around this, we defer telling
// the form control it has been touched until the next tick.
Promise.resolve().then(() => this.onTouched());

// Fade out and clear the focus ripple if one is currently present.
if (this._focusRipple) {
Expand Down

0 comments on commit e273a7a

Please sign in to comment.