Getting Started
Overview
Installation
Theme
Forms
Basic Inputs
Custom Inputs
Select
Components
Components
78
Micro Charts
Navigation
Libraries
Content Editor
Data View
Form Builder
Headless Editor
Image Designer
Kanban Board
PDF Builder
PDF Signer
PDF Viewer
Video Player
GitHub repositoryGitHub starsGitHub forksNgStarter UI npm versionNgStarter UI npm downloads per month

Mentions

Configure one mention plugin for teammates, emoji, commands, or other suggestions. Each configuration has its own trigger, async search callback and Angular option component. The editor keeps focus while the menu is open, so typing continues to filter suggestions.

Loading example…

Register the plugin

Import from @ngstarter-ui/components/headless-editor and install mentionEditorPlugin() through withHeadlessEditorPlugin(). Pass an array of configurations for triggers such as @, : and /. A single configuration object is also supported. One surface directive automatically selects the matching configuration and renders its option component. Triggers must be unique, non-empty and contain no whitespace. If triggers overlap, the longest matching trigger wins. All registrations use the same mention mark; these are not separate document mark types.

import {
  basicTextEditorPlugin, mentionEditorPlugin, NgsHeadlessEditorMentions,
  NgsHeadlessEditorSurface, provideNgsHeadlessEditor, withHeadlessEditorPlugin
} from '@ngstarter-ui/components/headless-editor';

const emoji = [{ id: 'rocket', label: 'rocket:', text: '🚀' }];

@Component({
  imports: [NgsHeadlessEditorSurface, NgsHeadlessEditorMentions],
  providers: [provideNgsHeadlessEditor(
    withHeadlessEditorPlugin(basicTextEditorPlugin()),
    withHeadlessEditorPlugin(mentionEditorPlugin([{
      trigger: '@',
      options: async query => users.filter(user =>
        user.label.toLocaleLowerCase().includes(query.toLocaleLowerCase())
      ),
      optionComponent: UserMentionOption
    }, {
      trigger: ':',
      options: async query => emoji.filter(item => item.label.includes(query)),
      optionComponent: EmojiMentionOption
    }, {
      trigger: '/',
      options: async query => commands.filter(item => item.label.includes(query)),
      optionComponent: CommandMentionOption
    }]))
  )],
  template: '<div ngsHeadlessEditorSurface ngsHeadlessEditorMentions></div>'
})
export class MessageEditor {}

Custom option component

Candidates require an id and label and may include any presentation data. Set text to insert a different value, such as an emoji glyph. When omitted, the inserted text is the trigger followed by the label. Set optionComponent separately in each configuration. The component receives that configuration's option and active inputs. Place avatars, descriptions, or other content inside it. The surrounding MenuItem handles selection and highlighting.

import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import {
  NgsHeadlessEditorMentionOption, NgsHeadlessEditorMentionOptionComponent
} from '@ngstarter-ui/components/headless-editor';

interface User extends NgsHeadlessEditorMentionOption {
  readonly role: string;
}

@Component({
  selector: 'app-user-mention-option',
  template: '<span>{{ option().label }}</span> <small>{{ option().role }}</small>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserMentionOption implements NgsHeadlessEditorMentionOptionComponent<User> {
  readonly option = input.required<User>();
  readonly active = input(false);
}

Per-surface menu template

Pass a Menu to ngsHeadlessEditorMentions to override the default menu. Import Menu and MenuItem from @ngstarter-ui/components/menu. Render suggestions(), give each item its optionId(index), highlight activeIndex(), and call select(option) when an item is clicked. Use optionComponent() to render the active configuration's component with NgComponentOutlet, or inspect activeTrigger() and registration() when your custom menu needs the active configuration's settings.

<!-- Import NgComponentOutlet, Menu and MenuItem in the host component. -->
<div ngsHeadlessEditorSurface
  [ngsHeadlessEditorMentions]="mentionMenu"
  #mentions="ngsHeadlessEditorMentions">
</div>

<ngs-menu #mentionMenu>
  @for (option of mentions.suggestions(); track option.id; let index = $index) {
    <ngs-menu-item
      [attr.id]="mentions.optionId(index)"
      [selected]="mentions.activeIndex() === index"
      (click)="mentions.select(option)">
      @if (mentions.optionComponent(); as component) {
        <ng-container [ngComponentOutlet]="component"
          [ngComponentOutletInputs]="{ option: option, active: mentions.activeIndex() === index }"/>
      } @else {
        {{ option.label }}
      }
    </ngs-menu-item>
  }
</ngs-menu>

Async search callback

Set options to a callback with the signature (query: string) => Promise<readonly User[]>. It receives the search text without the trigger, including an empty string when the user types only the configured symbol. Filter a local array as shown above, or return results from your backend. Returned candidates keep their order and are not filtered again by the plugin.

mentionEditorPlugin<User>({
  options: async query => {
    const response = await fetch('/api/users?search=' + encodeURIComponent(query));
    if (!response.ok) throw new Error('Could not load users');
    return response.json() as Promise<readonly User[]>;
  },
  optionComponent: UserMentionOption
})

The directive clears previous suggestions while searching and ignores outdated responses. Rejected searches leave the menu closed and expose the error through error(); loading() reports a pending search. A new query starts another search. Switching triggers starts the new configuration's search even when the query text stays the same. The mentionOptions and mentionTrigger surface overrides are supported for a single configuration. With multiple triggers, configure these values in the plugin's array. Ignoring an outdated response does not cancel its network request.

Search and keyboard behavior

  • Type a registered trigger at the start of text or after whitespace or an opening bracket. E-mail addresses do not open the menu.
  • Arrow keys choose a suggestion; Enter or Tab inserts it; Escape dismisses the current query.
  • The directive also handles mentions inside table cells when their marks allow mentions.
  • No menu opens during composition, for read-only editors, or when no candidates match.

Document value and undo

Selecting a candidate replaces the trigger and query with its insertion text and a trailing space in one undo step. Formatting around the query is preserved, and the space has no mention mark. The selected candidate is emitted through mentionSelected. The mark stores the trigger alongside its id and label, so different registrations can use the same candidate id. Each insertion gets a tokenId, so adjacent identical mentions remain independently deletable. Older documents without these attributes remain supported.

{
  "type": "text",
  "text": "@Anna Chen",
  "marks": [{ "type": "mention", "attrs": { "id": "anna", "label": "Anna Chen", "trigger": "@", "tokenId": "mention-1" } }]
}

Mentions are indivisible inline tokens. Users can delete the whole token and insert a new one; they cannot change individual characters. Backspace, Delete and replacing a selection that overlaps a token remove the entire token. Typing next to it creates ordinary text. The example uses no background highlight after insertion. Style .ngs-headless-editor-mention through the surface class with ::ng-deep or global styles, and map the mark explicitly in your HTML serializer.