# @libs-ui/components-buttons-sort

> Component nút sort asc/desc gọn nhẹ, hỗ trợ two-way binding, disable từng chiều và emit payload `ISort` kèm hàm `reset()`.

## Giới thiệu

Package `@libs-ui/components-buttons-sort` cung cấp 2 component sắp xếp dữ liệu cho bảng và danh sách. `LibsUiComponentsButtonsSortComponent` hiển thị 2 nút riêng biệt cho chiều tăng dần (asc) và giảm dần (desc). `LibsUiComponentsButtonsSortArrowComponent` là kiểu toggle 1 nút mũi tên chuyển đổi giữa asc và desc, tích hợp thêm popover mô tả trạng thái hiện tại.

## Tính năng

- ✅ Hai kiểu UI: **Sort** (2 nút riêng) và **Arrow** (1 nút toggle)
- ✅ Two-way binding qua `[(mode)]` — đồng bộ trạng thái với component cha
- ✅ Emit payload `(outChange)` kiểu `ISort` kèm hàm `reset()` để xóa sort
- ✅ Disable toàn bộ (`[disable]`) hoặc disable từng chiều (`[disableAsc]`, `[disableDesc]`)
- ✅ `[onlyEmit]` — luôn emit event kể cả khi click vào chiều đang active
- ✅ Arrow hỗ trợ `[defaultMode]` — chiều sort mặc định khi click lần đầu
- ✅ Arrow hỗ trợ popover mô tả asc/desc (`[popoverContentAsc]`, `[popoverContentDesc]`)
- ✅ Tùy chỉnh kích thước icon qua `[size]`
- ✅ Standalone component + OnPush Change Detection + Angular Signals

## Khi nào sử dụng

- Khi cần UI sort asc/desc cho cột trong bảng dữ liệu
- Khi cần binding trạng thái sort qua `[(mode)]` để đồng bộ với state bên ngoài
- Khi cần disable toàn bộ sort hoặc disable riêng từng chiều (chỉ cho phép sắp xếp 1 chiều)
- Khi muốn sort button luôn emit event mà không bị chặn khi click lại chiều đang active (`onlyEmit`)
- Khi cần kiểu toggle 1 nút (Arrow) với popover giải thích ý nghĩa asc/desc cho người dùng

## Cài đặt

```bash
npm install @libs-ui/components-buttons-sort
```

## Import

```typescript
import {
  LibsUiComponentsButtonsSortComponent,
  LibsUiComponentsButtonsSortArrowComponent,
  ISort,
  TYPE_SORT_TYPE,
} from '@libs-ui/components-buttons-sort';
```

## Ví dụ sử dụng

### 1. Sort cơ bản — 2 nút asc/desc

`example.component.html`

```html
<libs_ui-components-buttons-sort
  [fieldSort]="'name'"
  [(mode)]="sortMode"
  (outChange)="handlerSortChange($event)"
/>
```

`example.component.ts`

```typescript
import { Component, signal } from '@angular/core';
import {
  LibsUiComponentsButtonsSortComponent,
  ISort,
  TYPE_SORT_TYPE,
} from '@libs-ui/components-buttons-sort';

@Component({
  selector: 'app-example',
  standalone: true,
  imports: [LibsUiComponentsButtonsSortComponent],
  templateUrl: './example.component.html',
})
export class ExampleComponent {
  readonly sortMode = signal<TYPE_SORT_TYPE>('');

  handlerSortChange(sort: ISort): void {
    console.log(sort.mode, sort.fieldSort, sort.modeNumber);
    // Gọi sort.reset() để đưa mode về '' khi cần xóa sort
  }
}
```

### 2. Sort với custom size và disable chiều asc

`example.component.html`

```html
<libs_ui-components-buttons-sort
  [size]="14"
  [disableAsc]="true"
  [fieldSort]="'createdAt'"
  [(mode)]="sortMode"
  (outChange)="handlerSortChange($event)"
/>
```

`example.component.ts`

```typescript
import { Component, signal } from '@angular/core';
import {
  LibsUiComponentsButtonsSortComponent,
  ISort,
  TYPE_SORT_TYPE,
} from '@libs-ui/components-buttons-sort';

@Component({
  selector: 'app-example-size',
  standalone: true,
  imports: [LibsUiComponentsButtonsSortComponent],
  templateUrl: './example.component.html',
})
export class ExampleSizeComponent {
  readonly sortMode = signal<TYPE_SORT_TYPE>('');

  handlerSortChange(sort: ISort): void {
    console.log('sort changed', sort);
  }
}
```

### 3. Arrow toggle — 1 nút với popover

`example.component.html`

```html
<libs_ui-components-buttons-sort-arrow
  [fieldSort]="'name'"
  [ignorePopoverContent]="false"
  [popoverContentAsc]="'Sắp xếp tăng dần (A → Z)'"
  [popoverContentDesc]="'Sắp xếp giảm dần (Z → A)'"
  [defaultMode]="'asc'"
  [(mode)]="arrowMode"
  (outChange)="handlerArrowChange($event)"
/>
```

`example.component.ts`

```typescript
import { Component, signal } from '@angular/core';
import {
  LibsUiComponentsButtonsSortArrowComponent,
  ISort,
  TYPE_SORT_TYPE,
} from '@libs-ui/components-buttons-sort';

@Component({
  selector: 'app-example-arrow',
  standalone: true,
  imports: [LibsUiComponentsButtonsSortArrowComponent],
  templateUrl: './example.component.html',
})
export class ExampleArrowComponent {
  readonly arrowMode = signal<TYPE_SORT_TYPE>('');

  handlerArrowChange(sort: ISort): void {
    console.log('arrow sort changed', sort.mode, sort.fieldSort);
  }
}
```

### 4. Sort disable toàn bộ

```html
<libs_ui-components-buttons-sort
  [disable]="true"
  [fieldSort]="'status'"
  [(mode)]="sortMode"
/>
```

### 5. Sort với onlyEmit — luôn emit dù click lại chiều đang active

```html
<libs_ui-components-buttons-sort
  [onlyEmit]="true"
  [fieldSort]="'name'"
  [(mode)]="sortMode"
  (outChange)="handlerSortChange($event)"
/>
```

## @Input() — `libs_ui-components-buttons-sort`

| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| `[disable]` | `boolean` | `false` | Disable toàn bộ sort button, không thể click cả 2 chiều | `[disable]="true"` |
| `[disableAsc]` | `boolean` | `false` | Disable riêng chiều tăng dần (asc) | `[disableAsc]="true"` |
| `[disableDesc]` | `boolean` | `false` | Disable riêng chiều giảm dần (desc) | `[disableDesc]="true"` |
| `[fieldSort]` | `string` | `''` | Tên field đang sort — được trả về nguyên vẹn trong payload `ISort.fieldSort` | `[fieldSort]="'name'"` |
| `[(mode)]` | `TYPE_SORT_TYPE` | `''` | Two-way binding mode sort hiện tại: `'asc'` / `'desc'` / `''` (không sort) | `[(mode)]="sortMode"` |
| `[onlyEmit]` | `boolean` | `false` | Cấu hình dự kiến: khi `true` sẽ vẫn emit event kể cả khi click lại chiều đang active. **Lưu ý:** ở bản hiện tại, component luôn emit dù `onlyEmit` là `true` hay `false` (xem mục Lưu ý quan trọng) | `[onlyEmit]="true"` |
| `[size]` | `number` | `10` | Kích thước icon sort theo đơn vị px | `[size]="14"` |

## @Output() — `libs_ui-components-buttons-sort`

| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| `(outChange)` | `ISort` | Emit khi người dùng click 1 trong 2 nút sort, trả về object `ISort` gồm mode, modeNumber, fieldSort và hàm reset() | `handlerSortChange(sort: ISort): void { sort.reset; console.log(sort); }` | `(outChange)="handlerSortChange($event)"` |

## @Input() — `libs_ui-components-buttons-sort-arrow` (Sub-component)

| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| `[defaultMode]` | `TYPE_SORT_TYPE` | `''` | Mode sort mặc định khi người dùng click lần đầu (khi mode hiện tại đang rỗng) | `[defaultMode]="'asc'"` |
| `[disable]` | `boolean` | `false` | Disable sort arrow button, không thể click | `[disable]="true"` |
| `[fieldSort]` | `string` | `''` | Tên field đang sort — được trả về nguyên vẹn trong payload `ISort.fieldSort` | `[fieldSort]="'name'"` |
| `[ignorePopoverContent]` | `boolean` | `true` | Khi `false`: hiển thị popover khi hover, cần truyền `popoverContentAsc`/`popoverContentDesc` | `[ignorePopoverContent]="false"` |
| `[(mode)]` | `TYPE_SORT_TYPE` | `''` | Two-way binding mode sort hiện tại: `'asc'` / `'desc'` / `''` | `[(mode)]="arrowMode"` |
| `[popoverContentAsc]` | `string \| undefined` | `undefined` | Nội dung text hiển thị trong popover khi mode đang là `asc` | `[popoverContentAsc]="'Sắp xếp A → Z'"` |
| `[popoverContentDesc]` | `string \| undefined` | `undefined` | Nội dung text hiển thị trong popover khi mode đang là `desc` | `[popoverContentDesc]="'Sắp xếp Z → A'"` |
| `[size]` | `number` | `16` | Kích thước icon mũi tên theo đơn vị px | `[size]="20"` |
| `[zIndex]` | `number` | `10` | z-index của popover overlay | `[zIndex]="100"` |

## @Output() — `libs_ui-components-buttons-sort-arrow` (Sub-component)

| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| `(outChange)` | `ISort` | Emit khi người dùng click nút arrow, trả về object `ISort` gồm mode, modeNumber, fieldSort và hàm reset() | `handlerArrowChange(sort: ISort): void { console.log(sort.mode); }` | `(outChange)="handlerArrowChange($event)"` |

## Types & Interfaces

```typescript
import { ISort, TYPE_SORT_TYPE } from '@libs-ui/components-buttons-sort';

// Kiểu mode sort
export type TYPE_SORT_TYPE = 'asc' | 'desc' | '';

// Payload emit ra khi sort thay đổi
export interface ISort {
  mode: TYPE_SORT_TYPE;       // Chiều sort hiện tại: 'asc' | 'desc' | ''
  modeNumber: 1 | 2;          // 1 = asc, 2 = desc
  fieldSort: string;          // Tên field đang sort (từ input [fieldSort])
  reset: () => void;          // Gọi để reset mode về '' (xóa sort)
}
```

### Mô tả

- **`TYPE_SORT_TYPE`**: Union type cho trạng thái sort. Giá trị `''` (chuỗi rỗng) biểu thị không có sort nào đang active.
- **`ISort`**: Object được emit qua `(outChange)`. Trường `modeNumber` tiện dụng khi gửi tham số số lên API (`1` = asc, `2` = desc). Hàm `reset()` đưa mode về `''` — có thể gọi trực tiếp từ payload: `sort.reset()`.

## Lưu ý quan trọng

⚠️ **Two-way binding với signal**: Khi dùng `[(mode)]` cùng Angular Signal, phải truyền giá trị signal đã gọi `()`: `[(mode)]="sortMode()"` trong inline template hoặc khai báo biến `readonly sortMode = signal<TYPE_SORT_TYPE>('')` và dùng `[(mode)]="sortMode"` trong template file riêng — Angular sẽ tự xử lý.

⚠️ **`onlyEmit` hiện luôn có hiệu lực như `true`**: Do cách kiểm tra điều kiện nội bộ, ở bản hiện tại `libs_ui-components-buttons-sort` luôn emit `(outChange)` khi click — kể cả khi click lại đúng chiều đang active và `[onlyEmit]` không được truyền (mặc định `false`). Input `[onlyEmit]` hiện chưa tạo ra khác biệt hành vi. Logic `disableAsc`/`disableDesc` vẫn hoạt động độc lập — click vào chiều bị disable sẽ không emit.

⚠️ **Arrow popover cần `ignorePopoverContent="false"`**: Mặc định `ignorePopoverContent` là `true` (bỏ qua popover). Phải đặt `[ignorePopoverContent]="false"` mới kích hoạt popover — đồng thời cần truyền `popoverContentAsc` hoặc `popoverContentDesc` để có nội dung hiển thị.

⚠️ **`reset()` trong `ISort`**: Hàm `reset()` được bind vào instance component — gọi `sort.reset()` sẽ đặt lại `mode` về `''` trực tiếp trên component gốc. Dùng khi cần reset sort từ bên ngoài (vd: clear filter).
