2.0.0 โ€ข Published 6 months ago

@ngneat/cmdk v2.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

MIT commitizen PRs styled with prettier All Contributors ngneat-lib spectator semantic-release npm

Fast, composable, unstyled command menu for Angular. Directly inspired from pacocoursey/cmdk

@ngneat/cmdk

@ngneat/cmdk is a command menu Angular component that can also be used as an accessible combobox. You render items, it filters and sorts them automatically. @ngneat/cmdk supports a fully composable API, so you can wrap items in other components or even as static HTML.

Demo and examples: ngneat.github.io/cmdk

Features

  • ๐ŸŽจ Un-styled, so that you can provide your own styles easily
  • ๐Ÿฅ™ Provides wrapper, so that you can pass your own template, component or static HTML
  • ๐Ÿ” Default filtering present
  • ๐Ÿ–ผ๏ธ Drop in stylesheet themes provided
  • โ™ฟ Accessible

Compatibility with Angular Versions

@ngneat/cmdkAngular
1.x<16
2.x>=16

Installation

Angular CLI

ng add @ngneat/cmdk

NPM

# For Angular version < 16

## First, install dependencies
npm install @ngneat/overview@4 @ngneat/until-destroy@9 @angular/cdk@15

## Then library
npm install @ngneat/cmdk@1

# For Angular version >= 16

## First, install dependencies
npm install @ngneat/overview@5 @ngneat/until-destroy@10 @angular/cdk@16

## Then library
npm install @ngneat/cmdk@2

Yarn

Same as npm, just instead of npm install, write yarn add.

Usage

1. Setup

1.1 Module Setup

This is taken care with ng add @ngneat/cmdk

import { CmdkModule } from '@ngneat/cmdk';

@NgModule({
  imports: [
    CmdkModule,
  ],
})
export class AppModule {}

1.2 Standalone Setup

import { AppComponent } from './src/app.component';

import {
  CommandComponent,
  GroupComponent,
  InputDirective,
  ItemDirective,
  ListComponent,
  EmptyDirective
} from '@ngneat/cmdk';

@Component({
    selector: 'app-root',
    standalone: true,
    imports: [
        CommandComponent,
        InputDirective,
        ListComponent,
        GroupComponent,
        ItemDirective,
        EmptyDirective
    ],
})

2. Start using it

<cmdk-command>
  <input cmdkInput />
  <div *cmdkEmpty>No results found.</div>
  <cmdk-list>
    <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>

Components

Each component has a specific class (starting with cmdk-) that can be used for styling.

Command

Render this to show the command menu.

SelectorClass
cmdk-command.cmdk-command

Properties

NameDescription
@Input() ariaLabel: stringAccessible Label for this command menu. Not shown visibly.
@Input() filter?: ((value: string, search: string) => boolean)Custom filter function for whether each command menu item should matches the given search query. It should return a boolean, false being hidden entirely. You can pass null to disable default filtering. Default: (value, search) => value.toLowerCase().includes(search.toLowerCase())
@Input() value?: stringOptional controlled state of the selected command menu item.
@Input() loading?: booleanOptional indicator to show loader
@Input() loop?: booleanOptionally set to true to turn on looping around when using the arrow keys.
@Output() valueChanged: EventEmitter<string>Event handler called when the selected item of the menu changes.

Input

Render this to show the command input.

SelectorClass
input[cmdkinput].cmdk-input

Properties

NameDescription
@Input() updateOn: 'blur' | 'change' | 'input'Optional indicator to provide event listener when filtering should happen.Default: input

List

Contains items and groups.

SelectorClass
cmdk-list.cmdk-list

Animate height using the --cmdk-list-height CSS variable.

.cmdk-list {
  min-height: 300px;
  height: var(--cmdk-list-height);
  max-height: 500px;
  transition: height 100ms ease;
}

To scroll item into view earlier near the edges of the viewport, use scroll-padding:

.cmdk-list {
  scroll-padding-block-start: 8px;
  scroll-padding-block-end: 8px;
}

Properties

NameDescription
@Input() ariaLabel?: stringAccessible Label for this command menu. Not shown visibly.

Item

Item that becomes active on pointer enter. You should provide a unique value for each item, but it will be automatically inferred from the .textContent.

Items will not unmount from the DOM, rather the cmdk-hidden attribute is applied to hide it from view. This may be relevant in your styling.

StateSelectorClass
Default[cmdkItem].cmdk-item
Active[cmdkItem][aria-selected].cmdk-item-active
Filtered[cmdkItem].cmdk-item-filtered
Disabled[cmdkItem].cmdk-item-disabled
Hidden (not-filtered)[cmdkItem][cmdk-hidden]

Properties

NameDescription
value: string | undefined;Contextual Value of the list-item
@Input() disabled: booleanContextually mark the item as disabled. Keyboard navigation will skip this item.
@Input() filtered: booleanContextually mark the item as filtered.
@Output() selected: EventEmitter<void>Event handler called when the item is selected

Group

Groups items together with the given label (.cmdk-group-label).

SelectorClass
cmdk-group.cmdk-group

Groups will not unmount from the DOM, rather the cmdk-hidden attribute is applied to hide it from view. This may be relevant in your styling.

Properties

NameDescription
@Input() label: ContentLabel for this command group. Can be HTML string
@Input() ariaLabel?: stringAccessible Label for this command menu. Not shown visibly.

Empty

Automatically renders when there are no results for the search query.

SelectorClass
*cmdkEmpty.cmdk-empty

Loader

This will be conditionally renderer when you pass loading=true with cmdk-command

SelectorClass
*cmdkLoader.cmdk-loader

Examples

Code snippets for common use cases.

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 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 find global stylesheets to drop in as a starting point for styling. See ngneat/cmdk/styles for examples.

You can include the SCSS stylesheet in your application's style file:

// Global is needed for any theme
@use "~@ngneat/cmdk/styles/scss/globals";

// Then add theme
@use "~@ngneat/cmdk/styles/scss/framer";
// @use "~@ngneat/cmdk/styles/scss/vercel";
// @use "~@ngneat/cmdk/styles/scss/linear";
// @use "~@ngneat/cmdk/styles/scss/raycast";

or, use pre-built CSS file in angular.json

// ...
"styles": [
  "...",
  "node_modules/@ngneat/cmdk/styles/globals.css"
  "node_modules/@ngneat/cmdk/styles/framer.css"
],
// ...

FAQ

Accessible? Yes. Labeling, aria attributes, and DOM ordering tested with Voice Over and Chrome DevTools.

Virtualization? No. Good performance up to 2,000-3,000 items, though. Read below to bring your own.

Filter/sort items manually? Yes. Pass filter={yourFilter} to Command. Better memory usage and performance. Bring your own virtualization this way.

Unstyled? Yes, use the listed CSS selectors.

Weird/wrong behavior? Make sure your [cdkItem] has a unique value.

Listen for โŒ˜K automatically? No, do it yourself to have full control over keybind context.

Contributors โœจ

Thanks goes to these wonderful people (emoji key):

This project follows the all-contributors specification. Contributions of any kind welcome!