Skip to content

Commit

Permalink
fix(kit): Segmented fix native reset form action (#8605)
Browse files Browse the repository at this point in the history
Co-authored-by: taiga-family-bot <[email protected]>
  • Loading branch information
waterplea and taiga-family-bot authored Aug 20, 2024
1 parent 6cba52c commit ec87062
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 110 deletions.
25 changes: 10 additions & 15 deletions projects/cdk/classes/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {ControlValueAccessor, FormControlStatus} from '@angular/forms';
import {NgControl, NgModel} from '@angular/forms';
import {EMPTY_FUNCTION} from '@taiga-ui/cdk/constants';
import {TUI_FALLBACK_VALUE} from '@taiga-ui/cdk/tokens';
import {tuiIsPresent, tuiProvide, tuiPure} from '@taiga-ui/cdk/utils';
import {tuiProvide, tuiPure} from '@taiga-ui/cdk/utils';
import {
delay,
distinctUntilChanged,
Expand All @@ -27,25 +27,24 @@ import {

import {TuiValueTransformer} from './value-transformer';

const FLAGS = {self: true, optional: true};

/**
* Basic ControlValueAccessor class to build form components upon
*/
@Directive()
export abstract class TuiControl<T> implements ControlValueAccessor {
private readonly fallback = inject(TUI_FALLBACK_VALUE, FLAGS) as T;
private readonly refresh$ = new Subject<void>();
private readonly pseudoInvalid = signal<boolean | null>(null);
private readonly internal = signal(
inject(TUI_FALLBACK_VALUE, {self: true, optional: true}) as T,
);
private readonly internal = signal(this.fallback);

protected readonly control = inject(NgControl, {self: true});
protected readonly destroyRef = inject(DestroyRef);
protected readonly cdr = inject(ChangeDetectorRef);
protected readonly transformer = inject<TuiValueTransformer<T>>(TuiValueTransformer, {
optional: true,
});
protected readonly transformer = inject(TuiValueTransformer, FLAGS);

public readonly value = computed(() => this.internal());
public readonly value = computed(() => this.internal() ?? this.fallback);
public readonly readOnly = signal(false);
public readonly touched = signal(false);
public readonly status = signal<FormControlStatus | undefined>(undefined);
Expand All @@ -72,16 +71,12 @@ export abstract class TuiControl<T> implements ControlValueAccessor {
delay(0),
startWith(null),
map(() => this.control.control),
filter(tuiIsPresent),
filter(Boolean),
distinctUntilChanged(),
switchMap((control) =>
merge(control.valueChanges, control.statusChanges),
),
switchMap((c) => merge(c.valueChanges, c.statusChanges)),
takeUntilDestroyed(this.destroyRef),
)
.subscribe(() => {
this.update();
});
.subscribe(() => this.update());
}

@Input('readOnly')
Expand Down
18 changes: 3 additions & 15 deletions projects/cdk/observables/control-value.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/// <reference types="@taiga-ui/tsconfig/ng-dev-mode" />

import type {AbstractControl, AbstractControlDirective} from '@angular/forms';
import {Observable, startWith} from 'rxjs';

Expand All @@ -9,17 +7,7 @@ import {Observable, startWith} from 'rxjs';
export function tuiControlValue<T>(
control: AbstractControl | AbstractControlDirective | null,
): Observable<T> {
return new Observable((subscriber) => {
if (!control?.valueChanges) {
throw new TuiValueChangesException();
}

return control.valueChanges.pipe(startWith(control.value)).subscribe(subscriber);
});
}

export class TuiValueChangesException extends Error {
constructor() {
super(ngDevMode ? 'Control does not have valueChanges' : '');
}
return new Observable((subscriber) =>
control?.valueChanges?.pipe(startWith(control.value)).subscribe(subscriber),
);
}
36 changes: 0 additions & 36 deletions projects/cdk/observables/test/control-value.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {fakeAsync} from '@angular/core/testing';
import type {AbstractControl} from '@angular/forms';
import {FormControl} from '@angular/forms';
import {tuiControlValue} from '@taiga-ui/cdk';
import {tuiSwitchNgDevMode} from '@taiga-ui/testing';
import {skip} from 'rxjs';

describe('tuiControlValue', () => {
Expand Down Expand Up @@ -31,38 +29,4 @@ describe('tuiControlValue', () => {

expect(actual).toBe('test');
}));

describe('dev mode', () => {
beforeEach(() => tuiSwitchNgDevMode(true));

it('throws an error if there is no valueChanges', fakeAsync(() => {
let actual = '';

tuiControlValue({} as AbstractControl).subscribe({
next: () => {},
error: (err: unknown) => {
actual = (err as Error).message;
},
});

expect(actual).toBe('Control does not have valueChanges');
}));

afterEach(() => tuiSwitchNgDevMode(false));
});

describe('production mode', () => {
it('throws an error if there is no valueChanges', fakeAsync(() => {
let actual = '';

tuiControlValue({} as AbstractControl).subscribe({
next: () => {},
error: (err: unknown) => {
actual = (err as Error).message;
},
});

expect(actual).toBe('');
}));
});
});
4 changes: 2 additions & 2 deletions projects/cdk/tokens/fallback-value.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {ValueProvider} from '@angular/core';
import {InjectionToken} from '@angular/core';
import {tuiCreateToken} from '@taiga-ui/cdk/utils/miscellaneous';

export const TUI_FALLBACK_VALUE = new InjectionToken('');
export const TUI_FALLBACK_VALUE = tuiCreateToken<any>(null);

export function tuiFallbackValueProvider<T>(useValue: T): ValueProvider {
return {
Expand Down
23 changes: 23 additions & 0 deletions projects/cdk/utils/miscellaneous/count-filled-controls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type {AbstractControl} from '@angular/forms';
import {FormArray, FormGroup} from '@angular/forms';
import {tuiToInt} from '@taiga-ui/cdk/utils/math';

import {tuiIsControlEmpty} from './is-control-empty';

export function tuiCountFilledControls(control: AbstractControl): number {
if (control instanceof FormArray) {
return control.controls.reduce(
(acc, nestedControl) => acc + tuiCountFilledControls(nestedControl),
0,
);
}

if (control instanceof FormGroup) {
return Object.values(control.controls).reduce(
(acc, nestedControl) => acc + tuiCountFilledControls(nestedControl),
0,
);
}

return tuiToInt(!tuiIsControlEmpty(control));
}
2 changes: 2 additions & 0 deletions projects/cdk/utils/miscellaneous/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './array-remove';
export * from './array-shallow-equals';
export * from './array-toggle';
export * from './change-date-separator';
export * from './count-filled-controls';
export * from './create-token';
export * from './default-sort';
export * from './directive-binding';
Expand All @@ -10,6 +11,7 @@ export * from './distance-between-touches';
export * from './ease-in-out-quad';
export * from './flat-length';
export * from './get-original-array-from-query-list';
export * from './is-control-empty';
export * from './is-falsy';
export * from './is-number';
export * from './is-object';
Expand Down
5 changes: 5 additions & 0 deletions projects/cdk/utils/miscellaneous/is-control-empty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type {AbstractControl} from '@angular/forms';

export function tuiIsControlEmpty({value = null}: AbstractControl): boolean {
return value === null || value === '' || (Array.isArray(value) && !value.length);
}
6 changes: 4 additions & 2 deletions projects/core/components/textfield/select.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {TuiTextfieldBase, TuiTextfieldDirective} from './textfield.directive';
selector: 'select[tuiTextfield]',
imports: [CommonModule],
templateUrl: './select.template.html',
changeDetection: ChangeDetectionStrategy.OnPush,
// We want this template to follow change detection to parent textfield.
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
changeDetection: ChangeDetectionStrategy.Default,
providers: [tuiProvide(TuiTextfieldDirective, TuiSelect)],
hostDirectives: [TuiNativeValidator, TuiAppearance],
host: {
Expand Down Expand Up @@ -50,7 +52,7 @@ export class TuiSelect extends TuiTextfieldBase {
}

protected get value(): string {
return this.textfield.stringify(this.control.value);
return this.textfield.stringify(this.control.value ?? '');
}

protected async onCopy(): Promise<void> {
Expand Down
18 changes: 7 additions & 11 deletions projects/core/styles/components/textfield.less
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,12 @@ tui-textfield {
}

&::before {
margin-inline-start: -0.25rem;
margin-inline-end: 0.5rem;
margin: 0 0.5rem 0 -0.125rem;
font-size: 1rem;
}

&::after {
margin-inline-end: -0.175rem;
margin-inline-start: 0.25rem;
margin: 0 -0.175rem 0 0.25rem;
font-size: 1rem;
}

Expand Down Expand Up @@ -90,13 +88,11 @@ tui-textfield {
}

&::before {
margin-inline-start: -0.125rem;
margin-inline-end: 0.375rem;
margin: 0 0.375rem 0 -0.125rem;
}

&::after {
margin-inline-start: 0.375rem;
margin-inline-end: -0.125rem;
margin: 0 -0.125rem 0 0.375rem;
}

input,
Expand Down Expand Up @@ -220,7 +216,7 @@ tui-textfield {
}
}

label:defined {
label:not([data-orientation='vertical']) {
.transition(all);
.text-overflow();

Expand All @@ -231,8 +227,8 @@ tui-textfield {
}

label:defined,
input::placeholder,
select._empty {
input:defined::placeholder,
select:defined._empty {
color: var(--tui-text-secondary);
}

Expand Down
4 changes: 2 additions & 2 deletions projects/kit/components/segmented/segmented.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {TuiSegmentedDirective} from './segmented.directive';
@Component({
standalone: true,
selector: 'tui-segmented',
template: '<ng-content></ng-content>',
template: '<ng-content />',
styleUrls: ['./segmented.style.less'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
Expand Down Expand Up @@ -53,7 +53,7 @@ export class TuiSegmented implements OnChanges {
}

public update(activeItemIndex: number): void {
if (activeItemIndex === this.activeItemIndex) {
if (activeItemIndex === this.activeItemIndex || activeItemIndex < 0) {
return;
}

Expand Down
38 changes: 11 additions & 27 deletions projects/kit/components/segmented/segmented.directive.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import {isPlatformBrowser} from '@angular/common';
import type {AfterContentChecked, AfterContentInit, QueryList} from '@angular/core';
import {
ContentChildren,
DestroyRef,
Directive,
ElementRef,
inject,
PLATFORM_ID,
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {NgControl} from '@angular/forms';
import {ContentChildren, Directive, ElementRef, inject} from '@angular/core';
import {NgControl, RadioControlValueAccessor} from '@angular/forms';
import {RouterLinkActive} from '@angular/router';
import {EMPTY_QUERY} from '@taiga-ui/cdk/constants';
import {tuiControlValue, tuiQueryListChanges} from '@taiga-ui/cdk/observables';
import {tuiInjectElement} from '@taiga-ui/cdk/utils/dom';
import {filter, merge, mergeAll, switchMap} from 'rxjs';
import {map, switchMap} from 'rxjs';

import {TuiSegmented} from './segmented.component';

Expand All @@ -28,29 +19,26 @@ export class TuiSegmentedDirective implements AfterContentChecked, AfterContentI
@ContentChildren(NgControl, {descendants: true})
private readonly controls: QueryList<NgControl> = EMPTY_QUERY;

@ContentChildren(RadioControlValueAccessor, {descendants: true})
private readonly radios: QueryList<RadioControlValueAccessor> = EMPTY_QUERY;

@ContentChildren(RouterLinkActive)
private readonly links: QueryList<RouterLinkActive> = EMPTY_QUERY;

@ContentChildren(RouterLinkActive, {read: ElementRef})
private readonly elements: QueryList<ElementRef<HTMLElement>> = EMPTY_QUERY;

private readonly destroyRef = inject(DestroyRef);
private readonly component = inject(TuiSegmented);
private readonly el = tuiInjectElement();

// TODO: Debug prerender
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

public ngAfterContentInit(): void {
tuiQueryListChanges(this.controls)
.pipe(
switchMap((controls) => merge(controls.map((c) => tuiControlValue(c)))),
mergeAll(),
filter(() => this.isBrowser),
takeUntilDestroyed(this.destroyRef),
switchMap(() => tuiControlValue(this.controls.first)),
map((value) => this.radios.toArray().findIndex((c) => c.value === value)),
)
.subscribe(() => {
this.update(this.el.querySelector(':checked'));
.subscribe((index) => {
this.component.update(index);
});
}

Expand All @@ -61,11 +49,7 @@ export class TuiSegmentedDirective implements AfterContentChecked, AfterContentI
}

protected update(target: Element | null): void {
const index = this.getIndex(target);

if (index >= 0) {
this.component.update(index);
}
this.component.update(this.getIndex(target));
}

private get linkIndex(): number {
Expand Down

0 comments on commit ec87062

Please sign in to comment.