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-tabs

v0.2.357-4

Published

> Angular Standalone Component hiển thị thanh Tabs với hỗ trợ đầy đủ Angular Signals, Responsive "More" Menu tự động, Drag & Drop và nhiều chế độ căn chỉnh.

Readme

@libs-ui/components-tabs

Angular Standalone Component hiển thị thanh Tabs với hỗ trợ đầy đủ Angular Signals, Responsive "More" Menu tự động, Drag & Drop và nhiều chế độ căn chỉnh.

Giới thiệu

LibsUiComponentsTabsComponent là một standalone Angular component được xây dựng hoàn toàn trên Angular Signals. Component tự động tính toán và ẩn các tab không vừa chiều ngang vào menu "More" (chế độ left), hỗ trợ kéo thả để sắp xếp lại thứ tự, cho phép kiểm soát chuyển tab bằng callback bất đồng bộ, và cung cấp FunctionsControl để điều khiển từ component cha qua viewChild.

Tính năng

  • Angular Signals: Full support — itemsWritableSignal<Array<WritableSignal<ITabsItem>>>, phản ứng ngay khi signal bên trong thay đổi.
  • Responsive "More" Menu: Chế độ left tự động ẩn các tab thừa vào popover "Xem thêm" khi container hẹp.
  • Calculator V2: Thuật toán tính chiều rộng thế hệ mới dùng ResizeObserver, không flicker, không reorder DOM.
  • Drag & Drop: Hỗ trợ kéo thả để thay đổi vị trí tab (tích hợp @libs-ui/components-drag-drop).
  • 4 chế độ hiển thị: left, center, space-between, center-has-line.
  • Rich Content: Mỗi tab item hỗ trợ icon trái/phải, badge số lượng, red dot, ảnh avatar, nút action trái/phải.
  • Guard chuyển tab: Input checkCanChangeTabSelected cho phép chặn hoặc xác nhận trước khi đổi tab (hỗ trợ async).
  • FunctionsControl: Expose API addTabsItem, selectedTabsItem, calculatorTabsItemsDisplay ra ngoài qua outFunctionsControl.
  • OnPush Change Detection: Tối ưu hiệu năng cho danh sách tab lớn.

Khi nào sử dụng

  • Phân chia nội dung thành nhiều view/module trong cùng một màn hình (trang chi tiết, dashboard).
  • Thanh điều hướng ngang với số tab không cố định — tính năng "More" sẽ gom phần thừa tự động.
  • Tab có thể thêm/xóa động (browser-like tabs) — dùng FunctionsControl.addTabsItem.
  • Cần quy trình dạng bước (step wizard) — dùng mode="center-has-line" kết hợp hasStep.
  • Danh sách tab cần sắp xếp lại bằng kéo thả.

Cài đặt

npm install @libs-ui/components-tabs

Import

import { LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';
import { ITabs, ITabsItem, ITabsFunctionControlEvent, ITabsItemEvent, ITabCssConfig, TYPE_TAB_MODE } from '@libs-ui/components-tabs';

@Component({
  standalone: true,
  imports: [LibsUiComponentsTabsComponent],
})
export class YourComponent {}

Ví dụ sử dụng

Ví dụ 1 — Basic (tabs căn trái, responsive tự động)

import { signal, WritableSignal } from '@angular/core';
import { ITabs, ITabsItem, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsTabsComponent],
  template: `
    <libs_ui-components-tabs
      [tabs]="tabsConfig"
      [(keySelected)]="selectedKey"
      (outKeySelected)="handlerKeySelected($event)"
    />
    <p>Tab đang chọn: {{ selectedKey() }}</p>
  `,
})
export class BasicTabsComponent {
  protected selectedKey = signal<string>('overview');

  protected tabsConfig: ITabs = {
    items: signal<Array<WritableSignal<ITabsItem>>>([
      signal({ key: 'overview', label: 'Tổng quan' }),
      signal({ key: 'detail', label: 'Chi tiết' }),
      signal({ key: 'history', label: 'Lịch sử' }),
      signal({ key: 'setting', label: 'Cài đặt', disable: true }),
    ]),
  };

  protected handlerKeySelected(key: string): void {
    // event.stopPropagation() không cần ở đây vì outKeySelected là OutputEmitterRef
    console.log('Tab đã chọn:', key);
  }
}

Ví dụ 2 — Center mode với icon và badge

import { signal, WritableSignal } from '@angular/core';
import { ITabs, ITabsItem, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsTabsComponent],
  template: `
    <libs_ui-components-tabs
      [tabs]="tabsConfig"
      [(keySelected)]="selectedKey"
      mode="center"
    />
  `,
})
export class CenterTabsComponent {
  protected selectedKey = signal<string>('messages');

  protected tabsConfig: ITabs = {
    hasCount: true,
    items: signal<Array<WritableSignal<ITabsItem>>>([
      signal({
        key: 'messages',
        label: 'Tin nhắn',
        iconLeft: 'libs-ui-icon-mail',
        count: 5,
        classCircle: 'bg-red-500 text-white',
      }),
      signal({
        key: 'notifications',
        label: 'Thông báo',
        iconLeft: 'libs-ui-icon-bell',
        count: 99,
        modeCount: 'x+',
        maxCount: 9,
        classCircle: 'bg-blue-500 text-white',
      }),
      signal({
        key: 'profile',
        label: 'Hồ sơ',
        iconRight: 'libs-ui-icon-user',
        hasRedDot: true,
      }),
    ]),
  };
}

Ví dụ 3 — Drag & Drop với Calculator V2

import { signal, WritableSignal } from '@angular/core';
import { ITabs, ITabsItem, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsTabsComponent],
  template: `
    <libs_ui-components-tabs
      [tabs]="tabsConfig"
      [(keySelected)]="selectedKey"
      [allowDragDropPosition]="true"
      [useCalculatorV2]="true"
      (outDragTabChange)="handlerDragChange()"
    />
  `,
})
export class DragTabsComponent {
  protected selectedKey = signal<string>('tab1');

  protected tabsConfig: ITabs = {
    items: signal<Array<WritableSignal<ITabsItem>>>([
      signal({ key: 'tab1', label: 'Mục 1' }),
      signal({ key: 'tab2', label: 'Mục 2' }),
      signal({ key: 'tab3', label: 'Mục 3' }),
      signal({ key: 'tab4', label: 'Mục 4' }),
    ]),
  };

  protected handlerDragChange(): void {
    console.log('Thứ tự tab đã thay đổi');
  }
}

Ví dụ 4 — Guard chuyển tab (async confirmation)

import { signal, WritableSignal } from '@angular/core';
import { ITabs, ITabsItem, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsTabsComponent],
  template: `
    <libs_ui-components-tabs
      [tabs]="tabsConfig"
      [(keySelected)]="selectedKey"
      [checkCanChangeTabSelected]="checkCanChange"
    />
  `,
})
export class GuardedTabsComponent {
  protected selectedKey = signal<string>('tab1');
  protected hasUnsavedChanges = signal<boolean>(true);

  protected tabsConfig: ITabs = {
    items: signal<Array<WritableSignal<ITabsItem>>>([
      signal({ key: 'tab1', label: 'Form nhập liệu' }),
      signal({ key: 'tab2', label: 'Xem trước' }),
    ]),
  };

  protected checkCanChange = async (): Promise<boolean> => {
    if (!this.hasUnsavedChanges()) return true;
    return confirm('Bạn có thay đổi chưa lưu. Tiếp tục chuyển tab?');
  };
}

Ví dụ 5 — FunctionsControl để thêm tab động

import { signal, WritableSignal } from '@angular/core';
import { ITabs, ITabsItem, ITabsFunctionControlEvent, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsTabsComponent],
  template: `
    <libs_ui-components-tabs
      [tabs]="tabsConfig"
      [(keySelected)]="selectedKey"
      [useEffectUpdateItems]="true"
      (outFunctionsControl)="handlerFunctionsControl($event)"
    />
    <button (click)="addNewTab()">Thêm tab</button>
  `,
})
export class DynamicTabsComponent {
  protected selectedKey = signal<string>('tab1');
  private functionsControl: ITabsFunctionControlEvent | undefined;

  protected tabsConfig: ITabs = {
    allowRemove: true,
    items: signal<Array<WritableSignal<ITabsItem>>>([
      signal({ key: 'tab1', label: 'Tab đầu tiên' }),
    ]),
  };

  protected handlerFunctionsControl(fc: ITabsFunctionControlEvent): void {
    this.functionsControl = fc;
  }

  protected addNewTab(): void {
    const key = `tab_${Date.now()}`;
    const newItem = signal<ITabsItem>({ key, label: `Tab ${key.slice(-4)}` });
    this.functionsControl?.addTabsItem(newItem, true);
  }
}

Ví dụ 6 — Step mode (quy trình dạng bước)

import { signal, WritableSignal } from '@angular/core';
import { ITabs, ITabsItem, LibsUiComponentsTabsComponent } from '@libs-ui/components-tabs';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsTabsComponent],
  template: `
    <libs_ui-components-tabs
      [tabs]="tabsConfig"
      [(keySelected)]="selectedKey"
      mode="center-has-line"
      [ignoreCalculatorTab]="true"
    />
  `,
})
export class StepTabsComponent {
  protected selectedKey = signal<string>('step1');

  protected tabsConfig: ITabs = {
    hasStep: true,
    stepCompleted: 1,
    items: signal<Array<WritableSignal<ITabsItem>>>([
      signal({ key: 'step1', label: 'Thông tin cơ bản' }),
      signal({ key: 'step2', label: 'Xác minh' }),
      signal({ key: 'step3', label: 'Hoàn tất' }),
    ]),
  };
}

@Input()

| Input | Type | Default | Mô tả | Ví dụ | |---|---|---|---|---| | tabs | ITabs | Required | Cấu hình chính chứa danh sách items dạng nested Signal. | [tabs]="tabsConfig" | | keySelected | string (model) | Required | Key của tab đang chọn, hỗ trợ two-way binding. | [(keySelected)]="selectedKey" | | mode | TYPE_TAB_MODE | 'left' | Chế độ căn chỉnh: 'left', 'center', 'space-between', 'center-has-line'. | mode="center" | | fieldKey | string | 'key' | Tên trường làm định danh duy nhất trong object item. | [fieldKey]="'id'" | | fieldLabel | string | 'label' | Tên trường hiển thị nhãn tab (hỗ trợ i18n key). | [fieldLabel]="'name'" | | disable | boolean | undefined | Vô hiệu hóa toàn bộ component tabs. | [disable]="true" | | disableLabel | boolean | undefined | Ẩn nhãn văn bản trên tab item. | [disableLabel]="true" | | heightTabItem | number | 40 | Chiều cao của thanh tab header tính bằng px. | [heightTabItem]="48" | | ignoreCalculatorTab | boolean | false | Bỏ qua tính toán responsive — dùng khi biết trước số tab vừa vặn. | [ignoreCalculatorTab]="true" | | size | 'langer' \| 'medium' | 'medium' | Kích thước tổng thể của component. | size="langer" | | allowDragDropPosition | boolean | undefined | Cho phép kéo thả thay đổi thứ tự tab. | [allowDragDropPosition]="true" | | zIndex | number | undefined | Z-index áp dụng cho popover More và overlay. | [zIndex]="100" | | configCss | ITabCssConfig (model) | undefined | Override CSS padding/margin cho các mode. Tự động set nếu không truyền. | [(configCss)]="cssConfig" | | popoverShowMoreTabItem | IPopover | undefined | Cấu hình vị trí và style cho popover menu "Xem thêm". | [popoverShowMoreTabItem]="popoverCfg" | | checkCanChangeTabSelected | () => boolean \| Promise<boolean> | undefined | Callback gác cổng trước khi chuyển tab. Trả về false để hủy. | [checkCanChangeTabSelected]="checkFn" | | useEffectUpdateItems | boolean | false | Dùng effect() để tự động cập nhật danh sách hiển thị khi signal items thay đổi từ bên ngoài. Bật khi tab list là động. | [useEffectUpdateItems]="true" | | useCalculatorV2 | boolean | false | Bật thuật toán V2 dùng ResizeObserver — không flicker, không reorder DOM. Khuyến nghị cho tab mới. | [useCalculatorV2]="true" |

@Output()

| Output | Type | Mô tả | Handler TS | Binding HTML | |---|---|---|---|---| | (outKeySelected) | string | Emit key của tab vừa được chọn. | handlerKeySelected(key: string): void { /* key là tab key */ } | (outKeySelected)="handlerKeySelected($event)" | | (outFunctionsControl) | ITabsFunctionControlEvent | Emit object chứa API điều khiển tabs: addTabsItem, selectedTabsItem, calculatorTabsItemsDisplay. | handlerFunctionsControl(fc: ITabsFunctionControlEvent): void { this.tabsFc = fc; } | (outFunctionsControl)="handlerFunctionsControl($event)" | | (outDragTabChange) | void | Emit sau khi người dùng kéo thả hoàn tất, thứ tự items đã cập nhật. | handlerDragChange(): void { /* đọc lại tabs().items() */ } | (outDragTabChange)="handlerDragChange()" | | (outDisplayMoreItem) | boolean | Emit true khi có tab bị ẩn vào menu More, false khi tất cả tab vừa. | handlerDisplayMore(visible: boolean): void { this.hasMore.set(visible); } | (outDisplayMoreItem)="handlerDisplayMore($event)" | | (outAction) | ITabsItemEvent | Emit khi người dùng click vào action item (nút remove, configButtonLeft/Right). key'remove' hoặc key action tùy chỉnh. | handlerAction(event: ITabsItemEvent): void { if (event.key === 'remove') this.removeTab(event.item); } | (outAction)="handlerAction($event)" |

Types & Interfaces

import {
  ITabs,
  ITabsItem,
  ITabCssConfig,
  ITabsFunctionControlEvent,
  ITabsItemEvent,
  TYPE_TAB_MODE,
} from '@libs-ui/components-tabs';
/** Cấu hình tổng thể cho component tabs */
export interface ITabs {
  /** Danh sách các tab item dạng nested Signal — BẮT BUỘC */
  items: WritableSignal<Array<WritableSignal<ITabsItem>>>;

  /** Hiển thị ảnh avatar bên trái nhãn tab */
  hasImage?: boolean;

  /** Hiển thị badge số đếm bên phải nhãn tab */
  hasCount?: boolean;

  /** Hiển thị nút xóa (remove) trên mỗi tab */
  allowRemove?: boolean;

  /** Cấu hình nút xóa (IButton) */
  configButtonRemove?: IButton;

  /** Chế độ step wizard — hiển thị số thứ tự trên mỗi tab */
  hasStep?: boolean;

  /** Số bước đã hoàn thành — dùng với hasStep */
  stepCompleted?: number;

  /** Hiển thị nền cho step đã hoàn thành */
  stepHasBackGround?: boolean;

  /** Bỏ qua nền cho tab đang selected trong step mode */
  ignoreSelectedBackgroundStep?: boolean;

  /** Ẩn đường kẻ dưới trên tab header */
  ignoreShowLineBottomInTab?: boolean;

  /** Class CSS tùy chỉnh cho phần header */
  classIncludeHeader?: string;

  /** Class CSS cho vùng center của header */
  classIncludeHeaderCenter?: string;

  /** Class CSS cho vùng right của header */
  classIncludeHeaderRight?: string;

  /** Class CSS áp dụng lên mỗi tab item */
  classIncludeItem?: string;

  /** Class CSS áp dụng lên tab item đang active */
  classIncludeActiveItem?: string;

  /** Giới hạn chiều rộng tối đa (px) của nhãn tab */
  maxWidthTextLabelItem?: number;

  /** Cấu hình action ở góc phải header (popover với danh sách) */
  actionRightConfig?: WritableSignal<{
    getListViewConfig: TYPE_FUNCTION<WritableSignal<IListConfigItem>>;
    config?: WritableSignal<IPopoverOverlay>;
    onlyShowWhenHoverItemActive?: boolean;
    classInclude?: string;
    customView?: () => Observable<string>;
  }>;

  /** Bỏ qua margin-left cho nút "Xem thêm" */
  viewMoreIgnoreMarginLeft?: boolean;
}

/** Cấu hình cho từng tab item */
export interface ITabsItem {
  /** Định danh duy nhất của tab */
  key?: string;

  /** Class CSS bổ sung cho tab item */
  classInclude?: string;

  /** Vô hiệu hóa tab item này */
  disable?: boolean;

  /** Nhãn hiển thị (hỗ trợ i18n key) */
  label?: string;

  /** Class CSS cho nhãn */
  classLabel?: string;

  /** Icon class bên trái nhãn (vd: 'libs-ui-icon-mail') */
  iconLeft?: string;

  /** Icon class bên phải nhãn */
  iconRight?: string;

  /** Hiển thị red dot trên tab */
  hasRedDot?: boolean;

  /** Số đếm hiển thị dạng badge */
  count?: number;

  /** Chế độ hiển thị badge: 'x' | '0x' | 'x+' */
  modeCount?: TYPE_BADGE_MODE;

  /** Số tối đa hiển thị trước khi thêm '+' */
  maxCount?: number;

  /** Class CSS cho vòng tròn badge */
  classCircle?: string;

  /** URL ảnh avatar bên trái */
  linkImage?: string;

  /** URL ảnh fallback khi ảnh chính lỗi */
  linkImageError?: string;

  /** Trạng thái invalid — đổi màu sang đỏ trong step mode */
  invalid?: boolean;

  /** Nút action bên phải tab item */
  configButtonRight?: IButton;

  /** Nút action bên trái tab item */
  configButtonLeft?: IButton;

  /** Chiều rộng đã đo được — do component tự tính, không set thủ công */
  specificWidth?: number;

  /** Trạng thái hiển thị — do component tự quản lý */
  specificDisplay?: boolean;

  /** Thứ tự sắp xếp — do component tự quản lý */
  order?: number;

  /** Cho phép thêm thuộc tính tuỳ chỉnh */
  [param: string]: any;
}

/** Override CSS theo từng mode */
export interface ITabCssConfig {
  /** Class áp dụng cho tab đầu tiên */
  first: string;
  /** Class áp dụng cho các tab còn lại */
  other: string;
  /** Class áp dụng cho header wrapper */
  header?: string;
  /** Class áp dụng cho phần center của header */
  headerCenter?: string;
}

/** API điều khiển tabs từ bên ngoài (nhận qua outFunctionsControl) */
export interface ITabsFunctionControlEvent {
  /** Thêm tab item mới vào danh sách */
  addTabsItem: (
    item: WritableSignal<ITabsItem>,
    selected?: boolean,    // true = chuyển sang tab mới ngay
    addFirst?: boolean,    // true = thêm vào đầu danh sách
    indexAdd?: number      // vị trí cụ thể để chèn vào
  ) => Promise<void>;

  /** Tính toán lại các tab cần hiển thị (dùng sau khi resize thủ công) */
  calculatorTabsItemsDisplay: () => Promise<void>;

  /** Chuyển sang tab theo key */
  selectedTabsItem: (
    key: string,
    resetDisable?: boolean  // true = bật lại tab dù đang disable
  ) => Promise<void>;
}

/** Dữ liệu emit từ outAction */
export interface ITabsItemEvent {
  /** Key của action: 'remove' hoặc key action từ configButtonLeft/Right */
  key: string;
  /** Data của tab item liên quan */
  item: ITabsItem;
}

/** Chế độ hiển thị của component */
export type TYPE_TAB_MODE = 'left' | 'center' | 'space-between' | 'center-has-line';

Lưu ý quan trọng

⚠️ Nested Signals bắt buộc: tabs.items PHẢI là WritableSignal<Array<WritableSignal<ITabsItem>>>. Không truyền plain array — component sẽ không phản ứng khi thêm/xóa item.

⚠️ useEffectUpdateItems khi items thay đổi động: Khi danh sách tab được thay đổi từ bên ngoài component (vd: sau khi nhận dữ liệu từ API hoặc Modal), BẮT BUỘC bật [useEffectUpdateItems]="true" để component tự cập nhật danh sách hiển thị.

⚠️ Calculator V2 khuyến nghị cho code mới: [useCalculatorV2]="true" dùng ResizeObserver thay vì MutationObserver + setTimeout — không flicker, không reorder DOM, an toàn với ChangeDetectionStrategy.OnPush. Mặc định false để giữ backward compatible với code cũ.

⚠️ mode center-has-line cho step wizard: Kết hợp với tabs.hasStep = truetabs.stepCompleted để hiển thị chỉ báo bước hoàn thành. Nên dùng với [ignoreCalculatorTab]="true" khi số bước cố định.

⚠️ allowRemove cần xử lý outAction: Khi bật tabs.allowRemove, lắng nghe (outAction) để xử lý sự kiện key === 'remove'. Component chỉ emit event, không tự xóa item khỏi danh sách.

⚠️ fieldKey và fieldLabel cho data custom: Khi object item dùng tên trường khác key/label (vd: từ API trả về id/name), phải truyền [fieldKey]="'id'"[fieldLabel]="'name'" để component đọc đúng trường.

Demo

npx nx serve core-ui

Truy cập: http://localhost:4500/tabs

Bao gồm các ví dụ: Basic responsive, Center mode, Rich content (icon + badge), Drag & Drop, và tích hợp Modal V2 để cấu hình danh sách tab động.

Unit Tests

npx nx test components-tabs

Chạy test cho file cụ thể:

npx nx test components-tabs --testFile=libs-ui/components/tabs/src/tabs.component.spec.ts