Skip to content
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
5 changes: 4 additions & 1 deletion goldens/material/checkbox/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as i0 from '@angular/core';
import * as i2 from '@angular/cdk/bidi';
import { InjectionToken } from '@angular/core';
import { OnChanges } from '@angular/core';
import { OnInit } from '@angular/core';
import { SimpleChanges } from '@angular/core';
import { ValidationErrors } from '@angular/forms';
import { Validator } from '@angular/forms';
Expand All @@ -22,7 +23,7 @@ import { Validator } from '@angular/forms';
export const MAT_CHECKBOX_DEFAULT_OPTIONS: InjectionToken<MatCheckboxDefaultOptions>;

// @public (undocumented)
export class MatCheckbox implements AfterViewInit, OnChanges, ControlValueAccessor, Validator, FocusableOption {
export class MatCheckbox implements OnInit, AfterViewInit, OnChanges, ControlValueAccessor, Validator, FocusableOption {
constructor();
protected _animationClasses: {
uncheckedToChecked: string;
Expand Down Expand Up @@ -86,6 +87,8 @@ export class MatCheckbox implements AfterViewInit, OnChanges, ControlValueAccess
// (undocumented)
ngOnChanges(changes: SimpleChanges<this>): void;
// (undocumented)
ngOnInit(): void;
// (undocumented)
_onBlur(): void;
// (undocumented)
_onInputClick(): void;
Expand Down
5 changes: 4 additions & 1 deletion goldens/material/slide-toggle/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as i2 from '@angular/cdk/bidi';
import { InjectionToken } from '@angular/core';
import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { OnInit } from '@angular/core';
import { SimpleChanges } from '@angular/core';
import { ValidationErrors } from '@angular/forms';
import { Validator } from '@angular/forms';
Expand All @@ -24,7 +25,7 @@ import { Validator } from '@angular/forms';
export const MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS: InjectionToken<MatSlideToggleDefaultOptions>;

// @public (undocumented)
export class MatSlideToggle implements OnDestroy, AfterContentInit, OnChanges, ControlValueAccessor, Validator {
export class MatSlideToggle implements OnInit, OnDestroy, AfterContentInit, OnChanges, ControlValueAccessor, Validator {
constructor();
ariaDescribedby: string;
ariaLabel: string | null;
Expand Down Expand Up @@ -78,6 +79,8 @@ export class MatSlideToggle implements OnDestroy, AfterContentInit, OnChanges, C
ngOnChanges(changes: SimpleChanges<this>): void;
// (undocumented)
ngOnDestroy(): void;
// (undocumented)
ngOnInit(): void;
_noopAnimations: boolean;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
Expand Down
63 changes: 62 additions & 1 deletion src/material/checkbox/checkbox.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {dispatchFakeEvent} from '@angular/cdk/testing/private';
import {ChangeDetectionStrategy, Component, DebugElement} from '@angular/core';
import {ChangeDetectionStrategy, Component, DebugElement, signal} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
import {FormField, form, required} from '@angular/forms/signals';
import {ThemePalette} from '../core';
import {By} from '@angular/platform-browser';
import {
Expand Down Expand Up @@ -1105,6 +1106,55 @@ describe('MatCheckbox', () => {
});
});

describe('with signal forms', () => {
let checkboxDebugElement: DebugElement;
let checkboxInstance: MatCheckbox;
let testComponent: CheckboxWithSignalForms;
let inputElement: HTMLInputElement;

beforeEach(() => {
fixture = TestBed.createComponent(CheckboxWithSignalForms);
fixture.detectChanges();

checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!;
checkboxInstance = checkboxDebugElement.componentInstance;
testComponent = fixture.debugElement.componentInstance;
inputElement = <HTMLInputElement>checkboxDebugElement.nativeElement.querySelector('input');
});

it('should produce a single `required` error when used with signal forms', () => {
expect(checkboxInstance.checked).toBe(false);
expect(checkboxInstance.required).toBe(true);
expect(inputElement.required).toBe(true);
expect(testComponent.form().valid()).toBe(false);
expect(
testComponent
.form()
.errors()
.map(e => e.kind),
).toEqual(['required']);

inputElement.click();
fixture.detectChanges();

expect(checkboxInstance.checked).toBe(true);
expect(testComponent.form().valid()).toBe(true);
expect(testComponent.form().errors().length).toBe(0);

inputElement.click();
fixture.detectChanges();

expect(checkboxInstance.checked).toBe(false);
expect(testComponent.form().valid()).toBe(false);
expect(
testComponent
.form()
.errors()
.map(e => e.kind),
).toEqual(['required']);
});
});

describe('without label', () => {
let checkboxInnerContainer: HTMLElement;

Expand Down Expand Up @@ -1359,3 +1409,14 @@ class CheckboxWithTabindexAttr {}
changeDetection: ChangeDetectionStrategy.Eager,
})
class CheckboxWithStaticAriaAttributes {}

@Component({
template: `<mat-checkbox [formField]="form"></mat-checkbox>`,
imports: [MatCheckbox, FormField],
})
class CheckboxWithSignalForms {
readonly isChecked = signal(false);
readonly form = form(this.isChecked, path => {
required(path);
});
}
15 changes: 13 additions & 2 deletions src/material/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
inject,
HostAttributeToken,
signal,
Injector,
OnInit,
} from '@angular/core';
import {
AbstractControl,
Expand All @@ -47,6 +49,7 @@ import {
MatCheckboxDefaultOptions,
} from './checkbox-config';
import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
import {FORM_FIELD, FormField} from '@angular/forms/signals';

/**
* Represents the different states that require custom transitions between them.
Expand Down Expand Up @@ -106,7 +109,7 @@ export class MatCheckboxChange {
imports: [MatRipple, _MatInternalFormField],
})
export class MatCheckbox
implements AfterViewInit, OnChanges, ControlValueAccessor, Validator, FocusableOption
implements OnInit, AfterViewInit, OnChanges, ControlValueAccessor, Validator, FocusableOption
{
_elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private _changeDetectorRef = inject(ChangeDetectorRef);
Expand All @@ -115,6 +118,8 @@ export class MatCheckbox
private _options = inject<MatCheckboxDefaultOptions>(MAT_CHECKBOX_DEFAULT_OPTIONS, {
optional: true,
});
private readonly _injector = inject(Injector);
private _signalFormField: FormField<unknown> | null = null;

/** Focuses the checkbox. */
focus() {
Expand Down Expand Up @@ -251,6 +256,10 @@ export class MatCheckbox
}
}

ngOnInit() {
this._signalFormField = this._injector.get(FORM_FIELD, undefined, {optional: true, self: true});
}

ngAfterViewInit() {
this._syncIndeterminate(this.indeterminate);
}
Expand Down Expand Up @@ -346,7 +355,9 @@ export class MatCheckbox

// Implemented as a part of Validator.
validate(control: AbstractControl<boolean>): ValidationErrors | null {
return this.required && control.value !== true ? {'required': true} : null;
return this.required && control.value !== true && !this._signalFormField
? {'required': true}
: null;
}

// Implemented as a part of Validator.
Expand Down
62 changes: 61 additions & 1 deletion src/material/slide-toggle/slide-toggle.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {FocusMonitor} from '@angular/cdk/a11y';
import {BidiModule, Direction} from '@angular/cdk/bidi';
import {dispatchFakeEvent} from '@angular/cdk/testing/private';
import {Component, ChangeDetectionStrategy} from '@angular/core';
import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
import {FormField, form, required} from '@angular/forms/signals';
import {By} from '@angular/platform-browser';
import {MatSlideToggle, MatSlideToggleChange, MatSlideToggleModule} from './index';
import {MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS} from './slide-toggle-config';
Expand Down Expand Up @@ -807,6 +808,54 @@ describe('MatSlideToggle with forms', () => {
expect(fixture.componentInstance.onChange).toHaveBeenCalledTimes(1);
});
});

describe('with signal forms', () => {
let fixture: ComponentFixture<SlideToggleWithSignalForms>;
let testComponent: SlideToggleWithSignalForms;
let slideToggle: MatSlideToggle;
let buttonElement: HTMLButtonElement;

beforeEach(() => {
fixture = TestBed.createComponent(SlideToggleWithSignalForms);
fixture.detectChanges();

testComponent = fixture.debugElement.componentInstance;
slideToggle = fixture.debugElement.query(By.directive(MatSlideToggle))!.componentInstance;
buttonElement = fixture.debugElement.query(By.css('button'))!.nativeElement;
});

it('should produce a single `required` error when used with signal forms', () => {
expect(slideToggle.checked).toBe(false);
expect(slideToggle.required).toBe(true);
expect(buttonElement.getAttribute('aria-required')).toBe('true');
expect(testComponent.form().valid()).toBe(false);
expect(
testComponent
.form()
.errors()
.map(e => e.kind),
).toEqual(['required']);

buttonElement.click();
fixture.detectChanges();

expect(slideToggle.checked).toBe(true);
expect(testComponent.form().valid()).toBe(true);
expect(testComponent.form().errors().length).toBe(0);

buttonElement.click();
fixture.detectChanges();

expect(slideToggle.checked).toBe(false);
expect(testComponent.form().valid()).toBe(false);
expect(
testComponent
.form()
.errors()
.map(e => e.kind),
).toEqual(['required']);
});
});
});

@Component({
Expand Down Expand Up @@ -934,3 +983,14 @@ class SlideToggleWithModelAndChangeEvent {
changeDetection: ChangeDetectionStrategy.Eager,
})
class SlideToggleWithStaticAriaAttributes {}

@Component({
template: `<mat-slide-toggle [formField]="form"></mat-slide-toggle>`,
imports: [MatSlideToggle, FormField],
})
class SlideToggleWithSignalForms {
readonly isChecked = signal(false);
readonly form = form(this.isChecked, path => {
required(path);
});
}
15 changes: 13 additions & 2 deletions src/material/slide-toggle/slide-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
ViewEncapsulation,
inject,
HostAttributeToken,
Injector,
OnInit,
} from '@angular/core';
import {
AbstractControl,
Expand All @@ -45,6 +47,7 @@ import {
MatRipple,
} from '../core';
import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
import {FORM_FIELD, FormField} from '@angular/forms/signals';

/** Change event object emitted by a slide toggle. */
export class MatSlideToggleChange {
Expand Down Expand Up @@ -91,12 +94,14 @@ export class MatSlideToggleChange {
imports: [MatRipple, _MatInternalFormField],
})
export class MatSlideToggle
implements OnDestroy, AfterContentInit, OnChanges, ControlValueAccessor, Validator
implements OnInit, OnDestroy, AfterContentInit, OnChanges, ControlValueAccessor, Validator
{
private _elementRef = inject(ElementRef);
protected _focusMonitor = inject(FocusMonitor);
protected _changeDetectorRef = inject(ChangeDetectorRef);
defaults = inject<MatSlideToggleDefaultOptions>(MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS);
private readonly _injector = inject(Injector);
private _signalFormField: FormField<unknown> | null = null;

private _onChange = (_: any) => {};
private _onTouched = () => {};
Expand Down Expand Up @@ -219,6 +224,10 @@ export class MatSlideToggle
this._labelId = this._uniqueId + '-label';
}

ngOnInit() {
this._signalFormField = this._injector.get(FORM_FIELD, undefined, {optional: true, self: true});
}

ngAfterContentInit() {
this._focusMonitor.monitor(this._elementRef, true).subscribe(focusOrigin => {
if (focusOrigin === 'keyboard' || focusOrigin === 'program') {
Expand Down Expand Up @@ -266,7 +275,9 @@ export class MatSlideToggle

/** Implemented as a part of Validator. */
validate(control: AbstractControl<boolean>): ValidationErrors | null {
return this.required && control.value !== true ? {'required': true} : null;
return this.required && control.value !== true && !this._signalFormField
? {'required': true}
: null;
}

/** Implemented as a part of Validator. */
Expand Down
Loading