diff --git a/components/collapse/nz-collapse.component.ts b/components/collapse/nz-collapse.component.ts index 7614b0a7ed8..21cc144396a 100644 --- a/components/collapse/nz-collapse.component.ts +++ b/components/collapse/nz-collapse.component.ts @@ -9,8 +9,8 @@ import { ViewEncapsulation } from '@angular/core'; -import { merge, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { merge, Subject, Subscription } from 'rxjs'; +import { startWith, takeUntil } from 'rxjs/operators'; import { InputBoolean } from '../core/util/convert'; import { NzCollapsePanelComponent } from './nz-collapse-panel.component'; @@ -29,7 +29,8 @@ export class NzCollapseComponent implements AfterContentInit, OnDestroy { @ContentChildren(NzCollapsePanelComponent) listOfNzCollapsePanelComponent: QueryList; @Input() @InputBoolean() nzAccordion = false; @Input() @InputBoolean() nzBordered = true; - destroy$ = new Subject(); + private destroy$ = new Subject(); + private clickSubscription: Subscription; click(collapse: NzCollapsePanelComponent): void { if (this.nzAccordion && !collapse.nzActive) { @@ -46,9 +47,20 @@ export class NzCollapseComponent implements AfterContentInit, OnDestroy { } ngAfterContentInit(): void { - merge(...this.listOfNzCollapsePanelComponent.map(item => item.click$)).pipe(takeUntil(this.destroy$)).subscribe((data) => { - this.click(data); + this.listOfNzCollapsePanelComponent.changes.pipe( + startWith(null), + takeUntil(this.destroy$) + ).subscribe(() => { + if (this.clickSubscription) { + this.clickSubscription.unsubscribe(); + } + this.clickSubscription = merge(...this.listOfNzCollapsePanelComponent.map(item => item.click$)).pipe( + takeUntil(this.destroy$) + ).subscribe((data) => { + this.click(data); + }); }); + } ngOnDestroy(): void { diff --git a/components/layout/nz-sider.component.html b/components/layout/nz-sider.component.html index 49d8ff6ada8..e1208937d4a 100644 --- a/components/layout/nz-sider.component.html +++ b/components/layout/nz-sider.component.html @@ -9,7 +9,7 @@
+ [style.width.px]="nzCollapsed ? nzCollapsedWidth : nzWidth">
diff --git a/components/layout/nz-sider.component.ts b/components/layout/nz-sider.component.ts index 0a59163cbd4..19d68de49d2 100644 --- a/components/layout/nz-sider.component.ts +++ b/components/layout/nz-sider.component.ts @@ -1,6 +1,7 @@ import { AfterViewInit, ChangeDetectionStrategy, + ChangeDetectorRef, Component, EventEmitter, Host, @@ -82,6 +83,7 @@ export class NzSiderComponent implements OnInit, AfterViewInit, OnDestroy { this.below = matchBelow; this.nzCollapsed = matchBelow; this.nzCollapsedChange.emit(matchBelow); + this.cdr.markForCheck(); } } @@ -98,7 +100,7 @@ export class NzSiderComponent implements OnInit, AfterViewInit, OnDestroy { return this.nzCollapsible && this.nzTrigger && (this.nzCollapsedWidth !== 0); } - constructor(@Optional() @Host() private nzLayoutComponent: NzLayoutComponent, private mediaMatcher: MediaMatcher, private ngZone: NgZone, private platform: Platform) { + constructor(@Optional() @Host() private nzLayoutComponent: NzLayoutComponent, private mediaMatcher: MediaMatcher, private ngZone: NgZone, private platform: Platform, private cdr: ChangeDetectorRef) { } ngOnInit(): void { diff --git a/components/radio/nz-radio-button.component.html b/components/radio/nz-radio-button.component.html index 1a3e2402dcd..5ad88e5cc4a 100644 --- a/components/radio/nz-radio-button.component.html +++ b/components/radio/nz-radio-button.component.html @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/components/radio/nz-radio-button.component.ts b/components/radio/nz-radio-button.component.ts index cf85515e1a2..00a645142f4 100644 --- a/components/radio/nz-radio-button.component.ts +++ b/components/radio/nz-radio-button.component.ts @@ -1,29 +1,45 @@ import { DOCUMENT } from '@angular/common'; import { + forwardRef, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, + ElementRef, Inject, - OnInit, - Optional, - Renderer2 + Renderer2, + ViewEncapsulation } from '@angular/core'; -import { NzRadioGroupComponent } from './nz-radio-group.component'; +import { FocusMonitor } from '@angular/cdk/a11y'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NzRadioComponent } from './nz-radio.component'; @Component({ selector : '[nz-radio-button]', + providers : [ + { + provide : NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NzRadioComponent), + multi : true + }, + { + provide : NzRadioComponent, + useExisting: forwardRef(() => NzRadioButtonComponent) + } + ], + encapsulation : ViewEncapsulation.None, + changeDetection : ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, templateUrl : './nz-radio-button.component.html', host : { '[class.ant-radio-button-wrapper]' : 'true', - '[class.ant-radio-button-wrapper-checked]' : 'nzChecked', + '[class.ant-radio-button-wrapper-checked]' : 'checked', '[class.ant-radio-button-wrapper-disabled]': 'nzDisabled' } }) -export class NzRadioButtonComponent extends NzRadioComponent implements OnInit { - prefixCls = 'ant-radio-button'; +export class NzRadioButtonComponent extends NzRadioComponent { /* tslint:disable-next-line:no-any */ - constructor(@Optional() nzRadioGroup: NzRadioGroupComponent, renderer: Renderer2, @Inject(DOCUMENT) document: any) { - super(nzRadioGroup, renderer, document); + constructor(elementRef: ElementRef, renderer: Renderer2, @Inject(DOCUMENT) document: any, cdr: ChangeDetectorRef, focusMonitor: FocusMonitor) { + super(elementRef, renderer, document, cdr, focusMonitor); } } diff --git a/components/radio/nz-radio-group.component.ts b/components/radio/nz-radio-group.component.ts index 53eb00cf874..8d12acc018b 100644 --- a/components/radio/nz-radio-group.component.ts +++ b/components/radio/nz-radio-group.component.ts @@ -1,147 +1,127 @@ import { forwardRef, AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, - ElementRef, - HostBinding, - Input + ContentChildren, + Input, + OnChanges, + OnDestroy, + QueryList, + SimpleChanges, + ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { merge, Subject, Subscription } from 'rxjs'; +import { startWith, takeUntil } from 'rxjs/operators'; import { NzSizeLDSType } from '../core/types/size'; import { isNotNil } from '../core/util/check'; -import { toBoolean } from '../core/util/convert'; +import { InputBoolean } from '../core/util/convert'; +import { NzRadioComponent } from './nz-radio.component'; export type NzRadioButtonStyle = 'outline' | 'solid'; -import { NzRadioButtonComponent } from './nz-radio-button.component'; -import { NzRadioComponent } from './nz-radio.component'; - @Component({ selector : 'nz-radio-group', preserveWhitespaces: false, templateUrl : './nz-radio-group.component.html', - host : { - '[class.ant-radio-group]': 'true' - }, + encapsulation : ViewEncapsulation.None, + changeDetection : ChangeDetectionStrategy.OnPush, providers : [ { provide : NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NzRadioGroupComponent), multi : true } - ] + ], + host : { + '[class.ant-radio-group]' : 'true', + '[class.ant-radio-group-large]': `nzSize === 'large'`, + '[class.ant-radio-group-small]': `nzSize === 'small'`, + '[class.ant-radio-group-solid]': `nzButtonStyle === 'solid'` + } }) -export class NzRadioGroupComponent implements AfterContentInit, ControlValueAccessor { - private _size: NzSizeLDSType = 'default'; - private _name: string; - private _disabled: boolean; - el: HTMLElement = this.elementRef.nativeElement; - value: string; - - // ngModel Access +export class NzRadioGroupComponent implements AfterContentInit, ControlValueAccessor, OnDestroy, OnChanges { + /* tslint:disable-next-line:no-any */ + private value: any; + private destroy$ = new Subject(); + private selectSubscription: Subscription; + private touchedSubscription: Subscription; onChange: (_: string) => void = () => null; onTouched: () => void = () => null; - - radios: Array = []; - - @Input() - set nzSize(value: NzSizeLDSType) { - this._size = value; - } - - get nzSize(): NzSizeLDSType { - return this._size; - } - - @Input() - set nzDisabled(value: boolean) { - this._disabled = toBoolean(value); - this.updateDisabledState(); - } - - get nzDisabled(): boolean { - return this._disabled; - } - - @Input() - set nzName(value: string) { - this._name = value; - this.updateChildrenName(); - } - - get nzName(): string { - return this._name; - } - + @ContentChildren(forwardRef(() => NzRadioComponent)) radios: QueryList; + @Input() @InputBoolean() nzDisabled: boolean; @Input() nzButtonStyle: NzRadioButtonStyle = 'outline'; - - updateDisabledState(): void { - if (isNotNil(this.nzDisabled)) { - this.radios.forEach((radio) => { - radio.nzDisabled = this.nzDisabled; + @Input() nzSize: NzSizeLDSType = 'default'; + @Input() nzName: string; + + updateChildrenStatus(): void { + if (this.radios) { + Promise.resolve().then(() => { + this.radios.forEach(radio => { + radio.checked = radio.nzValue === this.value; + if (isNotNil(this.nzDisabled)) { + radio.nzDisabled = this.nzDisabled; + } + if (this.nzName) { + radio.name = this.nzName; + } + radio.markForCheck(); + }); }); } } - updateChildrenName(): void { - if (this.nzName) { - this.radios.forEach((item) => { - item.name = this.nzName; - }); - } + constructor(private cdr: ChangeDetectorRef) { } - syncCheckedValue(): void { - this.radios.forEach((item) => { - item.nzChecked = item.nzValue === this.value; + ngAfterContentInit(): void { + this.radios.changes.pipe( + startWith(null), + takeUntil(this.destroy$) + ).subscribe(() => { + this.updateChildrenStatus(); + if (this.selectSubscription) { + this.selectSubscription.unsubscribe(); + } + this.selectSubscription = merge(...this.radios.map(radio => radio.select$)).pipe( + takeUntil(this.destroy$) + ).subscribe((radio) => { + if (this.value !== radio.nzValue) { + this.value = radio.nzValue; + this.updateChildrenStatus(); + this.onChange(this.value); + } + }); + if (this.touchedSubscription) { + this.touchedSubscription.unsubscribe(); + } + this.touchedSubscription = merge(...this.radios.map(radio => radio.touched$)).pipe( + takeUntil(this.destroy$) + ).subscribe(() => { + Promise.resolve().then(() => this.onTouched()); + }); }); - } - - @HostBinding('class.ant-radio-group-large') - get isLarge(): boolean { - return this.nzSize === 'large'; - } - - @HostBinding('class.ant-radio-group-small') - get isSmall(): boolean { - return this.nzSize === 'small'; - } - @HostBinding('class.ant-radio-group-solid') - get isSolid(): boolean { - return this.nzButtonStyle === 'solid'; } - addRadio(radio: NzRadioComponent | NzRadioButtonComponent): void { - this.radios.push(radio); - radio.nzChecked = radio.nzValue === this.value; - } - - selectRadio(radio: NzRadioComponent | NzRadioButtonComponent): void { - this.updateValue(radio.nzValue, true); - } - - updateValue(value: string, emit: boolean): void { - this.value = value; - this.syncCheckedValue(); - if (emit) { - this.onChange(value); + ngOnChanges(changes: SimpleChanges): void { + if (changes.nzDisabled || changes.nzName) { + this.updateChildrenStatus(); } } - constructor(private elementRef: ElementRef) { + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } - ngAfterContentInit(): void { - this.syncCheckedValue(); - this.updateChildrenName(); - Promise.resolve().then(() => { - this.updateDisabledState(); - }); - } - - writeValue(value: string): void { - this.updateValue(value, false); + /* tslint:disable-next-line:no-any */ + writeValue(value: any): void { + this.value = value; + this.updateChildrenStatus(); + this.cdr.markForCheck(); } registerOnChange(fn: (_: string) => void): void { @@ -154,5 +134,6 @@ export class NzRadioGroupComponent implements AfterContentInit, ControlValueAcce setDisabledState(isDisabled: boolean): void { this.nzDisabled = isDisabled; + this.cdr.markForCheck(); } } diff --git a/components/radio/nz-radio.component.html b/components/radio/nz-radio.component.html index bc1a6356b42..07d2ab9374e 100644 --- a/components/radio/nz-radio.component.html +++ b/components/radio/nz-radio.component.html @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/components/radio/nz-radio.component.ts b/components/radio/nz-radio.component.ts index d9a7c6af661..ad3b7e35dbf 100644 --- a/components/radio/nz-radio.component.ts +++ b/components/radio/nz-radio.component.ts @@ -1,84 +1,60 @@ +import { FocusMonitor } from '@angular/cdk/a11y'; import { DOCUMENT } from '@angular/common'; import { forwardRef, AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, HostListener, Inject, Input, - OnInit, - Optional, + OnChanges, Renderer2, - ViewChild + SimpleChanges, + ViewChild, + ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { toBoolean } from '../core/util/convert'; - -import { NzRadioGroupComponent } from './nz-radio-group.component'; +import { Subject } from 'rxjs'; +import { InputBoolean } from '../core/util/convert'; @Component({ selector : '[nz-radio]', preserveWhitespaces: false, templateUrl : './nz-radio.component.html', - host : { - '[class.ant-radio-wrapper]' : 'true', - '[class.ant-radio-wrapper-checked]' : 'nzChecked', - '[class.ant-radio-wrapper-disabled]': 'nzDisabled' - }, + encapsulation : ViewEncapsulation.None, + changeDetection : ChangeDetectionStrategy.OnPush, providers : [ { provide : NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NzRadioComponent), multi : true } - ] + ], + host : { + '[class.ant-radio-wrapper]' : 'true', + '[class.ant-radio-wrapper-checked]' : 'checked', + '[class.ant-radio-wrapper-disabled]': 'nzDisabled' + } }) -export class NzRadioComponent implements OnInit, ControlValueAccessor, AfterViewInit { - private _checked = false; - private _disabled = false; - private _autoFocus = false; - isInit = false; - classMap; +export class NzRadioComponent implements ControlValueAccessor, AfterViewInit, OnChanges { + select$ = new Subject(); + touched$ = new Subject(); + checked = false; name: string; - prefixCls = 'ant-radio'; - @ViewChild('inputElement') inputElement: ElementRef; + isNgModel = false; onChange: (_: boolean) => void = () => null; onTouched: () => void = () => null; - @Input() nzValue: string; - - set nzChecked(value: boolean) { - this._checked = toBoolean(value); - this.setClassMap(); - } - - get nzChecked(): boolean { - return this._checked; - } - - @Input() - set nzDisabled(value: boolean) { - this._disabled = toBoolean(value); - this.setClassMap(); - } - - get nzDisabled(): boolean { - return this._disabled; - } - - @Input() - set nzAutoFocus(value: boolean) { - this._autoFocus = toBoolean(value); - this.updateAutoFocus(); - } - - get nzAutoFocus(): boolean { - return this._autoFocus; - } + @ViewChild('inputElement') inputElement: ElementRef; + /* tslint:disable-next-line:no-any */ + @Input() nzValue: any; + @Input() @InputBoolean() nzDisabled = false; + @Input() @InputBoolean() nzAutoFocus = false; updateAutoFocus(): void { - if (this.isInit) { + if (this.inputElement) { if (this.nzAutoFocus) { this.renderer.setAttribute(this.inputElement.nativeElement, 'autofocus', 'autofocus'); } else { @@ -87,86 +63,46 @@ export class NzRadioComponent implements OnInit, ControlValueAccessor, AfterView } } - updateInputFocus(): void { - if (this.inputElement) { - if (this.nzChecked) { - if (this.document.activeElement.nodeName === 'BODY') { - this.inputElement.nativeElement.focus(); - } - } else { - this.inputElement.nativeElement.blur(); - } - } - } - - @HostListener('click', [ '$event' ]) - onClick(e: MouseEvent): void { - e.preventDefault(); - this.setClassMap(); - if (this.nzDisabled || this.nzChecked) { - this.updateInputFocus(); - return; - } else { - if (this.nzRadioGroup) { - this.nzRadioGroup.selectRadio(this); - } else { - this.updateValue(true); + @HostListener('click') + onClick(): void { + this.focus(); + if (!this.nzDisabled && !this.checked) { + this.select$.next(this); + if (this.isNgModel) { + this.checked = true; + this.onChange(true); } - this.updateInputFocus(); - } - } - - onBlur(): void { - this.onTouched(); - if (this.nzRadioGroup) { - this.nzRadioGroup.onTouched(); } } - setClassMap(): void { - this.classMap = { - [ this.prefixCls ] : true, - [ `${this.prefixCls}-checked` ] : this.nzChecked, - [ `${this.prefixCls}-disabled` ]: this.nzDisabled - }; - } - focus(): void { - this.inputElement.nativeElement.focus(); + this.focusMonitor.focusVia(this.inputElement, 'keyboard'); } blur(): void { this.inputElement.nativeElement.blur(); - this.onBlur(); - } - - /* tslint:disable-next-line:no-any */ - constructor(@Optional() public nzRadioGroup: NzRadioGroupComponent, private renderer: Renderer2, @Inject(DOCUMENT) private document: any) { } - ngOnInit(): void { - if (this.nzRadioGroup) { - this.nzRadioGroup.addRadio(this); - } - this.setClassMap(); + markForCheck(): void { + this.cdr.markForCheck(); } - updateValue(value: boolean): void { - this.onChange(value); - this.nzChecked = value; - this.setClassMap(); + /* tslint:disable-next-line:no-any */ + constructor(private elementRef: ElementRef, private renderer: Renderer2, @Inject(DOCUMENT) private document: any, private cdr: ChangeDetectorRef, private focusMonitor: FocusMonitor) { } setDisabledState(isDisabled: boolean): void { this.nzDisabled = isDisabled; + this.cdr.markForCheck(); } writeValue(value: boolean): void { - this.nzChecked = value; - this.setClassMap(); + this.checked = value; + this.cdr.markForCheck(); } registerOnChange(fn: (_: boolean) => {}): void { + this.isNgModel = true; this.onChange = fn; } @@ -175,8 +111,18 @@ export class NzRadioComponent implements OnInit, ControlValueAccessor, AfterView } ngAfterViewInit(): void { - this.isInit = true; + this.focusMonitor.monitor(this.elementRef, true).subscribe(focusOrigin => { + if (!focusOrigin) { + Promise.resolve().then(() => this.onTouched()); + this.touched$.next(); + } + }); this.updateAutoFocus(); - this.updateInputFocus(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.nzAutoFocus) { + this.updateAutoFocus(); + } } } diff --git a/components/radio/nz-radio.spec.ts b/components/radio/nz-radio.spec.ts index 266c159f4bd..93554d7ea5a 100644 --- a/components/radio/nz-radio.spec.ts +++ b/components/radio/nz-radio.spec.ts @@ -151,11 +151,13 @@ describe('radio', () => { expect(testComponent.value).toBe('A'); expect(testComponent.modelChange).toHaveBeenCalledTimes(0); })); - it('should name work', () => { + it('should name work', fakeAsync(() => { testComponent.name = 'test'; fixture.detectChanges(); + tick(); + fixture.detectChanges(); expect(radios.every(radio => radio.nativeElement.querySelector('input').name === 'test')).toBe(true); - }); + })); }); describe('radio group disabled', () => { let fixture; @@ -172,6 +174,8 @@ describe('radio', () => { it('should group disable work', fakeAsync(() => { testComponent.disabled = true; fixture.detectChanges(); + flush(); + fixture.detectChanges(); expect(testComponent.value).toBe('A'); radios[ 1 ].nativeElement.click(); fixture.detectChanges(); @@ -272,10 +276,12 @@ describe('radio', () => { expect(testComponent.formGroup.get('radioGroup').value).toBe('A'); testComponent.disable(); fixture.detectChanges(); - tick(); + flush(); fixture.detectChanges(); radios[ 1 ].nativeElement.click(); fixture.detectChanges(); + flush(); + fixture.detectChanges(); expect(testComponent.formGroup.get('radioGroup').value).toBe('A'); })); });