diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d51df20e15..29f978e4375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,11 +33,14 @@ All notable changes for each version of this project will be documented in this ### Behavioral Changes +- `IgxCheckboxComponent`, `IgxSwitchComponent`, `IgxRadioComponent` - the three components now use `ChangeDetectionStrategy.OnPush` instead of `Eager`. Their internal state is backed by Angular signals, so each component marks itself for check whenever that state is written, and is no longer re-checked on every application-wide change detection pass. The public `@Input()`/`@Output()` API is unchanged, and the components keep reflecting state written directly on the instance, e.g. `checkbox.checked = true`. - **Theming** - Scrollbar arrow buttons cannot be styled or enabled through the standard properties, and `scrollbar-width: thin` removes them where the platform draws them. - **Firefox** - The `scrollbar-color` and `scrollbar-width` properties are not supported on Firefox versions prior to 64, so the scrollbars in those versions will render with the platform default colors and size. ### Bug Fixes +- `IgxRadioGroupDirective` + - The group's subscriptions to a radio button's events are now released when that button itself is destroyed, instead of living until the whole group is destroyed. Previously, radio buttons added and removed dynamically - for example through `@for` - leaked a subscription per button for the lifetime of the group. - `IgxCheckboxComponent` - Fixed the tick-mark icon rendering with the Indigo shape (rounded rect + custom path) inside CSS-scoped subtrees that use a different design system than the application's global theme, e.g. a `material`-themed widget nested inside an `indigo`-themed app. Both tick-mark variants are now always rendered and toggled purely via CSS (`@container style(--ig-theme: indigo)`), removing the dependency on JS-side theme detection that could go stale in nested/multi-theme scenarios (#15021). - **Ripple** diff --git a/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.html b/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.html index a2fce07ba00..532dd5f2f86 100644 --- a/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.html +++ b/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.html @@ -1,25 +1,25 @@
+ [class]="_labelClass()" + [id]="_labelId()"> diff --git a/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.ts b/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.ts index 074fd84d7b9..ef9e9a75113 100644 --- a/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.ts +++ b/projects/igniteui-angular/checkbox/src/checkbox/checkbox.component.ts @@ -1,11 +1,11 @@ import { - Component, - HostBinding, - Input, - AfterViewInit, - booleanAttribute, - ChangeDetectionStrategy, - ViewEncapsulation + Component, + Input, + AfterViewInit, + booleanAttribute, + ChangeDetectionStrategy, + ViewEncapsulation, + signal } from '@angular/core'; import { CheckboxBaseDirective, IgxRippleDirective } from 'igniteui-angular/directives'; import { ControlValueAccessor } from '@angular/forms'; @@ -46,12 +46,23 @@ import { EditorProvider, EDITOR_PROVIDER } from 'igniteui-angular/core'; templateUrl: 'checkbox.component.html', styleUrl: 'checkbox.component.css', encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [IgxRippleDirective], + host: { + '[class.igx-checkbox]': 'cssClass', + '[class.igx-checkbox--focused]': 'focused', + '[class.igx-checkbox--indeterminate]': 'indeterminate', + '[class.igx-checkbox--checked]': 'checked', + '[class.igx-checkbox--disabled]': 'disabled', + '[class.igx-checkbox--invalid]': 'invalid', + '[class.igx-checkbox--plain]': '_disableTransitions()', + }, }) export class IgxCheckboxComponent extends CheckboxBaseDirective implements AfterViewInit, ControlValueAccessor, EditorProvider { + protected readonly _disableTransitions = signal(false); + /** * Returns the class of the checkbox component. * @@ -60,7 +71,6 @@ export class IgxCheckboxComponent * let class = this.checkbox.cssClass; * ``` */ - @HostBinding('class.igx-checkbox') public override cssClass = 'igx-checkbox'; /** @@ -75,8 +85,12 @@ export class IgxCheckboxComponent * let isFocused = this.checkbox.focused; * ``` */ - @HostBinding('class.igx-checkbox--focused') - public override focused = false; + public override get focused() { + return super.focused; + } + public override set focused(value: boolean) { + super.focused = value; + } /** * Sets/gets the checkbox indeterminate visual state. @@ -90,9 +104,13 @@ export class IgxCheckboxComponent * let isIndeterminate = this.checkbox.indeterminate; * ``` */ - @HostBinding('class.igx-checkbox--indeterminate') @Input({ transform: booleanAttribute }) - public override indeterminate = false; + public override get indeterminate() { + return super.indeterminate; + } + public override set indeterminate(value: boolean) { + super.indeterminate = value; + } /** * Sets/gets whether the checkbox is checked. @@ -106,7 +124,6 @@ export class IgxCheckboxComponent * let isChecked = this.checkbox.checked; * ``` */ - @HostBinding('class.igx-checkbox--checked') @Input({ transform: booleanAttribute }) public override set checked(value: boolean) { super.checked = value; @@ -127,9 +144,13 @@ export class IgxCheckboxComponent * let isDisabled = this.checkbox.disabled; * ``` */ - @HostBinding('class.igx-checkbox--disabled') @Input({ transform: booleanAttribute }) - public override disabled = false; + public override get disabled() { + return super.disabled; + } + public override set disabled(value: boolean) { + super.disabled = value; + } /** * Sets/gets whether the checkbox is invalid. @@ -143,9 +164,13 @@ export class IgxCheckboxComponent * let isInvalid = this.checkbox.invalid; * ``` */ - @HostBinding('class.igx-checkbox--invalid') @Input({ transform: booleanAttribute }) - public override invalid = false; + public override get invalid() { + return super.invalid; + } + public override set invalid(value: boolean) { + super.invalid = value; + } /** * Sets/gets whether the checkbox is readonly. @@ -160,7 +185,12 @@ export class IgxCheckboxComponent * ``` */ @Input({ transform: booleanAttribute }) - public override readonly = false; + public override get readonly() { + return super.readonly; + } + public override set readonly(value: boolean) { + super.readonly = value; + } /** * Sets/gets whether the checkbox should disable all css transitions. @@ -174,7 +204,11 @@ export class IgxCheckboxComponent * let disableTransitions = this.checkbox.disableTransitions; * ``` */ - @HostBinding('class.igx-checkbox--plain') @Input({ transform: booleanAttribute }) - public disableTransitions = false; + public get disableTransitions() { + return this._disableTransitions(); + } + public set disableTransitions(value: boolean) { + this._disableTransitions.set(value); + } } diff --git a/projects/igniteui-angular/directives/src/directives/checkbox/checkbox-base.directive.ts b/projects/igniteui-angular/directives/src/directives/checkbox/checkbox-base.directive.ts index d0ea269bab1..82c75217eac 100644 --- a/projects/igniteui-angular/directives/src/directives/checkbox/checkbox-base.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/checkbox/checkbox-base.directive.ts @@ -1,8 +1,8 @@ -import { Directive, EventEmitter, HostListener, HostBinding, Input, Output, ViewChild, ElementRef, ChangeDetectorRef, booleanAttribute, inject, AfterViewInit, Injector } from '@angular/core'; +import { Directive, EventEmitter, Input, Output, ViewChild, ElementRef, ChangeDetectorRef, booleanAttribute, inject, AfterViewInit, Injector, signal, computed, DestroyRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { NgControl } from '@angular/forms'; import { IBaseEventArgs, NgControlAdapter } from 'igniteui-angular/core'; -import { noop, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { noop } from 'rxjs'; export const LabelPosition = { BEFORE: 'before', @@ -17,12 +17,61 @@ export interface IChangeCheckboxEventArgs extends IBaseEventArgs { let nextId = 0; -@Directive() -export abstract class CheckboxBaseDirective implements AfterViewInit { +@Directive({ + host: { + '[attr.id]': '_id()', + '(keyup)': 'onKeyUp($event)', + '(click)': '_onCheckboxClick($event)', + '(blur)': 'onBlur()', + } +}) +export class CheckboxBaseDirective implements AfterViewInit { protected cdr = inject(ChangeDetectorRef); + + /** + * @hidden + * @internal + */ + public destroyRef = inject(DestroyRef); + public ngControl = inject(NgControl, { optional: true, self: true }); private control = NgControlAdapter.from(this.ngControl, inject(Injector)); + // Internal state. + // `_labelId` and `_ariaLabelledBy` snapshot `id`/`labelId` once on + // initialization - deliberately not derived, so that a later write to `id` + // never clobbers a caller-provided `labelId`. + protected readonly _id = signal(`igx-checkbox-${nextId++}`); + protected readonly _labelId = signal(`${this.id}-label`); + protected readonly _ariaLabelledBy = signal(this.labelId); + protected readonly _ariaLabel = signal(null); + protected readonly _checked = signal(false); + protected readonly _required = signal(false); + protected readonly _disabled = signal(false); + protected readonly _readonly = signal(false); + protected readonly _indeterminate = signal(false); + protected readonly _focused = signal(false); + protected readonly _invalid = signal(false); + protected readonly _value = signal(undefined); + protected readonly _name = signal(undefined!); + protected readonly _tabindex = signal(null!); + protected readonly _labelPosition = signal(LabelPosition.AFTER); + protected readonly _disableRipple = signal(false); + + // Derived view state, consumed by the templates. + protected readonly _ariaChecked = computed(() => + this._indeterminate() ? 'mixed' : this._checked() + ); + + // `cssClass` is a per-subclass constant rather than a signal, so it is read + // once and memoized. That is safe only because the first read happens while + // rendering, after the subclass field initializer has assigned it. + protected readonly _labelClass = computed(() => + this._labelPosition() === LabelPosition.BEFORE + ? `${this.cssClass}__label ${this.cssClass}__label--before` + : `${this.cssClass}__label` + ); + /** * An event that is emitted after the checkbox state is changed. * Provides references to the checkbox and the `checked` property as event arguments. @@ -31,12 +80,6 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { @Output() public readonly change: EventEmitter = new EventEmitter(); - /** - * @hidden - * @internal - */ - public destroy$ = new Subject(); - /** * Returns reference to the native checkbox element. * @@ -60,21 +103,51 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { public nativeLabel!: ElementRef; public cssClass!: string; - public abstract disabled: boolean; - public readonly!: boolean; - public indeterminate!: boolean; - public focused!: boolean; - public invalid!: boolean; + + public get disabled(): boolean { + return this._disabled(); + } + public set disabled(value: boolean) { + this._disabled.set(value); + } + + public get readonly(): boolean { + return this._readonly(); + } + public set readonly(value: boolean) { + this._readonly.set(value); + } + + public get indeterminate(): boolean { + return this._indeterminate(); + } + public set indeterminate(value: boolean) { + this._indeterminate.set(value); + } + + public get focused(): boolean { + return this._focused(); + } + public set focused(value: boolean) { + this._focused.set(value); + } + + public get invalid(): boolean { + return this._invalid(); + } + public set invalid(value: boolean) { + this._invalid.set(value); + } @Input({ transform: booleanAttribute }) public get checked() { - return this._checked; + return this._checked(); } public set checked(value: boolean) { - if (this._checked !== value) { - this._checked = value; - this._onChangeCallback(this._checked); + if (this._checked() !== value) { + this._checked.set(value); + this._onChangeCallback(value); } } @@ -113,9 +186,13 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { * let checkboxId = this.checkbox.id; * ``` */ - @HostBinding('attr.id') @Input() - public id = `igx-checkbox-${nextId++}`; + public get id() { + return this._id(); + } + public set id(value: string) { + this._id.set(value); + } /** * Sets/gets the id of the `label` element. @@ -129,7 +206,13 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { * let labelId = this.component.labelId; * ``` */ - @Input() public labelId = `${this.id}-label`; + @Input() + public get labelId() { + return this._labelId(); + } + public set labelId(value: string) { + this._labelId.set(value); + } /** * Sets/gets the `value` attribute. @@ -142,7 +225,13 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { * let value = this.checkbox.value; * ``` */ - @Input() public value: any; + @Input() + public get value() { + return this._value(); + } + public set value(value: any) { + this._value.set(value); + } /** * Sets/gets the `name` attribute. @@ -155,7 +244,13 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { * let name = this.checkbox.name; * ``` */ - @Input() public name!: string; + @Input() + public get name() { + return this._name(); + } + public set name(value: string) { + this._name.set(value); + } /** * Sets/gets the value of the `tabindex` attribute. @@ -168,7 +263,13 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { * let tabIndex = this.checkbox.tabindex; * ``` */ - @Input() public tabindex: number = null!; + @Input() + public get tabindex() { + return this._tabindex(); + } + public set tabindex(value: number) { + this._tabindex.set(value); + } /** * Sets/gets the position of the `label`. @@ -183,7 +284,12 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { * ``` */ @Input() - public labelPosition: LabelPosition | string = LabelPosition.AFTER; + public get labelPosition() { + return this._labelPosition(); + } + public set labelPosition(value: LabelPosition | string) { + this._labelPosition.set(value); + } /** * Enables/Disables the ripple effect. @@ -198,7 +304,12 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { * ``` */ @Input({ transform: booleanAttribute }) - public disableRipple = false; + public get disableRipple() { + return this._disableRipple(); + } + public set disableRipple(value: boolean) { + this._disableRipple.set(value); + } /** * Sets/gets the `aria-labelledby` attribute. @@ -213,7 +324,12 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { * ``` */ @Input('aria-labelledby') - public ariaLabelledBy = this.labelId; + public get ariaLabelledBy() { + return this._ariaLabelledBy(); + } + public set ariaLabelledBy(value: string) { + this._ariaLabelledBy.set(value); + } /** * Sets/gets the value of the `aria-label` attribute. @@ -227,7 +343,12 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { * ``` */ @Input('aria-label') - public ariaLabel: string | null = null; + public get ariaLabel() { + return this._ariaLabel(); + } + public set ariaLabel(value: string | null) { + this._ariaLabel.set(value); + } constructor() { if (this.ngControl !== null) { @@ -249,13 +370,13 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { */ @Input({ transform: booleanAttribute }) public get required(): boolean { - return this._required || this.nativeElement.hasAttribute('required'); + return this._required() || this.nativeElement.hasAttribute('required'); } public set required(value: boolean) { if (!value) { this.nativeElement.removeAttribute('required'); } - this._required = value; + this._required.set(value); } /** @@ -265,11 +386,11 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { public ngAfterViewInit() { if (this.control) { this.control.statusChanges - .pipe(takeUntil(this.destroy$)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(this.updateValidityState.bind(this)); if (this.control.hasValidators) { - this._required = this.control.required; + this._required.set(this.control.required); this.cdr.detectChanges(); } } @@ -291,27 +412,13 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { */ private _onTouchedCallback: () => void = noop; - /** - * @hidden - * @internal - */ - protected _checked = false; - - /** - * @hidden - * @internal - */ - public _required = false; - /** @hidden @internal */ - @HostListener('keyup', ['$event']) public onKeyUp(event: KeyboardEvent) { event.stopPropagation(); - this.focused = true; + this._focused.set(true); } /** @hidden @internal */ - @HostListener('click', ['$event']) public _onCheckboxClick(event: PointerEvent | MouseEvent) { // Since the original checkbox is hidden and the label // is used for styling and to change the checked state of the checkbox, @@ -319,7 +426,7 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { // as it gets triggered on label click // NOTE: The above is no longer valid, as the native checkbox is not labeled // by the SVG anymore. - if (this.disabled || this.readonly) { + if (this._disabled() || this._readonly()) { // readonly prevents the component from changing state (see toggle() method). // However, the native checkbox can still be activated through user interaction (focus + space, label click) // Prevent the native change so the input remains in sync @@ -329,32 +436,20 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { this.nativeElement.focus(); - this.indeterminate = false; - this.checked = !this.checked; + this._indeterminate.set(false); + this.checked = !this._checked(); this.updateValidityState(); // K.D. March 23, 2021 Emitting on click and not on the setter because otherwise every component // bound on change would have to perform self checks for weather the value has changed because // of the initial set on initialization this.change.emit({ - checked: this.checked, - value: this.value, + checked: this._checked(), + value: this._value(), owner: this, }); } - /** - * @hidden - * @internal - */ - public get ariaChecked() { - if (this.indeterminate) { - return 'mixed'; - } else { - return this.checked; - } - } - /** @hidden @internal */ public _onCheckboxChange(event: Event) { // We have to stop the original checkbox change event @@ -363,27 +458,15 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { } /** @hidden @internal */ - @HostListener('blur') public onBlur() { - this.focused = false; + this._focused.set(false); this._onTouchedCallback(); this.updateValidityState(); } /** @hidden @internal */ public writeValue(value: boolean) { - this._checked = value; - } - - /** @hidden @internal */ - public get labelClass(): string { - switch (this.labelPosition) { - case LabelPosition.BEFORE: - return `${this.cssClass}__label ${this.cssClass}__label--before`; - case LabelPosition.AFTER: - default: - return `${this.cssClass}__label`; - } + this._checked.set(value); } /** @hidden @internal */ @@ -398,7 +481,7 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { /** @hidden @internal */ public setDisabledState(isDisabled: boolean) { - this.disabled = isDisabled; + this._disabled.set(isDisabled); } /** @hidden @internal */ @@ -412,13 +495,13 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { */ protected updateValidityState() { if (this.control) { - if (!this.disabled && !this.readonly && this.control.touchedOrDirty) { + if (!this._disabled() && !this._readonly() && this.control.touchedOrDirty) { // the control is not disabled and is touched or dirty - this.invalid = this.control.invalid; + this._invalid.set(this.control.invalid); } else { // if the control is untouched, pristine, or disabled, its state is initial. This is when the user did not interact // with the checkbox or when the form/control is reset - this.invalid = false; + this._invalid.set(false); } } else { this.checkNativeValidity(); @@ -434,14 +517,14 @@ export abstract class CheckboxBaseDirective implements AfterViewInit { */ private checkNativeValidity() { if ( - !this.disabled && - this._required && - !this.checked && - !this.readonly + !this._disabled() && + this._required() && + !this._checked() && + !this._readonly() ) { - this.invalid = true; + this._invalid.set(true); } else { - this.invalid = false; + this._invalid.set(false); } } } diff --git a/projects/igniteui-angular/radio/src/radio/radio-group/radio-group.directive.spec.ts b/projects/igniteui-angular/radio/src/radio/radio-group/radio-group.directive.spec.ts index dc314bec786..01a9043e82f 100644 --- a/projects/igniteui-angular/radio/src/radio/radio-group/radio-group.directive.spec.ts +++ b/projects/igniteui-angular/radio/src/radio/radio-group/radio-group.directive.spec.ts @@ -279,6 +279,24 @@ describe('IgxRadioGroupDirective', () => { expect(radioInstance.selected).toEqual(radioInstance.radioButtons.last); })); + it('Releases its subscriptions to a radio button that is removed from the group', fakeAsync(() => { + const fixture = TestBed.createComponent(RadioGroupDeepProjectionComponent); + fixture.detectChanges(); + tick(); + + const removed = fixture.componentInstance.radioGroup.radioButtons.last; + expect(removed.change.observed).toBe(true); + + fixture.componentInstance.choices = [0, 1]; + fixture.detectChanges(); + tick(); + + // The group must not keep listening to a button it no longer owns, + // otherwise subscriptions accumulate for the lifetime of the group. + expect(removed.change.observed).toBe(false); + expect(removed.blurRadio.observed).toBe(false); + })); + it('Updates checked radio button correctly', fakeAsync(() => { const fixture = TestBed.createComponent(RadioGroupSimpleComponent); fixture.detectChanges(); diff --git a/projects/igniteui-angular/radio/src/radio/radio-group/radio-group.directive.ts b/projects/igniteui-angular/radio/src/radio/radio-group/radio-group.directive.ts index a8fef75e07b..416b002f897 100644 --- a/projects/igniteui-angular/radio/src/radio/radio-group/radio-group.directive.ts +++ b/projects/igniteui-angular/radio/src/radio/radio-group/radio-group.directive.ts @@ -16,6 +16,7 @@ import { ElementRef, Injector } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ControlValueAccessor, NgControl } from '@angular/forms'; import { fromEvent, noop, Subject, Subscription, takeUntil } from 'rxjs'; import { IgxRadioComponent } from '../radio.component'; @@ -560,19 +561,25 @@ export class IgxRadioGroupDirective implements ControlValueAccessor, OnDestroy, * @hidden * @internal */ - private _setRadioButtonEvents(button: any) { + private _setRadioButtonEvents(button: IgxRadioComponent) { button.change.pipe( - takeUntil(button.destroy$), + takeUntilDestroyed(button.destroyRef), takeUntil(this.destroy$), takeUntil(this.queryChange$) ).subscribe((ev: IChangeCheckboxEventArgs) => this._selectedRadioButtonChanged(ev)); button.blurRadio - .pipe(takeUntil(this.destroy$)) + .pipe( + takeUntilDestroyed(button.destroyRef), + takeUntil(this.destroy$) + ) .subscribe(() => this.updateValidityOnBlur()); fromEvent(button.nativeElement, 'keyup') - .pipe(takeUntil(this.destroy$)) + .pipe( + takeUntilDestroyed(button.destroyRef), + takeUntil(this.destroy$) + ) .subscribe((event: KeyboardEvent) => this.updateOnKeyUp(event)); } diff --git a/projects/igniteui-angular/radio/src/radio/radio.component.html b/projects/igniteui-angular/radio/src/radio/radio.component.html index 7bf9fb5cb06..a948aef8467 100644 --- a/projects/igniteui-angular/radio/src/radio/radio.component.html +++ b/projects/igniteui-angular/radio/src/radio/radio.component.html @@ -1,28 +1,28 @@
+ [id]="_labelId()" + [class]="_labelClass()"> diff --git a/projects/igniteui-angular/radio/src/radio/radio.component.ts b/projects/igniteui-angular/radio/src/radio/radio.component.ts index ce0db37b67d..bebf08d4803 100644 --- a/projects/igniteui-angular/radio/src/radio/radio.component.ts +++ b/projects/igniteui-angular/radio/src/radio/radio.component.ts @@ -2,14 +2,13 @@ import { AfterViewInit, Component, EventEmitter, - HostBinding, - HostListener, Input, booleanAttribute, OnDestroy, inject, ChangeDetectionStrategy, - ViewEncapsulation + ViewEncapsulation, + signal } from '@angular/core'; import { ControlValueAccessor } from '@angular/forms'; import { EditorProvider, EDITOR_PROVIDER } from 'igniteui-angular/core'; @@ -39,8 +38,18 @@ import { IgxRadioGroupDirective } from './radio-group/radio-group.directive'; templateUrl: 'radio.component.html', styleUrl: 'radio.component.css', encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.Eager, - imports: [IgxRippleDirective] + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [IgxRippleDirective], + host: { + '[class.igx-radio]': 'cssClass', + '[class.igx-radio--checked]': 'checked', + '[class.igx-radio--disabled]': 'disabled', + '[class.igx-radio--invalid]': 'invalid', + '[class.igx-radio--focused]': 'focused', + '(change)': '_changed($event)', + '(click)': '_onCheckboxClick()', + '(blur)': 'onBlur()', + } }) export class IgxRadioComponent @@ -50,8 +59,7 @@ export class IgxRadioComponent public blurRadio = new EventEmitter(); private radioGroup = inject(IgxRadioGroupDirective, { optional: true, skipSelf: true }); - private _disabled = false; - private _groupDisabled = false; + private readonly _groupDisabled = signal(false); /** * Returns the class of the radio component. @@ -61,7 +69,6 @@ export class IgxRadioComponent * * @memberof IgxRadioComponent */ - @HostBinding('class.igx-radio') public override cssClass = 'igx-radio'; /** @@ -76,13 +83,12 @@ export class IgxRadioComponent * * @memberof IgxRadioComponent */ - @HostBinding('class.igx-radio--checked') @Input({ transform: booleanAttribute }) public override set checked(value: boolean) { - this._checked = value; + this._checked.set(value); } public override get checked() { - return this._checked; + return this._checked(); } /** @@ -97,13 +103,12 @@ export class IgxRadioComponent * * @memberof IgxRadioComponent */ - @HostBinding('class.igx-radio--disabled') @Input({ transform: booleanAttribute }) public override get disabled(): boolean { - return this._disabled || this._groupDisabled; + return super.disabled || this._groupDisabled(); } public override set disabled(value: boolean) { - this._disabled = value; + super.disabled = value; } /** @@ -113,7 +118,7 @@ export class IgxRadioComponent * @hidden @internal */ public set groupDisabled(value: boolean) { - this._groupDisabled = value; + this._groupDisabled.set(value); } /** @@ -128,9 +133,13 @@ export class IgxRadioComponent * * @memberof IgxRadioComponent */ - @HostBinding('class.igx-radio--invalid') @Input({ transform: booleanAttribute }) - public override invalid = false; + public override get invalid() { + return super.invalid; + } + public override set invalid(value: boolean) { + super.invalid = value; + } /** * Sets/gets whether the radio component is on focus. @@ -144,14 +153,17 @@ export class IgxRadioComponent * * @memberof IgxRadioComponent */ - @HostBinding('class.igx-radio--focused') - public override focused = false; + public override get focused() { + return super.focused; + } + public override set focused(value: boolean) { + super.focused = value; + } /** * @hidden * @internal */ - @HostListener('change', ['$event']) public _changed(event: IChangeCheckboxEventArgs) { if (event instanceof Event) { event.preventDefault(); @@ -161,7 +173,6 @@ export class IgxRadioComponent /** * @hidden */ - @HostListener('click') public override _onCheckboxClick() { this.select(); } @@ -175,12 +186,12 @@ export class IgxRadioComponent * @memberof IgxRadioComponent */ public select() { - if (!this.checked) { - this.checked = true; + if (!this._checked()) { + this._checked.set(true); this.change.emit({ value: this.value, owner: this, - checked: this.checked, + checked: this._checked(), }); this._onChangeCallback(this.value); } @@ -195,7 +206,7 @@ export class IgxRadioComponent * @memberof IgxRadioComponent */ public deselect() { - this.checked = false; + this._checked.set(false); this.focused = false; this.cdr.markForCheck(); } @@ -211,8 +222,8 @@ export class IgxRadioComponent this.value = this.value ?? value; if (value === this.value) { - if (!this.checked) { - this.checked = true; + if (!this._checked()) { + this._checked.set(true); } } else { this.deselect(); @@ -222,7 +233,6 @@ export class IgxRadioComponent /** * @hidden */ - @HostListener('blur') public override onBlur() { super.onBlur(); this.blurRadio.emit(); diff --git a/projects/igniteui-angular/switch/src/switch/switch.component.html b/projects/igniteui-angular/switch/src/switch/switch.component.html index e525680a699..c3c2bbb3300 100644 --- a/projects/igniteui-angular/switch/src/switch/switch.component.html +++ b/projects/igniteui-angular/switch/src/switch/switch.component.html @@ -1,23 +1,23 @@
@@ -27,7 +27,7 @@ + [class]="_labelClass()" + [id]="_labelId()"> diff --git a/projects/igniteui-angular/switch/src/switch/switch.component.ts b/projects/igniteui-angular/switch/src/switch/switch.component.ts index 4da1a1640f5..021a4595d7f 100644 --- a/projects/igniteui-angular/switch/src/switch/switch.component.ts +++ b/projects/igniteui-angular/switch/src/switch/switch.component.ts @@ -1,6 +1,5 @@ import { Component, - HostBinding, Input, AfterViewInit, booleanAttribute, @@ -43,8 +42,15 @@ import { EditorProvider, EDITOR_PROVIDER } from 'igniteui-angular/core'; templateUrl: 'switch.component.html', styleUrl: 'switch.component.css', encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.Eager, - imports: [IgxRippleDirective] + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [IgxRippleDirective], + host: { + '[class.igx-switch]': 'cssClass', + '[class.igx-switch--checked]': 'checked', + '[class.igx-switch--disabled]': 'disabled', + '[class.igx-switch--invalid]': 'invalid', + '[class.igx-switch--focused]': 'focused', + } }) export class IgxSwitchComponent extends CheckboxBaseDirective @@ -57,7 +63,6 @@ export class IgxSwitchComponent * let switchClass = this.switch.cssClass; * ``` */ - @HostBinding('class.igx-switch') public override cssClass = 'igx-switch'; /** * Sets/gets whether the switch is on or off. @@ -68,7 +73,6 @@ export class IgxSwitchComponent * * ``` */ - @HostBinding('class.igx-switch--checked') @Input() public override set checked(value: boolean) { super.checked = value; @@ -85,9 +89,13 @@ export class IgxSwitchComponent * * ``` */ - @HostBinding('class.igx-switch--disabled') @Input({ transform: booleanAttribute }) - public override disabled = false; + public override get disabled() { + return super.disabled; + } + public override set disabled(value: boolean) { + super.disabled = value; + } /** * Sets/gets whether the switch component is invalid. @@ -101,9 +109,13 @@ export class IgxSwitchComponent * let isInvalid = this.switch.invalid; * ``` */ - @HostBinding('class.igx-switch--invalid') @Input({ transform: booleanAttribute }) - public override invalid = false; + public override get invalid() { + return super.invalid; + } + public override set invalid(value: boolean) { + super.invalid = value; + } /** * Sets/gets whether the switch component is on focus. @@ -114,6 +126,10 @@ export class IgxSwitchComponent * this.switch.focused = true; * ``` */ - @HostBinding('class.igx-switch--focused') - public override focused = false; + public override get focused() { + return super.focused; + } + public override set focused(value: boolean) { + super.focused = value; + } }