Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(kit): Segmented fix native reset form action #8605

Merged
merged 6 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading