npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@libs-ui/components-inputs-mention

v0.2.357-4

Published

> Angular directive thêm tính năng mention (gợi ý khi gõ ký tự trigger) vào input, textarea hoặc contenteditable elements.

Readme

@libs-ui/components-inputs-mention

Angular directive thêm tính năng mention (gợi ý khi gõ ký tự trigger) vào input, textarea hoặc contenteditable elements.

Giới thiệu

LibsUiComponentsInputsMentionDirective là một Angular standalone directive cho phép người dùng tìm kiếm và chọn các mục từ danh sách khi gõ một ký tự kích hoạt (ví dụ: @, #). Directive hỗ trợ đầy đủ cả input/textarea thông thường lẫn contenteditable elements và các rich text editor như Quill. Danh sách gợi ý được render thông qua dynamic component, tự định vị theo vị trí con trỏ trong văn bản.

Tính năng

  • Hỗ trợ nhiều ký tự trigger (@, #, ...) với cấu hình riêng biệt trong cùng một input
  • Hoạt động trên input, textarea và các phần tử contenteditable
  • Tích hợp sẵn với Quill editor (tự động phát hiện .ql-editor)
  • Tùy chỉnh template hiển thị từng mục trong danh sách gợi ý qua TemplateRef
  • Lọc danh sách theo từ khóa tìm kiếm (tích hợp sẵn hoặc custom filter)
  • Điều hướng bàn phím: mũi tên lên/xuống, Enter để chọn, Escape để đóng
  • Hỗ trợ iframe
  • Expose FunctionControl để chèn mention thủ công từ bên ngoài
  • Angular Signals + ChangeDetectionStrategy.OnPush cho hiệu năng tốt nhất
  • Tự động dọn dẹp dynamic component khi directive bị destroy

Khi nào sử dụng

  • Khi cần tính năng mention trong khung chat hoặc khu vực bình luận
  • Khi muốn hỗ trợ phím tắt gắn thẻ người dùng nhanh trong các form nhập liệu
  • Khi cần tích hợp mention vào các rich text editor (Quill, contenteditable)
  • Khi muốn hỗ trợ nhiều loại trigger khác nhau trong cùng một vùng nhập (@ cho người dùng, # cho tag)
  • Khi cần chèn mention thủ công từ nút bên ngoài (qua outFunctionControl)

Cài đặt

npm install @libs-ui/components-inputs-mention

Import

import { LibsUiComponentsInputsMentionDirective } from '@libs-ui/components-inputs-mention';
import { IMentionConfig, IMentionInsert, IMentionFunctionControlEvent } from '@libs-ui/components-inputs-mention';

@Component({
  standalone: true,
  imports: [LibsUiComponentsInputsMentionDirective],
  // ...
})
export class MyComponent {}

Ví dụ sử dụng

1. Cơ bản — input với trigger @

import { Component } from '@angular/core';
import { LibsUiComponentsInputsMentionDirective, IMentionConfig } from '@libs-ui/components-inputs-mention';

@Component({
  standalone: true,
  imports: [LibsUiComponentsInputsMentionDirective],
  template: `
    <input
      type="text"
      class="w-full p-2 border rounded"
      placeholder="Gõ @ để mention người dùng..."
      LibsUiComponentsInputsMentionDirective
      [mentionConfig]="basicConfig"
      (outSearchTerm)="handlerSearchTerm($event)"
      (outItemSelected)="handlerItemSelected($event)"
    />
  `,
})
export class CommentInputComponent {
  readonly basicConfig: IMentionConfig = {
    items: [
      { id: '1', name: 'Anh Tuấn', username: '[email protected]' },
      { id: '2', name: 'Bảo Ngọc', username: '[email protected]' },
      { id: '3', name: 'Công Thành', username: '[email protected]' },
    ],
    triggerChar: '@',
    labelKey: 'name',
    mentionFilter: (search, items) => {
      const s = search.toLowerCase();
      return items?.filter((item: any) => item.name.toLowerCase().includes(s));
    },
    mentionSelect: (item: any, triggerChar) => `${triggerChar}${item.name}`,
  };

  handlerSearchTerm(term: string | undefined): void {
    // term là chuỗi người dùng đang gõ sau @
  }

  handlerItemSelected(item: unknown): void {
    // item là object được chọn từ danh sách
  }
}

2. Nhiều trigger — @ cho người dùng và # cho tag

import { Component } from '@angular/core';
import { LibsUiComponentsInputsMentionDirective, IMentionConfig } from '@libs-ui/components-inputs-mention';

@Component({
  standalone: true,
  imports: [LibsUiComponentsInputsMentionDirective],
  template: `
    <textarea
      class="w-full p-2 border rounded min-h-[100px]"
      placeholder="Gõ @ cho người dùng, # cho tags..."
      LibsUiComponentsInputsMentionDirective
      [mentionConfig]="multiConfig"
    ></textarea>
  `,
})
export class MultiTriggerComponent {
  readonly users = [
    { id: '1', name: 'Anh Tuấn', username: 'tuan' },
    { id: '2', name: 'Bảo Ngọc', username: 'ngoc' },
  ];

  readonly tags = [
    { id: 't1', name: 'angular', username: 'AG' },
    { id: 't2', name: 'typescript', username: 'TS' },
  ];

  readonly filterByName = (search: string, items?: Array<any>) =>
    items?.filter((item) => item.name.toLowerCase().includes(search.toLowerCase()));

  readonly multiConfig: IMentionConfig = {
    items: this.users,
    triggerChar: '@',
    labelKey: 'name',
    mentionFilter: this.filterByName,
    mentionSelect: (item: any, triggerChar) => `${triggerChar}${item.name}`,
    mention: [
      {
        items: this.tags,
        triggerChar: '#',
        labelKey: 'name',
        mentionFilter: this.filterByName,
        mentionSelect: (item: any, triggerChar) => `${triggerChar}${item.name}`,
      },
    ],
  };
}

3. ContentEditable với custom template danh sách

import { Component } from '@angular/core';
import { LibsUiComponentsInputsMentionDirective, IMentionConfig } from '@libs-ui/components-inputs-mention';

@Component({
  standalone: true,
  imports: [LibsUiComponentsInputsMentionDirective],
  template: `
    <div
      contenteditable="true"
      class="w-full p-4 border rounded bg-white min-h-[100px]"
      LibsUiComponentsInputsMentionDirective
      [mentionConfig]="basicConfig"
      [mentionListTemplate]="customTpl"
    >
      Thử gõ @ ở đây...
    </div>

    <ng-template #customTpl let-item="item">
      <div class="flex items-center gap-3 p-3 hover:bg-blue-50 cursor-pointer">
        <div class="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center font-bold">
          {{ item.name[0] }}
        </div>
        <div>
          <div class="font-medium text-gray-800 text-sm">{{ item.name }}</div>
          <div class="text-xs text-gray-500">{{ item.username }}</div>
        </div>
      </div>
    </ng-template>
  `,
})
export class ContentEditableMentionComponent {
  readonly basicConfig: IMentionConfig = {
    items: [
      { id: '1', name: 'Anh Tuấn', username: '[email protected]' },
      { id: '2', name: 'Bảo Ngọc', username: '[email protected]' },
    ],
    triggerChar: '@',
    labelKey: 'name',
    mentionFilter: (search, items) =>
      items?.filter((item: any) => item.name.toLowerCase().includes(search.toLowerCase())),
    mentionSelect: (item: any) => `@${item.name}`,
  };
}

4. Chèn mention thủ công từ nút bên ngoài

import { Component } from '@angular/core';
import { LibsUiComponentsInputsMentionDirective, IMentionConfig, IMentionFunctionControlEvent } from '@libs-ui/components-inputs-mention';

@Component({
  standalone: true,
  imports: [LibsUiComponentsInputsMentionDirective],
  template: `
    <div
      contenteditable="true"
      class="w-full p-3 border rounded min-h-[80px]"
      LibsUiComponentsInputsMentionDirective
      [mentionConfig]="config"
      (outFunctionControl)="handlerFunctionControl($event)"
    ></div>

    <button
      class="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      (click)="handlerAddMention($event)"
    >
      Thêm @mention
    </button>
  `,
})
export class ManualMentionComponent {
  private mentionControl: IMentionFunctionControlEvent | undefined;

  readonly config: IMentionConfig = {
    items: [
      { id: '1', name: 'Anh Tuấn', username: 'tuan' },
      { id: '2', name: 'Bảo Ngọc', username: 'ngoc' },
    ],
    triggerChar: '@',
    labelKey: 'name',
    mentionFilter: (search, items) =>
      items?.filter((item: any) => item.name.toLowerCase().includes(search.toLowerCase())),
    mentionSelect: (item: any) => `@${item.name}`,
  };

  handlerFunctionControl(event: IMentionFunctionControlEvent): void {
    this.mentionControl = event;
  }

  handlerAddMention(event: Event): void {
    event.stopPropagation();
    this.mentionControl?.addMention(false);
  }
}

5. Lắng nghe sự kiện chèn mention (Rich Text Editor)

import { Component } from '@angular/core';
import { LibsUiComponentsInputsMentionDirective, IMentionConfig, IMentionInsert } from '@libs-ui/components-inputs-mention';

@Component({
  standalone: true,
  imports: [LibsUiComponentsInputsMentionDirective],
  template: `
    <div
      contenteditable="true"
      class="w-full p-3 border rounded min-h-[80px]"
      LibsUiComponentsInputsMentionDirective
      [mentionConfig]="config"
      (outInsertMention)="handlerInsertMention($event)"
      (outToggle)="handlerToggle($event)"
    ></div>
  `,
})
export class RteIntegrationComponent {
  readonly config: IMentionConfig = {
    items: [{ id: '1', name: 'Anh Tuấn', username: '[email protected]' }],
    triggerChar: '@',
    labelKey: 'name',
    mentionFilter: (search, items) =>
      items?.filter((item: any) => item.name.toLowerCase().includes(search.toLowerCase())),
    mentionSelect: (item: any) => `@${item.name}`,
  };

  handlerInsertMention(event: IMentionInsert): void {
    event; // { data: { id, feId, value }, lengthKey }
    // Dùng trong Quill: editor.deleteText(pos - event.lengthKey, event.lengthKey)
    // rồi editor.insertEmbed(pos, 'mention', event.data)
  }

  handlerToggle(isOpen: boolean): void {
    // isOpen = true khi danh sách gợi ý hiện, false khi đóng
  }
}

@Input()

| Input | Type | Default | Mô tả | Ví dụ | |---|---|---|---|---| | [timeDelayInit] | number | 0 | Thời gian delay (ms) trước khi directive gắn event listeners. Hữu ích khi host element cần render xong trước. | [timeDelayInit]="500" | | [mentionConfig] | IMentionConfig | undefined | Cấu hình chính cho mention: danh sách items, ký tự trigger, các tùy chọn filter/select. Bắt buộc phải truyền để directive hoạt động. | [mentionConfig]="myConfig" | | [mentionListTemplate] | TemplateRef<any> | undefined | Template tùy chỉnh cho từng mục trong danh sách gợi ý. Context: { item, index }. Nếu không truyền, dùng template mặc định (avatar + tên + username). | [mentionListTemplate]="myTpl" |

@Output()

| Output | Type | Mô tả | Handler TS | Binding HTML | |---|---|---|---|---| | (outSearchTerm) | string \| undefined | Emits từ khóa người dùng đang gõ sau ký tự trigger. Dùng để load data bất đồng bộ từ API. | handlerSearchTerm(term: string \| undefined): void { term; } | (outSearchTerm)="handlerSearchTerm($event)" | | (outItemSelected) | unknown | Emits object mục được chọn từ danh sách (trước khi chèn vào input). | handlerItemSelected(item: unknown): void { item; } | (outItemSelected)="handlerItemSelected($event)" | | (outToggle) | boolean | Emits true khi danh sách gợi ý mở ra, false khi đóng lại. | handlerToggle(isOpen: boolean): void { isOpen; } | (outToggle)="handlerToggle($event)" | | (outInsertMention) | IMentionInsert | Emits khi một mention được chèn thành công. Dùng cho rich text editor cần xử lý chèn node thủ công. | handlerInsertMention(e: IMentionInsert): void { e.data; e.lengthKey; } | (outInsertMention)="handlerInsertMention($event)" | | (outFunctionControl) | IMentionFunctionControlEvent | Emits object chứa phương thức điều khiển directive từ bên ngoài (ví dụ: chèn mention thủ công). Emits một lần sau ngAfterViewInit. | handlerFunctionControl(ctrl: IMentionFunctionControlEvent): void { this.ctrl = ctrl; } | (outFunctionControl)="handlerFunctionControl($event)" |

Types & Interfaces

import {
  IMentionConfig,
  IMentionItem,
  IMentionInsert,
  IMentionFunctionControlEvent,
  INodeInsert,
} from '@libs-ui/components-inputs-mention';
/**
 * Cấu hình chính truyền vào [mentionConfig].
 * Kế thừa IMentionItem và bổ sung các tùy chọn giao diện + đa trigger.
 */
export interface IMentionConfig extends IMentionItem {
  /** Tắt style mặc định của danh sách gợi ý */
  disableStyle?: boolean;
  /** ID của iframe chứa host element (khi dùng trong iframe) */
  iframe?: string;
  /** Cấu hình cho các trigger bổ sung (ngoài trigger chính) */
  mention?: Array<IMentionItem>;
}

/**
 * Cấu hình cho từng trigger riêng lẻ.
 */
export interface IMentionItem {
  /** Ký tự kích hoạt danh sách gợi ý. Mặc định: '@' */
  triggerChar?: string;
  /** Tên field trong object dùng làm nhãn hiển thị. Mặc định: 'email' */
  labelKey?: string;
  /** Tắt tự động sắp xếp danh sách items theo labelKey */
  disableSort?: boolean;
  /** Tắt bộ lọc nội bộ (dùng khi tự xử lý filter bằng outSearchTerm + API) */
  disableSearch?: boolean;
  /** Hiển thị danh sách phía trên con trỏ thay vì bên dưới */
  dropUp?: boolean;
  /** Cho phép nhập khoảng trắng khi đang search mention */
  allowSpace?: boolean;
  /** Giới hạn số khoảng trắng tối đa trong search query */
  limitSpaceSearchQuery?: number;
  /** Bao gồm ký tự trigger trong giá trị emit ra outSearchTerm */
  returnTrigger?: boolean;
  /** Hàm format text được chèn vào input sau khi chọn item */
  mentionSelect?: (item: unknown, triggerChar?: string) => string;
  /** Hàm lọc danh sách items theo search string */
  mentionFilter?: (searchString: string, items?: Array<unknown>) => Array<unknown> | undefined;
  /** Sự kiện DOM trên mention span đã chèn kích hoạt mentionActionByEvent */
  mentionEventName?: 'click' | 'mouseenter';
  /** Callback được gọi khi sự kiện mentionEventName xảy ra trên span mention */
  mentionActionByEvent?: (item: unknown, triggerChars?: string) => void;
  /** Danh sách items hiển thị trong danh sách gợi ý. Bắt buộc. */
  items: Array<Record<string, string | undefined>>;
  /** z-index của dropdown danh sách gợi ý. Mặc định: 2500 */
  zIndex?: number;
}

/**
 * Dữ liệu emit qua outInsertMention khi một mention được chèn thành công.
 * Dùng cho rich text editor tích hợp sâu (Quill, ProseMirror).
 */
export interface IMentionInsert {
  data: {
    /** ID định danh duy nhất (từ item.id hoặc uuid tự sinh) */
    id: string;
    /** Frontend ID dùng để track node trong DOM */
    feId: string;
    /** Giá trị text được chèn (kết quả của mentionSelect) */
    value: string;
  };
  /** Số ký tự cần xóa tính từ vị trí con trỏ để thay bằng mention node */
  lengthKey: number;
}

/**
 * Object emit qua outFunctionControl để điều khiển directive từ bên ngoài.
 */
export interface IMentionFunctionControlEvent {
  /**
   * Chèn ký tự trigger vào vị trí con trỏ hiện tại và mở danh sách gợi ý.
   * @param inputEvent - true nếu gọi từ sự kiện input
   */
  addMention: (inputEvent: boolean) => void;
}

Cấu hình mặc định (DEFAULT_CONFIG)

Khi không truyền đủ các option, directive áp dụng các giá trị mặc định sau:

| Thuộc tính | Giá trị mặc định | |---|---| | triggerChar | '@' | | labelKey | 'email' | | allowSpace | true | | returnTrigger | false | | limitSpaceSearchQuery | 3 | | mentionEventName | 'click' | | mentionSelect | (item) => '@' + item.email | | mentionFilter | Lọc theo field nameusername (hỗ trợ tiếng Việt bỏ dấu) | | zIndex | 2500 |

Điều hướng bàn phím

| Phím | Hành động | |---|---| | @, #, ... (trigger char) | Mở danh sách gợi ý | | Mũi tên xuống | Chọn item tiếp theo | | Mũi tên lên | Chọn item trước đó | | Enter hoặc Tab | Chèn item đang active vào input | | Escape | Đóng danh sách gợi ý | | Backspace về trước ký tự trigger | Đóng danh sách gợi ý | | Space (khi allowSpace: false) | Đóng danh sách gợi ý |

Lưu ý quan trọng

⚠️ mentionConfig là bắt buộc: Nếu không truyền [mentionConfig], directive sẽ không gắn event listeners và không hoạt động. Kiểm tra ngAfterViewInit trong source.

⚠️ ContentEditable vs Input: Logic xử lý caret và chèn mention khác nhau giữa input/textareacontenteditable. Với input/textarea, directive dùng selectionStart. Với contenteditable, directive dùng window.getSelection() để tính vị trí và chèn HTML node.

⚠️ outInsertMention chỉ emit với contenteditable: Output (outInsertMention) chỉ được emit khi host element là contenteditable (mode editor). Với input/textarea, mention được chèn trực tiếp vào value.

⚠️ Dependency bắt buộc: Directive phụ thuộc vào LibsUiDynamicComponentService để render danh sách gợi ý. Đảm bảo service này được cung cấp trong ứng dụng.

⚠️ mentionFilter phải được truyền để lọc hoạt động: Nếu không truyền mentionFilter, directive dùng hàm mặc định lọc theo field nameusername. Nếu data của bạn dùng field khác, hãy truyền hàm mentionFilter riêng.

⚠️ timeDelayInit với lazy-loaded editor: Khi dùng cùng Quill hoặc editor khởi tạo bất đồng bộ, truyền [timeDelayInit]="500" hoặc giá trị phù hợp để đảm bảo editor đã render xong trước khi directive gắn event.

⚠️ disableSearch để load data từ API: Khi muốn tự load danh sách từ API dựa trên từ khóa, set disableSearch: true trong config và lắng nghe (outSearchTerm) để gọi API. Sau khi nhận kết quả, cập nhật mentionConfig.items để danh sách hiển thị mới.

Demo

npx nx serve core-ui

Truy cập: http://localhost:4500/inputs/mention