Skip to content

Commit 8df63da

Browse files
committed
feat: handle majority of functinalities from commans component
1 parent a167295 commit 8df63da

20 files changed

+320
-215
lines changed

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@angular/platform-browser-dynamic": "^15.1.0",
3131
"@angular/router": "^15.1.0",
3232
"@ngneat/lib": "^5.0.0",
33-
"@ngneat/overview": "^3.0.4",
33+
"@ngneat/overview": "^4.1.0",
3434
"@ngneat/until-destroy": "^9.2.3",
3535
"rxjs": "~7.5.0",
3636
"tslib": "^2.3.0",

projects/ngneat/cmdk/schematics/ng-add/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const ngAdd = (options: Schema): Rule => (tree: Tree) => {
3636

3737
const addPackageJsonDependencies = (): Rule => (host: Tree, context: SchematicContext) => {
3838
const dependencies: { name: string; version: string }[] = [
39-
{ name: '@ngneat/overview', version: '3.0.4' },
39+
{ name: '@ngneat/overview', version: '4.1.0' },
4040
{ name: '@ngneat//until-destroy', version: '9.2.3' },
4141
];
4242

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

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,17 @@ import { SeparatorComponent } from './components/separator/separator.component';
88
import { CommonModule } from '@angular/common';
99
import { ItemDirective } from './directives/item/item.directive';
1010

11+
const ComponentsAndDirectives = [
12+
CommandComponent,
13+
InputDirective,
14+
EmptyDirective,
15+
GroupComponent,
16+
SeparatorComponent,
17+
ItemDirective,
18+
];
1119
@NgModule({
12-
declarations: [
13-
CommandComponent,
14-
InputDirective,
15-
EmptyDirective,
16-
GroupComponent,
17-
SeparatorComponent,
18-
ItemDirective,
19-
],
20+
declarations: ComponentsAndDirectives,
2021
imports: [CommonModule, DynamicViewModule],
21-
exports: [
22-
CommandComponent,
23-
InputDirective,
24-
EmptyDirective,
25-
GroupComponent,
26-
SeparatorComponent,
27-
ItemDirective,
28-
],
22+
exports: ComponentsAndDirectives,
2923
})
3024
export class CmdkModule {}
Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
import { Injectable } from '@angular/core';
2-
import { Subject } from 'rxjs';
2+
import { ReplaySubject, Subject } from 'rxjs';
33

44
@Injectable()
55
export class CmdkService {
6-
private _searchSub = new Subject<string | undefined>();
6+
private _searchSub = new Subject<string>();
77
search$ = this._searchSub.asObservable();
8+
private _valueSub = new ReplaySubject<string>(1);
9+
value$ = this._valueSub.asObservable();
10+
private _activeItemSub = new ReplaySubject<string>(1);
11+
activeItem$ = this._activeItemSub.asObservable();
12+
private _activeGroupSub = new ReplaySubject<string>(1);
13+
activeGroup$ = this._activeGroupSub.asObservable();
814

9-
setSearch(value: string | undefined) {
15+
setSearch(value: string) {
1016
this._searchSub.next(value);
1117
}
18+
19+
setValue(value: string) {
20+
this._valueSub.next(value);
21+
}
22+
23+
setActiveItem(itemId: string) {
24+
this._activeItemSub.next(itemId);
25+
}
26+
27+
setActiveGroup(groupId: string) {
28+
this._activeGroupSub.next(groupId);
29+
}
1230
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
</div>
55
</ng-template>
66

7-
<ng-container *dynamicView="cmdkContent"></ng-container>
7+
<ng-container *ngTemplateOutlet="cmdkContent"></ng-container>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
.cmdk-command {
2-
display: inline-flex;
2+
display: flex;
33
flex-direction: column;
44
}

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

Lines changed: 156 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,201 @@ import {
33
EventEmitter,
44
Input,
55
Output,
6-
ViewEncapsulation,
76
ChangeDetectionStrategy,
87
ContentChildren,
98
QueryList,
109
inject,
1110
ContentChild,
12-
AfterContentInit,
11+
HostListener,
12+
AfterViewInit,
13+
OnChanges,
14+
SimpleChanges,
1315
} from '@angular/core';
1416
import { Content } from '@ngneat/overview';
1517
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
18+
import { first } from 'rxjs';
1619
import { CmdkService } from '../../cmdk.service';
1720
import { EmptyDirective } from '../../directives/empty/empty.directive';
1821
import { ItemDirective } from '../../directives/item/item.directive';
1922
import { CmdkCommandProps } from '../../types';
23+
import { GroupComponent } from '../group/group.component';
24+
import { SeparatorComponent } from '../separator/separator.component';
2025

2126
let commandId = 0;
22-
@UntilDestroy({ checkProperties: true })
27+
@UntilDestroy()
2328
@Component({
2429
selector: 'cmdk-command',
2530
templateUrl: './command.component.html',
2631
styleUrls: ['./command.component.scss'],
2732
providers: [CmdkService],
2833
changeDetection: ChangeDetectionStrategy.OnPush,
29-
encapsulation: ViewEncapsulation.None,
3034
exportAs: 'cmdkCommand',
3135
})
32-
export class CommandComponent implements CmdkCommandProps, AfterContentInit {
36+
export class CommandComponent
37+
implements CmdkCommandProps, AfterViewInit, OnChanges
38+
{
39+
@Output() valueChanged = new EventEmitter<string>();
40+
@Input() value?: string;
3341
@Input() label?: Content;
3442
@Input() ariaLabel?: string;
35-
@Input() shouldFilter = true;
36-
@Input() filter?: (value: string, search: string) => boolean;
37-
@Input() value?: string;
38-
@Output() valueChanged = new EventEmitter<string>();
43+
@Input() filter: ((value: string, search: string) => boolean) | null = (
44+
value,
45+
search
46+
) => {
47+
const searchValue = search.toLowerCase();
48+
return value.toLowerCase().includes(searchValue);
49+
};
3950

4051
@ContentChildren(ItemDirective, { descendants: true })
4152
items: QueryList<ItemDirective> | undefined;
53+
@ContentChildren(GroupComponent, { descendants: true })
54+
groups: QueryList<GroupComponent> | undefined;
55+
@ContentChildren(SeparatorComponent, { descendants: true })
56+
separators: QueryList<SeparatorComponent> | undefined;
4257
@ContentChild(EmptyDirective) empty!: EmptyDirective;
4358

4459
readonly panelId = `cmdk-command-${commandId++}`;
4560

4661
private cmdkService = inject(CmdkService);
4762

48-
ngAfterContentInit() {
49-
if (this.shouldFilter) {
63+
ngOnChanges(changes: SimpleChanges) {
64+
if (
65+
changes['value'] &&
66+
changes['value'].previousValue !== changes['value'].currentValue &&
67+
this.value
68+
) {
69+
this.setValue(this.value);
70+
}
71+
}
72+
73+
ngAfterViewInit() {
74+
if (this.filter) {
5075
this.cmdkService.search$
5176
.pipe(untilDestroyed(this))
52-
.subscribe(() => this.handleSearch());
77+
.subscribe((s) => this.handleSearch(s));
5378
}
54-
if (this.items) {
55-
this.items.first.active = true;
79+
80+
if (!this.value) {
81+
this.makeFirstItemActive();
82+
this.makeFirstGroupActive();
5683
}
84+
85+
this.cmdkService.value$
86+
.pipe(untilDestroyed(this))
87+
.subscribe((value) => this.valueChanged.emit(value));
88+
89+
this.cmdkService.activeItem$
90+
.pipe(untilDestroyed(this))
91+
.subscribe((itemId) => {
92+
this.setActiveGroupForActiveItem(itemId);
93+
});
94+
}
95+
96+
get filteredItems() {
97+
return this.items?.filter((item) => item.filtered);
98+
}
99+
100+
get filteredGroups() {
101+
return this.groups?.filter((group) => group.filtered);
57102
}
58103

59-
handleSearch() {
60-
if (this.items) {
61-
this.empty.cmdkEmpty = !this.items.some((item) => item.filtered);
62-
this.items.first.active = true;
104+
handleSearch(search: string) {
105+
if (this.items?.length) {
106+
// filter items
107+
this.items?.forEach((item) => {
108+
item.filtered = this.filter ? this.filter(item.value, search) : true;
109+
});
110+
111+
// show/hide empty directive
112+
this.empty.cmdkEmpty = this.filteredItems?.length === 0;
113+
114+
// make first item active and in-turn it will also make first group active, if available
115+
this.makeFirstItemActive();
116+
117+
// show/hide group
118+
this.groups?.forEach((group) => {
119+
group.showGroup = group.filteredItems?.length > 0;
120+
group._cdr.markForCheck();
121+
});
122+
123+
// hide separator if search and filter both are present, else show
124+
this.separators?.forEach((seperator) => {
125+
seperator.showSeparator = !(this.filter && search);
126+
seperator.cdr.markForCheck();
127+
});
128+
}
129+
}
130+
131+
@HostListener('keyup', ['$event'])
132+
onKeyUp(ev: KeyboardEvent) {
133+
if (ev.key === 'ArrowDown') {
134+
this.makeNextItemActive();
135+
} else if (ev.key === 'ArrowUp') {
136+
this.makePreviousItemActive();
137+
}
138+
}
139+
140+
private makeFirstItemActive() {
141+
setTimeout(() => {
142+
const firstItem = this.filteredItems?.[0];
143+
if (firstItem) {
144+
this.cmdkService.setActiveItem(firstItem.itemId);
145+
}
146+
});
147+
}
148+
149+
private makeFirstGroupActive() {
150+
setTimeout(() => {
151+
const firstGroup = this.filteredGroups?.[0];
152+
if (firstGroup) {
153+
this.cmdkService.setActiveGroup(firstGroup.groupId);
154+
}
155+
});
156+
}
157+
private makePreviousItemActive() {
158+
this.cmdkService.activeItem$
159+
.pipe(first(), untilDestroyed(this))
160+
.subscribe((activeItemId) => {
161+
if (this.filteredItems?.length) {
162+
const activeItemIndex = this.filteredItems.findIndex(
163+
(item) => item.itemId === activeItemId
164+
);
165+
const nextActiveItem = this.filteredItems[activeItemIndex - 1];
166+
if (nextActiveItem) {
167+
const nextActiveItemId = nextActiveItem.itemId;
168+
this.cmdkService.setActiveItem(nextActiveItemId);
169+
}
170+
}
171+
});
172+
}
173+
174+
private setActiveGroupForActiveItem(nextActiveItemId: string) {
175+
const nextActiveGroupId = this.filteredGroups?.find((group) =>
176+
group.filteredItems.some((item) => item.itemId === nextActiveItemId)
177+
)?.groupId;
178+
if (nextActiveGroupId) {
179+
this.cmdkService.setActiveGroup(nextActiveGroupId);
63180
}
64181
}
182+
183+
private makeNextItemActive() {
184+
this.cmdkService.activeItem$
185+
.pipe(first(), untilDestroyed(this))
186+
.subscribe((activeItemId) => {
187+
if (this.filteredItems?.length) {
188+
const activeItemIndex = this.filteredItems.findIndex(
189+
(item) => item.itemId === activeItemId
190+
);
191+
const nextActiveItem = this.filteredItems[activeItemIndex + 1];
192+
if (nextActiveItem) {
193+
const nextActiveItemId = nextActiveItem.itemId;
194+
this.cmdkService.setActiveItem(nextActiveItemId);
195+
}
196+
}
197+
});
198+
}
199+
200+
private setValue(value: string) {
201+
this.cmdkService.setValue(value);
202+
}
65203
}
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<ng-template #cmdkGroup>
2-
<ng-container *ngIf="showGroup">
3-
<div role="presentation" *ngIf="label" class="cmdk-group-label">{{ label }}</div>
4-
<div class="cmdk-group" role="group" [attr.aria-label]="ariaLabel">
2+
<div class="cmdk-group" [ngClass]="{'cmdk-group-active': active}" *ngIf="showGroup" [id]="groupId">
3+
<div role="presentation" *ngIf="label" class="cmdk-group-label">
4+
<ng-container *dynamicView="label"></ng-container>
5+
</div>
6+
<div class="cmdk-group-content" role="group" [attr.aria-label]="ariaLabel">
57
<ng-content></ng-content>
68
</div>
7-
</ng-container>
9+
</div>
810
</ng-template>
911

10-
<ng-container *dynamicView="cmdkGroup"></ng-container>
12+
<ng-container *ngTemplateOutlet="cmdkGroup"></ng-container>
Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
.cmdk-group-label {
2-
font-weight: bold;
3-
}
4-
5-
.cmdk-group {
6-
display: inline-flex;
1+
.cmdk-group-content {
2+
display: flex;
73
flex-direction: column;
8-
width: 100%;
94
}

0 commit comments

Comments
 (0)