Skip to content

Commit fb0635b

Browse files
fix(module:segmented): fix emitting unnecessary value changed events (#9125)
1 parent b78b99f commit fb0635b

4 files changed

Lines changed: 154 additions & 67 deletions

File tree

components/segmented/segmented-item.component.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
OnInit,
1616
ViewEncapsulation
1717
} from '@angular/core';
18-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
18+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
1919
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
2020

2121
import { NzIconModule } from 'ng-zorro-antd/icon';
@@ -46,7 +46,7 @@ import { NzSegmentedService } from './segmented.service';
4646
host: {
4747
class: 'ant-segmented-item',
4848
'[class.ant-segmented-item-selected]': 'isChecked',
49-
'[class.ant-segmented-item-disabled]': 'nzDisabled',
49+
'[class.ant-segmented-item-disabled]': 'nzDisabled || parentDisabled()',
5050
'(click)': 'handleClick()'
5151
},
5252
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -61,16 +61,13 @@ export class NzSegmentedItemComponent implements OnInit {
6161

6262
private readonly service = inject(NzSegmentedService);
6363

64+
readonly parentDisabled = toSignal(this.service.disabled$, { initialValue: false });
65+
6466
constructor(
6567
private cdr: ChangeDetectorRef,
6668
private elementRef: ElementRef,
6769
private destroyRef: DestroyRef
68-
) {
69-
this.service.disabled$.pipe(takeUntilDestroyed()).subscribe(disabled => {
70-
this.nzDisabled = disabled;
71-
this.cdr.markForCheck();
72-
});
73-
}
70+
) {}
7471

7572
ngOnInit(): void {
7673
this.service.selected$
@@ -99,8 +96,9 @@ export class NzSegmentedItemComponent implements OnInit {
9996
}
10097

10198
handleClick(): void {
102-
if (!this.nzDisabled) {
99+
if (!this.nzDisabled && !this.parentDisabled()) {
103100
this.service.selected$.next(this.nzValue);
101+
this.service.change$.next(this.nzValue);
104102
}
105103
}
106104
}

components/segmented/segmented.component.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
115115

116116
this.service.selected$.pipe(takeUntilDestroyed()).subscribe(value => {
117117
this.value = value;
118+
});
119+
120+
this.service.change$.pipe(takeUntilDestroyed()).subscribe(value => {
118121
this.nzValueChange.emit(value);
119122
this.onChange(value);
120123
});
@@ -141,8 +144,7 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
141144
}
142145

143146
if (
144-
this.value === null ||
145-
this.value === undefined ||
147+
this.value === undefined || // If no value is set, select the first item
146148
!itemCmps.some(item => item.nzValue === this.value) // handle value not in options
147149
) {
148150
this.service.selected$.next(itemCmps[0].nzValue);
@@ -168,7 +170,6 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
168170
}
169171

170172
writeValue(value: number | string): void {
171-
if (value === null || value === undefined) return;
172173
this.service.selected$.next(value);
173174
}
174175

components/segmented/segmented.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import { ReplaySubject, Subject } from 'rxjs';
1111
export class NzSegmentedService implements OnDestroy {
1212
readonly selected$ = new ReplaySubject<string | number>(1);
1313
readonly activated$ = new ReplaySubject<HTMLElement>(1);
14+
readonly change$ = new Subject<string | number>();
1415
readonly disabled$ = new ReplaySubject<boolean>(1);
1516
readonly animationDone$ = new Subject<AnimationEvent>();
1617

1718
ngOnDestroy(): void {
1819
this.selected$.complete();
1920
this.activated$.complete();
21+
this.change$.complete();
2022
this.disabled$.complete();
2123
this.animationDone$.complete();
2224
}

components/segmented/segmented.spec.ts

Lines changed: 141 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { Component, DebugElement } from '@angular/core';
7-
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
7+
import { ComponentFixture, TestBed } from '@angular/core/testing';
88
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
99
import { By } from '@angular/platform-browser';
1010
import { provideNoopAnimations } from '@angular/platform-browser/animations';
@@ -32,87 +32,159 @@ describe('nz-segmented', () => {
3232
});
3333
fixture = TestBed.createComponent(NzSegmentedTestComponent);
3434
component = fixture.componentInstance;
35+
spyOn(component, 'handleValueChange');
3536
segmentedComponent = fixture.debugElement.query(By.directive(NzSegmentedComponent));
3637
fixture.detectChanges();
3738
});
3839

3940
it('should support block mode', () => {
40-
expect((segmentedComponent.nativeElement as HTMLElement).classList.contains('ant-segmented-block')).toBeFalse();
41+
const segmentedElement: HTMLElement = segmentedComponent.nativeElement;
42+
expect(segmentedElement.classList).not.toContain('ant-segmented-block');
4143
component.block = true;
4244
fixture.detectChanges();
43-
expect((segmentedComponent.nativeElement as HTMLElement).classList.contains('ant-segmented-block')).toBeTrue();
45+
expect(segmentedElement.classList).toContain('ant-segmented-block');
4446
});
4547

46-
it('should be auto selected the first option', async () => {
48+
it('should support size', () => {
49+
const segmentedElement: HTMLElement = segmentedComponent.nativeElement;
50+
component.size = 'large';
51+
fixture.detectChanges();
52+
expect(segmentedElement.classList).toContain('ant-segmented-lg');
53+
component.size = 'small';
54+
fixture.detectChanges();
55+
expect(segmentedElement.classList).toContain('ant-segmented-sm');
56+
});
57+
58+
it('should be auto selected the first option when if no value is set', async () => {
4759
const theFirstElement = getSegmentedOptionByIndex(0);
4860
await fixture.whenStable();
4961
fixture.detectChanges();
50-
expect(component.value).toBe(1);
51-
expect(theFirstElement.classList.contains('ant-segmented-item-selected')).toBeTrue();
62+
expect(theFirstElement.classList).toContain('ant-segmented-item-selected');
5263
});
5364

54-
it('should emit when value changes', fakeAsync(() => {
55-
spyOn(component, 'handleValueChange');
56-
65+
it('should be change the value and emit an event by clicking', async () => {
5766
const theFirstElement = getSegmentedOptionByIndex(0);
58-
const theThirdElement = getSegmentedOptionByIndex(2);
59-
dispatchMouseEvent(theThirdElement.querySelector('.ant-segmented-item-label')!, 'click');
67+
const theSecondElement = getSegmentedOptionByIndex(1);
68+
69+
await fixture.whenStable();
6070
fixture.detectChanges();
61-
tick(400);
71+
72+
expect(theFirstElement.classList).toContain('ant-segmented-item-selected');
73+
expect(component.handleValueChange).toHaveBeenCalledTimes(0);
74+
75+
dispatchMouseEvent(theSecondElement, 'click');
76+
await fixture.whenStable();
6277
fixture.detectChanges();
63-
expect(component.value).toBe(3);
64-
expect(theFirstElement.classList.contains('ant-segmented-item-selected')).toBeFalse();
65-
expect(theThirdElement.classList.contains('ant-segmented-item-selected')).toBeTrue();
66-
expect(component.handleValueChange).toHaveBeenCalledWith(3);
78+
79+
expect(theFirstElement.classList).not.toContain('ant-segmented-item-selected');
80+
expect(theSecondElement.classList).toContain('ant-segmented-item-selected');
6781
expect(component.handleValueChange).toHaveBeenCalledTimes(1);
82+
expect(component.handleValueChange).toHaveBeenCalledWith(2);
83+
});
6884

69-
component.value = 2;
85+
it('should support object options', async () => {
86+
component.options = [
87+
'Daily',
88+
{ label: 'Weekly', value: 'Weekly', disabled: true },
89+
'Monthly',
90+
{ label: 'Quarterly', value: 'Quarterly', disabled: true },
91+
'Yearly'
92+
];
7093
fixture.detectChanges();
71-
tick(400);
94+
await fixture.whenStable();
7295
fixture.detectChanges();
96+
97+
const theFirstElement = getSegmentedOptionByIndex(0);
7398
const theSecondElement = getSegmentedOptionByIndex(1);
74-
expect(segmentedComponent.componentInstance.value).toBe(2);
75-
expect(component.handleValueChange).toHaveBeenCalledTimes(2);
76-
expect(theSecondElement.classList.contains('ant-segmented-item-selected')).toBeTrue();
77-
}));
99+
const theThirdElement = getSegmentedOptionByIndex(2);
78100

79-
it('should support disabled mode', fakeAsync(() => {
80-
component.disabled = true;
81-
fixture.detectChanges();
101+
expect(theFirstElement.classList).toContain('ant-segmented-item-selected');
102+
expect(theSecondElement.classList).not.toContain('ant-segmented-item-selected');
82103

83-
const theThirdElement = getSegmentedOptionByIndex(2);
84-
dispatchMouseEvent(theThirdElement.querySelector('.ant-segmented-item-label')!, 'click');
104+
dispatchMouseEvent(theSecondElement, 'click');
105+
await fixture.whenStable();
85106
fixture.detectChanges();
86-
tick(400);
107+
108+
expect(theFirstElement.classList).toContain('ant-segmented-item-selected');
109+
expect(theSecondElement.classList).not.toContain('ant-segmented-item-selected');
110+
111+
dispatchMouseEvent(theThirdElement, 'click');
112+
await fixture.whenStable();
87113
fixture.detectChanges();
88-
expect(component.value).toBe(1);
89114

90-
component.disabled = false;
115+
expect(theFirstElement.classList).not.toContain('ant-segmented-item-selected');
116+
expect(theThirdElement.classList).toContain('ant-segmented-item-selected');
117+
});
118+
119+
it('should support disabled mode', async () => {
120+
const theFirstElement = getSegmentedOptionByIndex(0);
121+
const theSecondElement = getSegmentedOptionByIndex(1);
122+
123+
component.disabled = true;
91124
fixture.detectChanges();
92-
dispatchMouseEvent(theThirdElement.querySelector('.ant-segmented-item-label')!, 'click');
125+
await fixture.whenStable();
93126
fixture.detectChanges();
94-
tick(400);
127+
128+
dispatchMouseEvent(theSecondElement, 'click');
129+
await fixture.whenStable();
95130
fixture.detectChanges();
96-
expect(component.value).toBe(3);
97131

98-
component.options = [
99-
'Daily',
100-
{ label: 'Weekly', value: 'Weekly', disabled: true },
101-
'Monthly',
102-
{ label: 'Quarterly', value: 'Quarterly', disabled: true },
103-
'Yearly'
104-
];
132+
expect(theFirstElement.classList).toContain('ant-segmented-item-selected');
133+
expect(theSecondElement.classList).not.toContain('ant-segmented-item-selected');
134+
});
135+
});
136+
137+
describe('ng model', () => {
138+
let fixture: ComponentFixture<NzSegmentedNgModelTestComponent>;
139+
let component: NzSegmentedNgModelTestComponent;
140+
let segmentedComponent: DebugElement;
141+
142+
function getSegmentedOptionByIndex(index: number): HTMLElement {
143+
return segmentedComponent.nativeElement.querySelectorAll('.ant-segmented-item')[index];
144+
}
145+
146+
beforeEach(() => {
147+
TestBed.configureTestingModule({
148+
providers: [provideNoopAnimations()]
149+
});
150+
fixture = TestBed.createComponent(NzSegmentedNgModelTestComponent);
151+
component = fixture.componentInstance;
152+
spyOn(component, 'handleValueChange');
153+
segmentedComponent = fixture.debugElement.query(By.directive(NzSegmentedComponent));
105154
fixture.detectChanges();
155+
});
106156

157+
it('should be support two-way binding', async () => {
158+
const theFirstElement = getSegmentedOptionByIndex(0);
107159
const theSecondElement = getSegmentedOptionByIndex(1);
108-
dispatchMouseEvent(theSecondElement.querySelector('.ant-segmented-item-label')!, 'click');
160+
161+
await fixture.whenStable();
109162
fixture.detectChanges();
110-
tick(400);
163+
164+
expect(theFirstElement.classList).toContain('ant-segmented-item-selected');
165+
expect(component.handleValueChange).toHaveBeenCalledTimes(0);
166+
167+
component.value = 2;
168+
fixture.detectChanges();
169+
await fixture.whenStable();
111170
fixture.detectChanges();
112-
expect(component.value).toBe('Daily');
113-
}));
171+
172+
expect(theFirstElement.classList).not.toContain('ant-segmented-item-selected');
173+
expect(theSecondElement.classList).toContain('ant-segmented-item-selected');
174+
expect(component.handleValueChange).toHaveBeenCalledTimes(0);
175+
176+
dispatchMouseEvent(theFirstElement, 'click');
177+
await fixture.whenStable();
178+
fixture.detectChanges();
179+
180+
expect(theFirstElement.classList).toContain('ant-segmented-item-selected');
181+
expect(theSecondElement.classList).not.toContain('ant-segmented-item-selected');
182+
expect(component.value).toBe(1);
183+
expect(component.handleValueChange).toHaveBeenCalledTimes(1);
184+
});
114185
});
115-
describe('in reactive form', () => {
186+
187+
describe('reactive form', () => {
116188
let fixture: ComponentFixture<NzSegmentedInReactiveFormTestComponent>;
117189
let component: NzSegmentedInReactiveFormTestComponent;
118190
let segmentedComponent: DebugElement;
@@ -131,16 +203,19 @@ describe('nz-segmented', () => {
131203
fixture.detectChanges();
132204
});
133205

134-
it('first change form control value should work', fakeAsync(() => {
135-
expect(component.formControl.value).toBe('Weekly');
206+
it('first change form control value should work', async () => {
207+
const theSecondElement = getSegmentedOptionByIndex(1);
136208
const theThirdElement = getSegmentedOptionByIndex(2);
137-
dispatchMouseEvent(theThirdElement.querySelector('.ant-segmented-item-label')!, 'click');
138-
tick(400);
209+
210+
expect(component.formControl.value).toBe('Weekly');
211+
212+
dispatchMouseEvent(theThirdElement, 'click');
213+
await fixture.whenStable();
139214
fixture.detectChanges();
140-
const theSecondElement = getSegmentedOptionByIndex(1);
141-
expect(theSecondElement.classList.contains('ant-segmented-item-selected')).toBeFalse();
142-
expect(theThirdElement.classList.contains('ant-segmented-item-selected')).toBeTrue();
143-
}));
215+
216+
expect(theSecondElement.classList).not.toContain('ant-segmented-item-selected');
217+
expect(theThirdElement.classList).toContain('ant-segmented-item-selected');
218+
});
144219
});
145220
});
146221

@@ -150,7 +225,6 @@ describe('nz-segmented', () => {
150225
<nz-segmented
151226
[nzSize]="size"
152227
[nzOptions]="options"
153-
[(ngModel)]="value"
154228
[nzDisabled]="disabled"
155229
[nzBlock]="block"
156230
(nzValueChange)="handleValueChange($event)"
@@ -160,7 +234,6 @@ describe('nz-segmented', () => {
160234
export class NzSegmentedTestComponent {
161235
size: NzSizeLDSType = 'default';
162236
options: NzSegmentedOptions = [1, 2, 3];
163-
value?: number | string;
164237
block = false;
165238
disabled = false;
166239

@@ -169,6 +242,19 @@ export class NzSegmentedTestComponent {
169242
}
170243
}
171244

245+
@Component({
246+
imports: [FormsModule, NzSegmentedModule],
247+
template: ` <nz-segmented [nzOptions]="options" [(ngModel)]="value" (ngModelChange)="handleValueChange($event)" /> `
248+
})
249+
export class NzSegmentedNgModelTestComponent {
250+
options: NzSegmentedOptions = [1, 2, 3];
251+
value: number | string = 1;
252+
253+
handleValueChange(_e: string | number): void {
254+
// empty
255+
}
256+
}
257+
172258
@Component({
173259
imports: [ReactiveFormsModule, NzSegmentedModule],
174260
template: `<nz-segmented [nzOptions]="options" [formControl]="formControl"></nz-segmented>`

0 commit comments

Comments
 (0)