Skip to content
Closed
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
6 changes: 5 additions & 1 deletion goldens/material/form-field/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
_forceDisplayInfixLabel(): boolean | 0;
// (undocumented)
_formFieldControl: MatFormFieldControl_2<any>;
// (undocumented)
_formFieldControls: QueryList<MatFormFieldControl_2<any>>;
getConnectedOverlayOrigin(): ElementRef;
getLabelId: i0.Signal<string | null>;
_getSubscriptMessageType(): 'error' | 'hint';
Expand Down Expand Up @@ -121,6 +123,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
ngOnDestroy(): void;
// (undocumented)
_notchedOutline: MatFormFieldNotchedOutline | undefined;
get otherFormFieldControls(): MatFormFieldControl_2<any>[];
get otherFormFieldControlsErrorState(): boolean;
// (undocumented)
_prefixChildren: QueryList<MatPrefix>;
_refreshOutlineNotchWidth(): void;
Expand All @@ -139,7 +143,7 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
// (undocumented)
_textSuffixContainer: ElementRef<HTMLElement>;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatFormField, "mat-form-field", ["matFormField"], { "hideRequiredMarker": { "alias": "hideRequiredMarker"; "required": false; }; "color": { "alias": "color"; "required": false; }; "floatLabel": { "alias": "floatLabel"; "required": false; }; "appearance": { "alias": "appearance"; "required": false; }; "subscriptSizing": { "alias": "subscriptSizing"; "required": false; }; "hintLabel": { "alias": "hintLabel"; "required": false; }; }, {}, ["_labelChild", "_formFieldControl", "_prefixChildren", "_suffixChildren", "_errorChildren", "_hintChildren"], ["mat-label", "[matPrefix], [matIconPrefix]", "[matTextPrefix]", "*", "[matTextSuffix]", "[matSuffix], [matIconSuffix]", "mat-error, [matError]", "mat-hint:not([align='end'])", "mat-hint[align='end']"], true, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MatFormField, "mat-form-field", ["matFormField"], { "hideRequiredMarker": { "alias": "hideRequiredMarker"; "required": false; }; "color": { "alias": "color"; "required": false; }; "floatLabel": { "alias": "floatLabel"; "required": false; }; "appearance": { "alias": "appearance"; "required": false; }; "subscriptSizing": { "alias": "subscriptSizing"; "required": false; }; "hintLabel": { "alias": "hintLabel"; "required": false; }; }, {}, ["_labelChild", "_formFieldControl", "_formFieldControls", "_prefixChildren", "_suffixChildren", "_errorChildren", "_hintChildren"], ["mat-label", "[matPrefix], [matIconPrefix]", "[matTextPrefix]", "*", "[matTextSuffix]", "[matSuffix], [matIconSuffix]", "mat-error, [matError]", "mat-hint:not([align='end'])", "mat-hint[align='end']"], true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatFormField, never>;
}
Expand Down
6 changes: 5 additions & 1 deletion goldens/material/input/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
_forceDisplayInfixLabel(): boolean | 0;
// (undocumented)
_formFieldControl: MatFormFieldControl<any>;
// (undocumented)
_formFieldControls: QueryList<MatFormFieldControl<any>>;
getConnectedOverlayOrigin(): ElementRef;
getLabelId: i0.Signal<string | null>;
_getSubscriptMessageType(): 'error' | 'hint';
Expand Down Expand Up @@ -114,6 +116,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
ngOnDestroy(): void;
// (undocumented)
_notchedOutline: MatFormFieldNotchedOutline | undefined;
get otherFormFieldControls(): MatFormFieldControl<any>[];
get otherFormFieldControlsErrorState(): boolean;
// (undocumented)
_prefixChildren: QueryList<MatPrefix>;
_refreshOutlineNotchWidth(): void;
Expand All @@ -132,7 +136,7 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
// (undocumented)
_textSuffixContainer: ElementRef<HTMLElement>;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatFormField, "mat-form-field", ["matFormField"], { "hideRequiredMarker": { "alias": "hideRequiredMarker"; "required": false; }; "color": { "alias": "color"; "required": false; }; "floatLabel": { "alias": "floatLabel"; "required": false; }; "appearance": { "alias": "appearance"; "required": false; }; "subscriptSizing": { "alias": "subscriptSizing"; "required": false; }; "hintLabel": { "alias": "hintLabel"; "required": false; }; }, {}, ["_labelChild", "_formFieldControl", "_prefixChildren", "_suffixChildren", "_errorChildren", "_hintChildren"], ["mat-label", "[matPrefix], [matIconPrefix]", "[matTextPrefix]", "*", "[matTextSuffix]", "[matSuffix], [matIconSuffix]", "mat-error, [matError]", "mat-hint:not([align='end'])", "mat-hint[align='end']"], true, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MatFormField, "mat-form-field", ["matFormField"], { "hideRequiredMarker": { "alias": "hideRequiredMarker"; "required": false; }; "color": { "alias": "color"; "required": false; }; "floatLabel": { "alias": "floatLabel"; "required": false; }; "appearance": { "alias": "appearance"; "required": false; }; "subscriptSizing": { "alias": "subscriptSizing"; "required": false; }; "hintLabel": { "alias": "hintLabel"; "required": false; }; }, {}, ["_labelChild", "_formFieldControl", "_formFieldControls", "_prefixChildren", "_suffixChildren", "_errorChildren", "_hintChildren"], ["mat-label", "[matPrefix], [matIconPrefix]", "[matTextPrefix]", "*", "[matTextSuffix]", "[matSuffix], [matIconSuffix]", "mat-error, [matError]", "mat-hint:not([align='end'])", "mat-hint[align='end']"], true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatFormField, never>;
}
Expand Down
6 changes: 5 additions & 1 deletion goldens/material/select/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
_forceDisplayInfixLabel(): boolean | 0;
// (undocumented)
_formFieldControl: MatFormFieldControl_2<any>;
// (undocumented)
_formFieldControls: QueryList<MatFormFieldControl_2<any>>;
getConnectedOverlayOrigin(): ElementRef;
getLabelId: i0.Signal<string | null>;
_getSubscriptMessageType(): 'error' | 'hint';
Expand Down Expand Up @@ -137,6 +139,8 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
ngOnDestroy(): void;
// (undocumented)
_notchedOutline: MatFormFieldNotchedOutline | undefined;
get otherFormFieldControls(): MatFormFieldControl_2<any>[];
get otherFormFieldControlsErrorState(): boolean;
// (undocumented)
_prefixChildren: QueryList<MatPrefix>;
_refreshOutlineNotchWidth(): void;
Expand All @@ -155,7 +159,7 @@ export class MatFormField implements FloatingLabelParent, AfterContentInit, Afte
// (undocumented)
_textSuffixContainer: ElementRef<HTMLElement>;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatFormField, "mat-form-field", ["matFormField"], { "hideRequiredMarker": { "alias": "hideRequiredMarker"; "required": false; }; "color": { "alias": "color"; "required": false; }; "floatLabel": { "alias": "floatLabel"; "required": false; }; "appearance": { "alias": "appearance"; "required": false; }; "subscriptSizing": { "alias": "subscriptSizing"; "required": false; }; "hintLabel": { "alias": "hintLabel"; "required": false; }; }, {}, ["_labelChild", "_formFieldControl", "_prefixChildren", "_suffixChildren", "_errorChildren", "_hintChildren"], ["mat-label", "[matPrefix], [matIconPrefix]", "[matTextPrefix]", "*", "[matTextSuffix]", "[matSuffix], [matIconSuffix]", "mat-error, [matError]", "mat-hint:not([align='end'])", "mat-hint[align='end']"], true, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MatFormField, "mat-form-field", ["matFormField"], { "hideRequiredMarker": { "alias": "hideRequiredMarker"; "required": false; }; "color": { "alias": "color"; "required": false; }; "floatLabel": { "alias": "floatLabel"; "required": false; }; "appearance": { "alias": "appearance"; "required": false; }; "subscriptSizing": { "alias": "subscriptSizing"; "required": false; }; "hintLabel": { "alias": "hintLabel"; "required": false; }; }, {}, ["_labelChild", "_formFieldControl", "_formFieldControls", "_prefixChildren", "_suffixChildren", "_errorChildren", "_hintChildren"], ["mat-label", "[matPrefix], [matIconPrefix]", "[matTextPrefix]", "*", "[matTextSuffix]", "[matSuffix], [matIconSuffix]", "mat-error, [matError]", "mat-hint:not([align='end'])", "mat-hint[align='end']"], true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatFormField, never>;
}
Expand Down
77 changes: 77 additions & 0 deletions src/material/chips/chip-grid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,50 @@ describe('MatChipGrid', () => {
});
});

describe('error message when multiple form field controls', () => {
let fixture: ComponentFixture<ChipGridWithMultipleControls>;
let errorTestComponent: ChipGridWithMultipleControls;
let containerEl: HTMLElement;
let chipGridEl: HTMLElement;

beforeEach(fakeAsync(() => {
fixture = createComponent(ChipGridWithMultipleControls);
flush();
fixture.detectChanges();

errorTestComponent = fixture.componentInstance;
containerEl = fixture.debugElement.query(By.css('mat-form-field'))!.nativeElement;
chipGridEl = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement;
}));

it('should display an error message when the grid is touched and invalid when multiple controls in mat form field', fakeAsync(() => {
expect(errorTestComponent.chipCtrl.invalid)
.withContext('Expected form control to be invalid')
.toBe(true);
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected no error message')
.toBe(0);

expect(containerEl.classList)
.withContext('Expected container not to have the invalid CSS class.')
.not.toContain('mat-form-field-invalid');

errorTestComponent.chipCtrl.markAsTouched();
fixture.detectChanges();
tick();

expect(containerEl.classList)
.withContext('Expected container to have the invalid CSS class.')
.toContain('mat-form-field-invalid');
expect(containerEl.querySelectorAll('mat-error').length)
.withContext('Expected one error message to have been rendered.')
.toBe(1);
expect(chipGridEl.getAttribute('aria-invalid'))
.withContext('Expected aria-invalid to be set to "true".')
.toBe('true');
}));
});

describe('error messages', () => {
let fixture: ComponentFixture<ChipGridWithFormErrorMessages>;
let errorTestComponent: ChipGridWithFormErrorMessages;
Expand Down Expand Up @@ -1228,3 +1272,36 @@ class ChipGridWithRemove {
this.chips.splice(event.chip.value, 1);
}
}

@Component({
template: `
<mat-form-field>
<input
matInput
type="text"
placeholder="New item..."
[matChipInputFor]="chipGrid"
/>

<mat-chip-grid #chipGrid required [formControl]="chipCtrl">
@for (i of chips; track i) {
<mat-chip-row [value]="i" >
Chip {{i + 1}}
<span matChipRemove>Remove</span>
</mat-chip-row>
}
</mat-chip-grid>

<mat-error data-test-id="errors">
@if (chipCtrl.errors?.['required']) {
{{ 'Error occurs' }}
}
</mat-error>
</mat-form-field>
`,
standalone: false,
})
class ChipGridWithMultipleControls {
chipCtrl = new FormControl();
chips = [0, 1, 2, 3, 4];
}
16 changes: 8 additions & 8 deletions src/material/form-field/form-field.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
[class.mdc-text-field--outlined]="_hasOutline()"
[class.mdc-text-field--no-label]="!_hasFloatingLabel()"
[class.mdc-text-field--disabled]="_control.disabled"
[class.mdc-text-field--invalid]="_control.errorState"
[class.mdc-text-field--invalid]="_control.errorState || otherFormFieldControlsErrorState"
(click)="_control.onContainerClick($event)"
>
@if (!_hasOutline() && !_control.disabled) {
Expand Down Expand Up @@ -96,20 +96,20 @@
</div>

<div
class="mat-mdc-form-field-subscript-wrapper mat-mdc-form-field-bottom-align"
[class.mat-mdc-form-field-subscript-dynamic-size]="subscriptSizing === 'dynamic'"
>
class="mat-mdc-form-field-subscript-wrapper mat-mdc-form-field-bottom-align"
[class.mat-mdc-form-field-subscript-dynamic-size]="subscriptSizing === 'dynamic'">
@let subscriptMessageType = _getSubscriptMessageType();

<!--
Use a single permanent wrapper for both hints and errors so aria-live works correctly,
as having it appear post render will not consistently work. We also do not want to add
additional divs as it causes styling regressions.
-->
<div aria-atomic="true" aria-live="polite"
[class.mat-mdc-form-field-error-wrapper]="subscriptMessageType === 'error'"
[class.mat-mdc-form-field-hint-wrapper]="subscriptMessageType === 'hint'"
>
<div
aria-atomic="true"
aria-live="polite"
[class.mat-mdc-form-field-error-wrapper]="subscriptMessageType === 'error'"
[class.mat-mdc-form-field-hint-wrapper]="subscriptMessageType === 'hint'">
@switch (subscriptMessageType) {
@case ('error') {
<ng-content select="mat-error, [matError]"></ng-content>
Expand Down
31 changes: 29 additions & 2 deletions src/material/form-field/form-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ interface MatFormFieldControl<T> extends _MatFormFieldControl<T> {}
// Note that these classes reuse the same names as the non-MDC version, because they can be
// considered a public API since custom form controls may use them to style themselves.
// See https://github.com/angular/components/pull/20502#discussion_r486124901.
'[class.mat-form-field-invalid]': '_control.errorState',
'[class.mat-form-field-invalid]': '_control.errorState || otherFormFieldControlsErrorState',
'[class.mat-form-field-disabled]': '_control.disabled',
'[class.mat-form-field-autofilled]': '_control.autofilled',
'[class.mat-form-field-appearance-fill]': 'appearance == "fill"',
Expand Down Expand Up @@ -219,6 +219,9 @@ export class MatFormField
});

@ContentChild(_MatFormFieldControl) _formFieldControl: MatFormFieldControl<any>;
@ContentChildren(_MatFormFieldControl, {descendants: true}) _formFieldControls: QueryList<
MatFormFieldControl<any>
>;
@ContentChildren(MAT_PREFIX, {descendants: true}) _prefixChildren: QueryList<MatPrefix>;
@ContentChildren(MAT_SUFFIX, {descendants: true}) _suffixChildren: QueryList<MatSuffix>;
@ContentChildren(MAT_ERROR, {descendants: true}) _errorChildren: QueryList<MatError>;
Expand Down Expand Up @@ -327,12 +330,23 @@ export class MatFormField
this._explicitFormFieldControl = value;
}

/** Gets the other form field controls if any */
get otherFormFieldControls(): MatFormFieldControl<any>[] {
return this._formFieldControls.filter(control => control.id !== this._control.id);
}

/** Gets the error state of other form field controls if any */
get otherFormFieldControlsErrorState(): boolean {
return this.otherFormFieldControls.some(control => control.errorState);
}

private _destroyed = new Subject<void>();
private _isFocused: boolean | null = null;
private _explicitFormFieldControl: MatFormFieldControl<any>;
private _previousControl: MatFormFieldControl<unknown> | null = null;
private _previousControlValidatorFn: ValidatorFn | null = null;
private _stateChanges: Subscription | undefined;
private _otherControlStateChanges: Subscription | undefined;
private _valueChanges: Subscription | undefined;
private _describedByChanges: Subscription | undefined;
protected readonly _animationsDisabled = _animationsDisabled();
Expand Down Expand Up @@ -412,6 +426,7 @@ export class MatFormField
ngOnDestroy() {
this._outlineLabelOffsetResizeObserver?.disconnect();
this._stateChanges?.unsubscribe();
this._otherControlStateChanges?.unsubscribe();
this._valueChanges?.unsubscribe();
this._describedByChanges?.unsubscribe();
this._destroyed.next();
Expand Down Expand Up @@ -466,6 +481,16 @@ export class MatFormField
this._changeDetectorRef.markForCheck();
});

if (this.otherFormFieldControls.length) {
this._otherControlStateChanges?.unsubscribe();
this.otherFormFieldControls.map(control => {
const subscription = control.stateChanges.subscribe(() =>
this._changeDetectorRef.markForCheck(),
);
this._otherControlStateChanges?.add(subscription);
});
}

// Updating the `aria-describedby` touches the DOM. Only do it if it actually needs to change.
this._describedByChanges?.unsubscribe();
this._describedByChanges = control.stateChanges
Expand Down Expand Up @@ -632,7 +657,9 @@ export class MatFormField

/** Gets the type of subscript message to render (error or hint). */
_getSubscriptMessageType(): 'error' | 'hint' {
return this._errorChildren && this._errorChildren.length > 0 && this._control.errorState
return this._errorChildren &&
this._errorChildren.length > 0 &&
(this._control.errorState || this.otherFormFieldControlsErrorState)
? 'error'
: 'hint';
}
Expand Down
Loading