+
+
+
+ @for (person of people; track person) {
+
+ @if (showEditIcon) {
+
+ }
+ @if (peopleWithAvatar && person.avatar) {
+ {{person.avatar}}
+ }
+ {{person.name}}
+
+
+ }
+
+
diff --git a/src/material/chips/chip-grid.spec.ts b/src/material/chips/chip-grid.spec.ts
index 385dca10c968..8058cb125377 100644
--- a/src/material/chips/chip-grid.spec.ts
+++ b/src/material/chips/chip-grid.spec.ts
@@ -588,6 +588,74 @@ describe('MatChipGrid', () => {
});
});
+ describe('ChipGrid without input', () => {
+ it('should not throw when used without a chip input', () => {
+ expect(() => createComponent(ChipGridWithoutInput)).not.toThrow();
+ });
+
+ it('should be able to focus the first chip', () => {
+ const fixture = createComponent(ChipGridWithoutInput);
+ chipGridInstance.focus();
+ fixture.detectChanges();
+ expect(document.activeElement).toBe(primaryActions[0]);
+ });
+
+ it('should not do anything on focus if there are no chips', () => {
+ const fixture = createComponent(ChipGridWithoutInput);
+ (testComponent as unknown as ChipGridWithoutInput).chips = [];
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ chipGridInstance.focus();
+ fixture.detectChanges();
+
+ expect(chipGridNativeElement.contains(document.activeElement)).toBe(false);
+ });
+
+ it('should have a default id on the component instance', () => {
+ createComponent(ChipGridWithoutInput);
+ expect(chipGridInstance.id).toMatch(/^mat-chip-grid-\w+$/);
+ });
+
+ it('should have empty getters that work without an input', () => {
+ const fixture = createComponent(ChipGridWithoutInput);
+ expect(chipGridInstance.empty).toBe(false);
+
+ (testComponent as unknown as ChipGridWithoutInput).chips = [];
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ expect(chipGridInstance.empty).toBe(true);
+ });
+
+ it('should have a placeholder getter that works without an input', () => {
+ const fixture = createComponent(ChipGridWithoutInput);
+ (testComponent as unknown as ChipGridWithoutInput).placeholder = 'Hello';
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+ expect(chipGridInstance.placeholder).toBe('Hello');
+ });
+
+ it('should have a focused getter that works without an input', () => {
+ const fixture = createComponent(ChipGridWithoutInput);
+ expect(chipGridInstance.focused).toBe(false);
+
+ chipGridInstance.focus();
+ fixture.detectChanges();
+
+ expect(chipGridInstance.focused).toBe(true);
+ });
+
+ it('should set aria-describedby on the grid when there is no input', fakeAsync(() => {
+ const fixture = createComponent(ChipGridWithoutInput);
+ const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
+ flush();
+ fixture.detectChanges();
+
+ expect(chipGridNativeElement.getAttribute('aria-describedby')).toBe(hint.id);
+ }));
+ });
+
describe('with chip remove', () => {
it('should properly focus next item if chip is removed through click', fakeAsync(() => {
// TODO(crisbeto): this test fails without the NoopAnimationsModule for some reason.
@@ -1234,3 +1302,22 @@ class ChipGridWithRemove {
this.chips.splice(event.chip.value, 1);
}
}
+
+@Component({
+ template: `
+
+ Foods
+
+ @for (food of chips; track food) {
+ {{ food }}
+ }
+
+ Some hint
+
+ `,
+ imports: [MatChipGrid, MatChipRow, MatFormField, MatLabel, MatHint],
+})
+class ChipGridWithoutInput {
+ chips = ['Pizza', 'Pasta', 'Tacos'];
+ placeholder: string;
+}
diff --git a/src/material/chips/chip-grid.ts b/src/material/chips/chip-grid.ts
index 5dfe91e9233e..24763b6c5a72 100644
--- a/src/material/chips/chip-grid.ts
+++ b/src/material/chips/chip-grid.ts
@@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
+import {_IdGenerator} from '@angular/cdk/a11y';
import {DOWN_ARROW, hasModifierKey, TAB, UP_ARROW} from '@angular/cdk/keycodes';
import {
AfterContentInit,
@@ -96,10 +97,11 @@ export class MatChipGrid
readonly controlType: string = 'mat-chip-grid';
/** The chip input to add more chips */
- protected _chipInput: MatChipTextControl;
+ protected _chipInput?: MatChipTextControl;
protected override _defaultRole = 'grid';
private _errorStateTracker: _ErrorStateTracker;
+ private _uid = inject(_IdGenerator).getId('mat-chip-grid-');
/**
* List of element ids to propagate to the chipInput's aria-describedby attribute.
@@ -137,7 +139,7 @@ export class MatChipGrid
* @docs-private
*/
get id(): string {
- return this._chipInput.id;
+ return this._chipInput ? this._chipInput.id : this._uid;
}
/**
@@ -166,7 +168,7 @@ export class MatChipGrid
/** Whether any chips or the matChipInput inside of this chip-grid has focus. */
override get focused(): boolean {
- return this._chipInput.focused || this._hasFocusedChip();
+ return this._chipInput?.focused || this._hasFocusedChip();
}
/**
@@ -285,14 +287,6 @@ export class MatChipGrid
.subscribe(() => this.stateChanges.next());
}
- override ngAfterViewInit() {
- super.ngAfterViewInit();
-
- if (!this._chipInput && (typeof ngDevMode === 'undefined' || ngDevMode)) {
- throw Error('mat-chip-grid must be used in combination with matChipInputFor.');
- }
- }
-
ngDoCheck() {
if (this.ngControl) {
// We need to re-evaluate this on every change detection cycle, because there are some
@@ -311,6 +305,9 @@ export class MatChipGrid
registerInput(inputElement: MatChipTextControl): void {
this._chipInput = inputElement;
this._chipInput.setDescribedByIds(this._ariaDescribedbyIds);
+
+ // If ids were already attached to host element, can now remove in favor of chipInput
+ this._elementRef.nativeElement.removeAttribute('aria-describedby');
}
/**
@@ -328,14 +325,18 @@ export class MatChipGrid
* are no eligible chips.
*/
override focus(): void {
- if (this.disabled || this._chipInput.focused) {
+ if (this.disabled || this._chipInput?.focused) {
return;
}
if (!this._chips.length || this._chips.first.disabled) {
+ if (!this._chipInput) {
+ return;
+ }
+
// Delay until the next tick, because this can cause a "changed after checked"
// error if the input does something on focus (e.g. opens an autocomplete).
- Promise.resolve().then(() => this._chipInput.focus());
+ Promise.resolve().then(() => this._chipInput!.focus());
} else {
const activeItem = this._keyManager.activeItem;
@@ -354,7 +355,11 @@ export class MatChipGrid
* @docs-private
*/
get describedByIds(): string[] {
- return this._chipInput?.describedByIds || [];
+ if (this._chipInput) {
+ return this._chipInput.describedByIds || [];
+ }
+ const existing = this._elementRef.nativeElement.getAttribute('aria-describedby');
+ return existing ? existing.split(' ') : [];
}
/**
@@ -365,7 +370,14 @@ export class MatChipGrid
// We must keep this up to date to handle the case where ids are set
// before the chip input is registered.
this._ariaDescribedbyIds = ids;
- this._chipInput?.setDescribedByIds(ids);
+
+ if (this._chipInput) {
+ this._chipInput.setDescribedByIds(ids);
+ } else if (ids.length) {
+ this._elementRef.nativeElement.setAttribute('aria-describedby', ids.join(' '));
+ } else {
+ this._elementRef.nativeElement.removeAttribute('aria-describedby');
+ }
}
/**
@@ -429,7 +441,7 @@ export class MatChipGrid
* it back to the first chip, creating a focus trap, if it user tries to tab away.
*/
protected override _allowFocusEscape() {
- if (!this._chipInput.focused) {
+ if (!this._chipInput?.focused) {
super._allowFocusEscape();
}
}
@@ -441,7 +453,7 @@ export class MatChipGrid
if (keyCode === TAB) {
if (
- this._chipInput.focused &&
+ this._chipInput?.focused &&
hasModifierKey(event, 'shiftKey') &&
this._chips.length &&
!this._chips.last.disabled
@@ -459,7 +471,7 @@ export class MatChipGrid
// disabled chip left in the list.
super._allowFocusEscape();
}
- } else if (!this._chipInput.focused) {
+ } else if (!this._chipInput?.focused) {
// The up and down arrows are supposed to navigate between the individual rows in the grid.
// We do this by filtering the actions down to the ones that have the same `_isPrimary`
// flag as the active action and moving focus between them ourseles instead of delegating
diff --git a/src/material/chips/chips.md b/src/material/chips/chips.md
index af5e6cb88a3c..3464f0acddbd 100644
--- a/src/material/chips/chips.md
+++ b/src/material/chips/chips.md
@@ -36,7 +36,7 @@ Users can move through the chips using the arrow keys and select/deselect them w
Use `` and `` for assisting users with text entry.
-Chips are always used inside a container. To create chips connected to an input field, start by creating a `` as the container. Add an `` element, and register it to the `` by passing the `matChipInputFor` Input. Always use an `` element with ``. Nest a `` element inside the `` for each piece of data entered by the user. An example of using chips for text input.
+Chips are always used inside a container. To create chips connected to an input field, start by creating a `` as the container. Add an `` element, and register it to the `` by passing the `matChipInputFor` Input. Nest a `` element inside the `` for each piece of data entered by the user. An example of using chips for text input.