Announcing @ngneat/cmdk β Fast, composable, unstyled command menu for Angular.
A few weeks ago I and Netanel Basal came up with an idea to create cmdk like a library for the Angular community.
And I am really glad that we ended up creating it, announcing...
π @ngneat/cmdk β Fast, composable, unstyled command menu for Angular.
Getting Started
You can install it through Angular CLI:
ng add @ngneat/cmdk
or with npm:
# First, install dependencies
npm install @ngneat/overview @ngneat/until-destroy @angular/cdk
npm install @ngneat/cmdk
When you install using npm or yarn, you will also need to import CmdkModule
in your app.module
.
import { CmdkModule } from '@ngneat/hot-toast';
@NgModule({
imports: [CmdkModule.forRoot()],
})
class AppModule {}
Features
π¨ Un-styled, so that you can provide your styles easily
π₯ Provides wrapper, so that you can pass your template, component or static HTML
π Default filtering present
πΌοΈ Drop in stylesheet themes provided
βΏ Accessible
β¨οΈ Keyboard interaction
Here are some basic examples:
<cmdk-command>
<input cmdkInput />
<cmdk-list>
<div *cmdkEmpty>No results found.</div>
<cmdk-group label="Letters">
<button cmdkItem>a</button>
<button cmdkItem>b</button>
<cmdk-separator></cmdk-separator>
<button cmdkItem>c</button>
</cmdk-group>
</cmdk-list>
<button cmdkItem>Apple</button>
</cmdk-command>
Nested items
Often selecting one item should navigate deeper, with a more refined set of items. For example, selecting "Change themeβ¦" should show new items "Dark theme" and "Light theme". We call these sets of items "pages", and they can be implemented with a simple state:
<cmdk-command (keydown)="onKeyDown($event)">
<input cmdkInput (input)="setSearch($event)" />
<ng-container *ngIf="!page">
<button cmdkItem (selected)="setPages('projects')">Search projects...</button>
<button cmdkItem (selected)="setPages('teams')">Join a team...</button>
</ng-container>
<ng-container *ngIf="page === 'projects'">
<button cmdkItem>Project A</button>
<button cmdkItem>Project B</button>
</ng-container>
<ng-container *ngIf="page === 'teams'">
<button cmdkItem>Team 1</button>
<button cmdkItem>Team 2</button>
</ng-container>
</cmdk-command>
pages: Array<string> = [];
search = '';
get page() {
return this.pages[this.pages.length - 1];
}
onKeyDown(e: KeyboardEvent) {
// Escape goes to previous page
// Backspace goes to previous page when search is empty
if (e.key === 'Escape' || (e.key === 'Backspace' && !this.search)) {
e.preventDefault();
this.pages = this.pages.slice(0, -1);
}
}
setSearch(ev: Event) {
this.search = (ev.target as HTMLInputElement)?.value;
}
setPages(page: string) {
this.pages.push(page);
}
Asynchronous results
Render the items as they become available. Filtering and sorting will happen automatically.
<cmdk-command [loading]="loading">
<input cmdkInput />
<div *cmdkLoader>Fetching words...</div>
<button cmdkItem *ngFor="let item of items" [value]="item">
{{item}}
</button>
</cmdk-command>
loading = false;
getItems() {
this.loading = true;
setTimeout(() => {
this.items = ['A', 'B', 'C', 'D'];
this.loading = false;
}, 3000);
}
Use inside Popover
We recommend using the Angular CDK Overlay. @ngneat/cdk relies on the Angular CDK, so this will reduce your bundle size a bit due to shared dependencies.
First, configure the trigger component:
<button (click)="isDialogOpen = !isDialogOpen" cdkOverlayOrigin #trigger="cdkOverlayOrigin" [attr.aria-expanded]="isDialogOpen">
Actions
<kbd>β</kbd>
<kbd>K</kbd>
</button>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isDialogOpen"
>
<app-sub-command-dialog [value]="value"></app-sub-command-dialog>
</ng-template>
isDialogOpen = false;
listener(e: KeyboardEvent) {
if (e.key === 'k' && (e.metaKey || e.altKey)) {
e.preventDefault();
if (this.isDialogOpen) {
this.isDialogOpen = false;
} else {
this.isDialogOpen = true;
}
}
}
ngOnInit() {
document.addEventListener('keydown', (ev) => this.listener(ev));
}
ngOnDestroy() {
document.removeEventListener('keydown', (ev) => this.listener(ev));
}
Then, render the cmdk-command
inside CDK Overlay content:
<div class="cmdk-submenu">
<cmdk-command>
<cmdk-list>
<cmdk-group [label]="value">
<button cmdkItem *ngFor="let item of items" [value]="item.label">
{{ item.label }}
</button>
</cmdk-group>
</cmdk-list>
<input cmdkInput #input placeholder="Search for actions..." />
</cmdk-command>
</div>
readonly items: Array<{ label: string }> = [
{
label: 'Open Application',
},
{
label: 'Show in Finder',
},
{
label: 'Show Info in Finder',
},
{
label: 'Add to Favorites',
},
];
ngAfterViewInit() {
this.input.nativeElement.focus();
}
Drop in stylesheets
You can use the provided global stylesheets to drop in as a starting point for styling.
First, include the SCSS
stylesheet in your application's style file:
// Global is needed for any theme
@use "../node_modules/@ngneat/cdk/styles/scss/globals";
// Then add theme
@use "../node_modules/@ngneat/cdk/styles/scss/raycast";
// @use "../node_modules/@ngneat/cdk/styles/scss/vercel";
// @use "../node_modules/@ngneat/cdk/styles/scss/framer";
// @use "../node_modules/@ngneat/cdk/styles/scss/linear";
or, use pre-built CSS
file in angular.json
// ...
"styles": [
"...",
"node_modules/@ngneat/cdk/styles/globals.css"
"node_modules/@ngneat/cdk/styles/raycast.css"
],
// ...
And then wrap your cmdk-command
in respective theme class:
<div class="raycast">
<cmdk-command>
<!-- ... -->
</cmdk-command>
</div>
And the output will be like the below:
Check out theme code examples at cmdk/src/app/themes.
Live Demo
A live demo is available at below link:
Time to give a β
If you liked our library, don't forget to give a β at the below link:
Do check out the library and let us know your feedback. You can reach out to me at @shhdharmen.
Happy Coding
π² π π