Skip to content

Commit

Permalink
perf(module:switch): reduce change detection cycles (#7105)
Browse files Browse the repository at this point in the history
  • Loading branch information
arturovt committed Dec 17, 2021
1 parent ca3299e commit 6d9b1ff
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 33 deletions.
85 changes: 53 additions & 32 deletions components/switch/switch.component.ts
Expand Up @@ -14,6 +14,7 @@ import {
ElementRef,
forwardRef,
Input,
NgZone,
OnDestroy,
OnInit,
Optional,
Expand All @@ -22,7 +23,7 @@ import {
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subject } from 'rxjs';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config';
Expand Down Expand Up @@ -57,7 +58,6 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'switch';
[class.ant-switch-small]="nzSize === 'small'"
[class.ant-switch-rtl]="dir === 'rtl'"
[nzWaveExtraNode]="true"
(keydown)="onKeyDown($event)"
>
<span class="ant-switch-handle">
<i *ngIf="nzLoading" nz-icon nzType="loading" class="ant-switch-loading-icon"></i>
Expand All @@ -72,10 +72,7 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'switch';
</span>
<div class="ant-click-animating-node"></div>
</button>
`,
host: {
'(click)': 'onHostClick($event)'
}
`
})
export class NzSwitchComponent implements ControlValueAccessor, AfterViewInit, OnDestroy, OnInit {
readonly _nzModuleName: NzConfigKey = NZ_CONFIG_MODULE_NAME;
Expand All @@ -87,7 +84,7 @@ export class NzSwitchComponent implements ControlValueAccessor, AfterViewInit, O
isChecked = false;
onChange: OnChangeType = () => {};
onTouched: OnTouchedType = () => {};
@ViewChild('switchElement', { static: true }) private switchElement?: ElementRef;
@ViewChild('switchElement', { static: true }) switchElement!: ElementRef<HTMLElement>;
@Input() @InputBoolean() nzLoading = false;
@Input() @InputBoolean() nzDisabled = false;
@Input() @InputBoolean() nzControl = false;
Expand All @@ -99,57 +96,81 @@ export class NzSwitchComponent implements ControlValueAccessor, AfterViewInit, O

private destroy$ = new Subject<void>();

onHostClick(e: MouseEvent): void {
e.preventDefault();
if (!this.nzDisabled && !this.nzLoading && !this.nzControl) {
this.updateValue(!this.isChecked);
}
}

updateValue(value: boolean): void {
if (this.isChecked !== value) {
this.isChecked = value;
this.onChange(this.isChecked);
}
}

onKeyDown(e: KeyboardEvent): void {
if (!this.nzControl && !this.nzDisabled && !this.nzLoading) {
if (e.keyCode === LEFT_ARROW) {
this.updateValue(false);
e.preventDefault();
} else if (e.keyCode === RIGHT_ARROW) {
this.updateValue(true);
e.preventDefault();
} else if (e.keyCode === SPACE || e.keyCode === ENTER) {
this.updateValue(!this.isChecked);
e.preventDefault();
}
}
}

focus(): void {
this.focusMonitor.focusVia(this.switchElement?.nativeElement, 'keyboard');
this.focusMonitor.focusVia(this.switchElement.nativeElement, 'keyboard');
}

blur(): void {
this.switchElement?.nativeElement.blur();
this.switchElement.nativeElement.blur();
}

constructor(
public nzConfigService: NzConfigService,
private host: ElementRef<HTMLElement>,
private ngZone: NgZone,
private cdr: ChangeDetectorRef,
private focusMonitor: FocusMonitor,
@Optional() private directionality: Directionality
) {}

ngOnInit(): void {
this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => {
this.directionality.change.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => {
this.dir = direction;
this.cdr.detectChanges();
});

this.dir = this.directionality.value;

this.ngZone.runOutsideAngular(() => {
fromEvent(this.host.nativeElement, 'click')
.pipe(takeUntil(this.destroy$))
.subscribe(event => {
event.preventDefault();

if (this.nzControl || this.nzDisabled || this.nzLoading) {
return;
}

this.ngZone.run(() => {
this.updateValue(!this.isChecked);
this.cdr.markForCheck();
});
});

fromEvent<KeyboardEvent>(this.switchElement.nativeElement, 'keydown')
.pipe(takeUntil(this.destroy$))
.subscribe(event => {
if (this.nzControl || this.nzDisabled || this.nzLoading) {
return;
}

const { keyCode } = event;
if (keyCode !== LEFT_ARROW && keyCode !== RIGHT_ARROW && keyCode !== SPACE && keyCode !== ENTER) {
return;
}

event.preventDefault();

this.ngZone.run(() => {
if (keyCode === LEFT_ARROW) {
this.updateValue(false);
} else if (keyCode === RIGHT_ARROW) {
this.updateValue(true);
} else if (keyCode === SPACE || keyCode === ENTER) {
this.updateValue(!this.isChecked);
}

this.cdr.markForCheck();
});
});
});
}

ngAfterViewInit(): void {
Expand Down
44 changes: 43 additions & 1 deletion components/switch/switch.spec.ts
@@ -1,6 +1,6 @@
import { BidiModule, Dir } from '@angular/cdk/bidi';
import { ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE } from '@angular/cdk/keycodes';
import { Component, DebugElement, TemplateRef, ViewChild } from '@angular/core';
import { ApplicationRef, Component, DebugElement, TemplateRef, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, TestBed, waitForAsync } from '@angular/core/testing';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
Expand Down Expand Up @@ -172,6 +172,48 @@ describe('switch', () => {
fixture.detectChanges();
expect(switchElement.nativeElement.firstElementChild === document.activeElement).toBe(false);
});
describe('change detection behavior', () => {
it('should not run change detection on `click` events if the switch is disabled', () => {
testComponent.disabled = true;
fixture.detectChanges();

const appRef = TestBed.inject(ApplicationRef);
const event = new MouseEvent('click');

spyOn(appRef, 'tick');
spyOn(event, 'preventDefault').and.callThrough();

switchElement.nativeElement.dispatchEvent(event);

expect(appRef.tick).not.toHaveBeenCalled();
expect(event.preventDefault).toHaveBeenCalled();
});
it('should not run change detection on `keydown` events if the switch is disabled', () => {
testComponent.disabled = true;
fixture.detectChanges();

const switchButton = switchElement.nativeElement.querySelector('.ant-switch');
const appRef = TestBed.inject(ApplicationRef);
const event = new KeyboardEvent('keydown', {
keyCode: SPACE
});

spyOn(appRef, 'tick');
spyOn(event, 'preventDefault').and.callThrough();

switchButton.dispatchEvent(event);

expect(appRef.tick).not.toHaveBeenCalled();
expect(event.preventDefault).not.toHaveBeenCalled();

testComponent.disabled = false;
fixture.detectChanges();

switchButton.dispatchEvent(event);

expect(event.preventDefault).toHaveBeenCalled();
});
});
});
describe('template switch', () => {
let fixture: ComponentFixture<NzTestSwitchTemplateComponent>;
Expand Down

0 comments on commit 6d9b1ff

Please sign in to comment.