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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<mat-label>Favorite Fruits</mat-label>
<mat-chip-grid #chipGrid aria-label="Fruit selection">
@for (fruit of fruits(); track $index) {
<mat-chip-row (removed)="remove(fruit)">
<mat-chip-row (removed)="remove(fruit)" [value]="fruit">
{{fruit}}
<button matChipRemove [attr.aria-label]="'remove ' + fruit">
<mat-icon>cancel</mat-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<mat-form-field class="example-form-field">
<mat-label>Video keywords</mat-label>
<mat-chip-grid #chipGrid aria-label="Enter keywords" [formControl]="formControl">
@for (keyword of keywords(); track keyword) {
<mat-chip-row (removed)="removeKeyword(keyword)">
@for (keyword of formControl.value; track keyword) {
<mat-chip-row (removed)="removeKeyword(keyword)" [value]="keyword">
{{keyword}}
<button matChipRemove [attr.aria-label]="'remove ' + keyword">
<mat-icon>cancel</mat-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,25 @@ import {MatIconModule} from '@angular/material/icon';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChipsFormControlExample {
readonly keywords = signal(['angular', 'how-to', 'tutorial', 'accessibility']);
readonly formControl = new FormControl(['angular']);

announcer = inject(LiveAnnouncer);
readonly formControl = new FormControl(['angular', 'how-to', 'tutorial', 'accessibility']);
private _announcer = inject(LiveAnnouncer);

removeKeyword(keyword: string) {
this.keywords.update(keywords => {
const index = keywords.indexOf(keyword);
if (index < 0) {
return keywords;
}

const keywords = this.formControl.value!;
const index = keywords.indexOf(keyword);
if (index > -1) {
keywords.splice(index, 1);
this.announcer.announce(`removed ${keyword}`);
return [...keywords];
});
this._announcer.announce(`removed ${keyword}`);
this.formControl.setValue([...keywords]);
}
}

add(event: MatChipInputEvent): void {
const value = (event.value || '').trim();

// Add our keyword
if (value) {
this.keywords.update(keywords => [...keywords, value]);
this.formControl.setValue([...this.formControl.value!, value]);
}

// Clear the input value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
[editable]="true"
(edited)="edit(fruit, $event)"
[aria-description]="'press enter to edit ' + fruit.name"
>
[value]="fruit.name">
<button matChipEdit [attr.aria-label]="'edit ' + fruit.name">
<mat-icon>edit</mat-icon>
</button>
</button>
{{fruit.name}}
<button matChipRemove [attr.aria-label]="'remove ' + fruit.name">
<mat-icon>cancel</mat-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ <h4>Chips inside of a Reactive form</h4>
<mat-form-field class="example-form-field">
<mat-label>Video keywords</mat-label>
<mat-chip-grid #reactiveChipGrid aria-label="Enter reactive form keywords" [formControl]="formControl">
@for (keyword of reactiveKeywords(); track keyword) {
<mat-chip-row (removed)="removeReactiveKeyword(keyword)">
@for (keyword of formControl.value; track keyword) {
<mat-chip-row (removed)="removeReactiveKeyword(keyword)" [value]="keyword">
{{keyword}}
<button matChipRemove [attr.aria-label]="'remove reactive form' + keyword">
<mat-icon>cancel</mat-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,25 @@ import {MatIconModule} from '@angular/material/icon';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChipsReactiveFormExample {
readonly reactiveKeywords = signal(['angular', 'how-to', 'tutorial', 'accessibility']);
readonly formControl = new FormControl(['angular']);

announcer = inject(LiveAnnouncer);
readonly formControl = new FormControl(['angular', 'how-to', 'tutorial', 'accessibility']);
private _announcer = inject(LiveAnnouncer);

removeReactiveKeyword(keyword: string) {
this.reactiveKeywords.update(keywords => {
const index = keywords.indexOf(keyword);
if (index < 0) {
return keywords;
}

const keywords = this.formControl.value!;
const index = keywords.indexOf(keyword);
if (index > -1) {
keywords.splice(index, 1);
this.announcer.announce(`removed ${keyword} from reactive form`);
return [...keywords];
});
this._announcer.announce(`removed ${keyword}`);
this.formControl.setValue([...keywords]);
}
}

addReactiveKeyword(event: MatChipInputEvent): void {
const value = (event.value || '').trim();

// Add our keyword
if (value) {
this.reactiveKeywords.update(keywords => [...keywords, value]);
this.announcer.announce(`added ${value} to reactive form`);
this.formControl.setValue([...this.formControl.value!, value]);
}

// Clear the input value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ <h4>Chips inside of a Template-driven form</h4>
<mat-label>Video keywords</mat-label>
<mat-chip-grid #templateChipGrid aria-label="Enter template form keywords" [(ngModel)]="templateKeywords">
@for (keyword of templateKeywords(); track keyword) {
<mat-chip-row (removed)="removeTemplateKeyword(keyword)">
<mat-chip-row (removed)="removeTemplateKeyword(keyword)" [value]="keyword">
{{keyword}}
<button matChipRemove [attr.aria-label]="'remove template form' + keyword">
<mat-icon>cancel</mat-icon>
Expand Down
7 changes: 4 additions & 3 deletions src/dev-app/chips/chips-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,11 @@ <h4>Input is last child of chip grid</h4>

<mat-form-field class="demo-has-chip-list">
<mat-label>New Contributor...</mat-label>
<mat-chip-grid #chipGrid1 [(ngModel)]="selectedPeople" required [disabled]="disableInputs">
<mat-chip-grid #chipGrid1 [(ngModel)]="people" required [disabled]="disableInputs">
@for (person of people; track person) {
<mat-chip-row
[editable]="editable"
[value]="person"
(removed)="remove(person)"
(edited)="edit(person, $event)">
@if (showEditIcon) {
Expand Down Expand Up @@ -202,9 +203,9 @@ <h4>Input is next sibling child of chip grid</h4>

<mat-form-field class="demo-has-chip-list">
<mat-label>New Contributor...</mat-label>
<mat-chip-grid #chipGrid2 [(ngModel)]="selectedPeople" required [disabled]="disableInputs">
<mat-chip-grid #chipGrid2 [(ngModel)]="people" required [disabled]="disableInputs">
@for (person of people; track person) {
<mat-chip-row (removed)="remove(person)">
<mat-chip-row (removed)="remove(person)" [value]="person">
{{person.name}}
<button matChipRemove aria-label="Remove contributor">
<mat-icon>close</mat-icon>
Expand Down
4 changes: 2 additions & 2 deletions src/dev-app/chips/chips-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ export class ChipsDemo {
// Enter, comma, semi-colon
separatorKeysCodes = [ENTER, COMMA, 186];

selectedPeople = null;

people: Person[] = [
{name: 'Kara', avatar: 'K'},
{name: 'Jeremy', avatar: 'J'},
Expand Down Expand Up @@ -107,6 +105,8 @@ export class ChipsDemo {
this.people.push({name: value});
}

console.log(this.people);

// Clear the input value
event.chipInput!.clear();
}
Expand Down
72 changes: 72 additions & 0 deletions src/material/chips/chip-grid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,59 @@ describe('MatChipGrid', () => {
}));
});

describe('chip grid with a form control', () => {
it('should emit the change event on blur if the set of chips changed', fakeAsync(() => {
const fixture = TestBed.createComponent(ChipGridWithFormControl);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();

const control = fixture.componentInstance.control;
const spy = jasmine.createSpy('change');
const subscription = control.valueChanges.subscribe(spy);
fixture.componentInstance.chips.push('one');
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();

expect(control.dirty).toBe(false);
expect(control.touched).toBe(false);
expect(spy).not.toHaveBeenCalled();

dispatchFakeEvent(fixture.nativeElement.querySelector('mat-chip-grid'), 'blur');
fixture.detectChanges();
flush();

expect(control.dirty).toBe(true);
expect(control.touched).toBe(true);
expect(spy).toHaveBeenCalledTimes(1);
subscription.unsubscribe();
}));

it('should not emit the change event on blur if the chips have not changed', fakeAsync(() => {
const fixture = TestBed.createComponent(ChipGridWithFormControl);
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();

const control = fixture.componentInstance.control;
const spy = jasmine.createSpy('change');
const subscription = control.valueChanges.subscribe(spy);

expect(control.dirty).toBe(false);
expect(control.touched).toBe(false);
expect(spy).not.toHaveBeenCalled();

dispatchFakeEvent(fixture.nativeElement.querySelector('mat-chip-grid'), 'blur');
fixture.detectChanges();
flush();

expect(control.dirty).toBe(false);
expect(control.touched).toBe(true);
expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
}));
});

function createComponent<T>(
component: Type<T>,
direction: Direction = 'ltr',
Expand Down Expand Up @@ -1234,3 +1287,22 @@ class ChipGridWithRemove {
this.chips.splice(event.chip.value, 1);
}
}

@Component({
template: `
<mat-form-field>
<mat-label>Enter a value</mat-label>
<mat-chip-grid #chipGrid [formControl]="control">
@for (value of chips; track value) {
<mat-chip-row [value]="value">{{ value }}</mat-chip-row>
}
</mat-chip-grid>
<input [matChipInputFor]="chipGrid"/>
</mat-form-field>
`,
imports: [MatChipGrid, MatChipRow, MatChipInput, MatFormField, MatLabel, ReactiveFormsModule],
})
class ChipGridWithFormControl {
control = new FormControl<string[]>([]);
chips: string[] = [];
}
32 changes: 27 additions & 5 deletions src/material/chips/chip-grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,11 +493,14 @@ export class MatChipGrid
/** Emits change event to set the model value. */
private _propagateChanges(): void {
const valueToEmit = this._chips.length ? this._chips.toArray().map(chip => chip.value) : [];
this._value = valueToEmit;
this.change.emit(new MatChipGridChange(this, valueToEmit));
this.valueChange.emit(valueToEmit);
this._onChange(valueToEmit);
this._changeDetectorRef.markForCheck();

if (!this._value || !arraysIdentical(this._value, valueToEmit)) {
this._value = valueToEmit;
this.change.emit(new MatChipGridChange(this, valueToEmit));
this.valueChange.emit(valueToEmit);
this._onChange(valueToEmit);
this._changeDetectorRef.markForCheck();
}
}

/** Mark the field as touched */
Expand All @@ -507,3 +510,22 @@ export class MatChipGrid
this.stateChanges.next();
}
}

/** Determines if two arrays are identical. */
function arraysIdentical(one: unknown[], two: unknown[]): boolean {
if (one === two) {
return true;
}

if (one.length !== two.length) {
return false;
}

for (let i = 0; i < one.length; i++) {
if (one[i] !== two[i]) {
return false;
}
}

return true;
}
Loading