Skip to content

Commit 050badc

Browse files
committed
feat: handle dynamic value and item changes
1 parent 10b59c3 commit 050badc

File tree

8 files changed

+195
-20
lines changed

8 files changed

+195
-20
lines changed

cypress/item.cy.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { TestsModule } from 'src/app/tests/tests.module';
2+
3+
describe('item', () => {
4+
beforeEach(() => {
5+
cy.mount(`<app-item></app-item>`, { imports: [TestsModule] });
6+
});
7+
8+
it('mounted item matches search', () => {
9+
cy.get(`[cmdkinput]`).type('b');
10+
cy.get(`[cmdkitem]`).should('not.be.visible');
11+
cy.get(`[data-testid="mount"]`).click();
12+
cy.get(`[cmdkitem]`).should('contain.text', 'B');
13+
});
14+
15+
it('mounted item does not match search', () => {
16+
cy.get(`[cmdkinput]`).type('z');
17+
cy.get(`[cmdkitem]`).should('not.be.visible');
18+
cy.get(`[data-testid="mount"]`).click();
19+
cy.get(`[cmdkitem]`).should('not.be.visible');
20+
});
21+
22+
it('unmount item that is selected', () => {
23+
cy.get(`[data-testid="mount"]`).click();
24+
cy.get(`[cmdkitem][aria-selected="true"]`).should('contain.text', 'A');
25+
cy.get(`[data-testid="unmount"]`).click();
26+
cy.get(`[cmdkitem]`).should('have.length', 1);
27+
cy.get(`[cmdkitem][aria-selected="true"]`).should('contain.text', 'B');
28+
});
29+
30+
it('unmount item that is the only result', () => {
31+
cy.get(`[data-testid="unmount"]`).click();
32+
cy.get(`[cmdkitem]`).should('have.length', 0);
33+
});
34+
35+
it('mount item that is the only result', () => {
36+
cy.get(`[data-testid="unmount"]`).click();
37+
cy.get(`.cmdk-empty`).should('have.length', 1);
38+
cy.get(`[data-testid="mount"]`).click();
39+
cy.get(`.cmdk-empty`).should('have.length', 0);
40+
cy.get(`[cmdkitem]`).should('have.length', 1);
41+
});
42+
43+
it('selected does not change when mounting new items', () => {
44+
cy.get(`[data-testid="mount"]`).click();
45+
cy.get(`[cmdkitem][data-value="b"]`).click();
46+
cy.get(`[cmdkitem][aria-selected="true"]`).should('contain.text', 'B');
47+
cy.get(`[data-testid="many"]`).click();
48+
cy.get(`[cmdkitem][aria-selected="true"]`).should('contain.text', 'B');
49+
});
50+
});
51+
52+
describe('item advanced', () => {
53+
beforeEach(() => {
54+
cy.mount('<app-item-advanced></app-item-advanced>', {
55+
imports: [TestsModule],
56+
});
57+
});
58+
59+
it('re-rendering re-matches implicit textContent value', () => {
60+
cy.get(`[cmdkitem]`).should('have.length', 2);
61+
cy.get(`[cmdkinput]`).type('2');
62+
const button = cy.get(`[data-testid="increment"]`);
63+
button.click();
64+
cy.get(`[cmdkitem]`).should('not.be.visible');
65+
button.click();
66+
cy.get(`[cmdkitem]`).should('have.length', 2);
67+
});
68+
});

projects/ngneat/cmdk/src/lib/cmdk.service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ export class CmdkService {
77
search$ = this._searchSub.asObservable();
88
private _itemClickedSub = new Subject<string>();
99
itemClicked$ = this._itemClickedSub.asObservable();
10+
private _itemValueChangedSub = new Subject<{
11+
oldValue: string;
12+
newValue: string;
13+
}>();
14+
itemValueChanged$ = this._itemValueChangedSub.asObservable();
1015

1116
setSearch(value: string) {
1217
this._searchSub.next(value);
@@ -15,4 +20,8 @@ export class CmdkService {
1520
itemClicked(value: string) {
1621
this._itemClickedSub.next(value);
1722
}
23+
24+
itemValueChanged(oldValue: string, newValue: string) {
25+
this._itemValueChangedSub.next({ oldValue, newValue });
26+
}
1827
}

projects/ngneat/cmdk/src/lib/components/command/command.component.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { SeparatorComponent } from '../separator/separator.component';
2525
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
2626
import { LoaderDirective } from '../../directives/loader/loader.directive';
2727
import { ListComponent } from '../list/list.component';
28+
import { race } from 'rxjs';
2829

2930
let commandId = 0;
3031
const GROUP_SELECTOR = 'cmdk-group';
@@ -64,6 +65,7 @@ export class CommandComponent
6465
separators: QueryList<SeparatorComponent> | undefined;
6566
@ContentChild(EmptyDirective) empty: EmptyDirective | undefined;
6667
@ContentChild(LoaderDirective) loader: LoaderDirective | undefined;
68+
search = '';
6769

6870
@HostBinding('attr.aria-label')
6971
get attrAriaLabel() {
@@ -94,6 +96,24 @@ export class CommandComponent
9496
}
9597

9698
ngAfterViewInit() {
99+
race(this.cmdkService.itemValueChanged$, this.items.changes)
100+
.pipe(untilDestroyed(this))
101+
.subscribe(() => {
102+
setTimeout(() => {
103+
if (this.keyManager) {
104+
this.keyManager.destroy();
105+
}
106+
// create key and focus managers
107+
this.keyManager = new ActiveDescendantKeyManager(this.items)
108+
.withWrap(this.loop)
109+
.skipPredicate((item) => item.disabled || !item.filtered);
110+
111+
if (this.filter) {
112+
this.handleSearch(this.search);
113+
}
114+
});
115+
});
116+
97117
// show/hide loader
98118
if (this.loader) {
99119
this.loader.cmdkLoader = this.loading;
@@ -149,32 +169,32 @@ export class CommandComponent
149169
}
150170

151171
handleSearch(search: string) {
172+
this.search = search;
152173
if (this.items?.length) {
153174
// filter items
154175
this.items?.forEach((item) => {
155176
item.filtered = this.filter ? this.filter(item.value, search) : true;
156177
});
157178

158-
// show/hide empty directive
159-
if (this.empty) {
160-
this.empty.cmdkEmpty = this.filteredItems?.length === 0;
161-
}
162-
163179
// make first item active and in-turn it will also make first group active, if available
164180
this.makeFirstItemActive();
181+
}
182+
// show/hide empty directive
183+
if (this.empty) {
184+
this.empty.cmdkEmpty = this.filteredItems?.length === 0;
185+
}
165186

166-
// show/hide group
167-
this.groups?.forEach((group) => {
168-
group.showGroup = group.filteredItems?.length > 0;
169-
group._cdr.markForCheck();
170-
});
187+
// show/hide group
188+
this.groups?.forEach((group) => {
189+
group.showGroup = group.filteredItems?.length > 0;
190+
group._cdr.markForCheck();
191+
});
171192

172-
// hide separator if search and filter both are present, else show
173-
this.separators?.forEach((seperator) => {
174-
seperator.showSeparator = !(this.filter && search);
175-
seperator.cdr.markForCheck();
176-
});
177-
}
193+
// hide separator if search and filter both are present, else show
194+
this.separators?.forEach((seperator) => {
195+
seperator.showSeparator = !(this.filter && search);
196+
seperator.cdr.markForCheck();
197+
});
178198
}
179199

180200
@HostListener('keydown', ['$event'])
@@ -185,6 +205,7 @@ export class CommandComponent
185205
this.filteredItems.length > 0
186206
) {
187207
this.valueChanged.emit(this.keyManager.activeItem.value);
208+
this.keyManager.activeItem.selected.emit();
188209
} else {
189210
this.keyManager.onKeydown(ev);
190211
}

projects/ngneat/cmdk/src/lib/directives/item/item.directive.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ export class ItemDirective
4444
private _value: string = '';
4545
@Input()
4646
set value(value: string) {
47-
this._value = value;
47+
if (value !== this.value) {
48+
this._cmdkService.itemValueChanged(this.value, value);
49+
this._value = value;
50+
}
4851
}
4952
get value() {
5053
return this._value
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Component } from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-item-advanced',
5+
template: `
6+
<div>
7+
<button data-testid="increment" (click)="setCount()">
8+
Increment count
9+
</button>
10+
<cmdk-command>
11+
<input cmdkInput placeholder="Search…" />
12+
<cmdk-list>
13+
<div *cmdkEmpty>No results.</div>
14+
<button cmdkItem [value]="'Item A ' + count">
15+
Item A {{ count }}
16+
</button>
17+
<button cmdkItem [value]="'Item B ' + count">
18+
Item B {{ count }}
19+
</button>
20+
</cmdk-list>
21+
</cmdk-command>
22+
</div>
23+
`,
24+
})
25+
export class ItemAdvancedComponent {
26+
count = 0;
27+
28+
setCount() {
29+
this.count++;
30+
}
31+
}

src/app/tests/item.component.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Component } from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-item',
5+
template: `
6+
<div>
7+
<button data-testid="mount" (click)="mount = !mount">
8+
Toggle item B
9+
</button>
10+
11+
<button data-testid="unmount" (click)="unmount = !unmount">
12+
Toggle item A
13+
</button>
14+
15+
<button data-testid="many" (click)="many = !many">
16+
Toggle many items
17+
</button>
18+
19+
<cmdk-command>
20+
<input cmdkInput placeholder="Search…" />
21+
<cmdk-list>
22+
<div *cmdkEmpty>No results.</div>
23+
<button cmdkItem *ngIf="!unmount">A</button>
24+
<ng-container *ngIf="many">
25+
<button cmdkItem>1</button>
26+
<button cmdkItem>2</button>
27+
<button cmdkItem>3</button>
28+
</ng-container>
29+
<button cmdkItem *ngIf="mount">B</button>
30+
</cmdk-list>
31+
</cmdk-command>
32+
</div>
33+
`,
34+
})
35+
export class ItemComponent {
36+
mount = false;
37+
unmount = false;
38+
many = false;
39+
}

src/app/tests/tests.module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { NgModule } from '@angular/core';
22
import { CommonModule } from '@angular/common';
33
import { GroupComponent } from './group.component';
44
import { CmdkModule } from '@ngneat/cmdk';
5+
import { ItemComponent } from './item.component';
6+
import { ItemAdvancedComponent } from './item-advanced.component';
57

68
@NgModule({
7-
declarations: [GroupComponent],
9+
declarations: [GroupComponent, ItemComponent, ItemAdvancedComponent],
810
imports: [CommonModule, CmdkModule.forRoot()],
9-
exports: [GroupComponent],
11+
exports: [GroupComponent, ItemComponent, ItemAdvancedComponent],
1012
})
1113
export class TestsModule {}

src/app/themes/vercel/vercel.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export class VercelComponent {
3030
items: [
3131
{
3232
label: 'Search Projects...',
33-
itemSelected: () => this.searchProjects(),
33+
itemSelected: () => {
34+
this.searchProjects();
35+
},
3436
icon: ProjectsIconComponent,
3537
shortcut: 'S P',
3638
},

0 commit comments

Comments
 (0)