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;
+ }
}