@libs-ui/components-buttons-group 0.2.356-41 → 0.2.356-43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,38 +1,33 @@
1
1
  # @libs-ui/components-buttons-group
2
2
 
3
- > Component Button Group cho phép nhóm nhiều button thành một nhóm liền kề với trạng thái active.
3
+ > Component nhóm nhiều button thành một group liền kề với quản lý trạng thái active tự động.
4
4
 
5
5
  ## Giới thiệu
6
6
 
7
- `LibsUiComponentsButtonsGroupComponent` là một standalone Angular component được thiết kế để hiển thị một nhóm các button liền kề nhau. Component hỗ trợ quản lý trạng thái active cho phép chuyển đổi giữa các button trong nhóm.
7
+ `LibsUiComponentsButtonsGroupComponent` là standalone Angular component cho phép nhóm nhiều button thành một dải liền kề. Component tự động xử lý trạng thái active/inactive theo index, hỗ trợ two-way binding, disable theo group hoặc từng button riêng lẻ, và tích hợp popover cho từng button trong group.
8
8
 
9
- ### Tính năng
9
+ ## Tính năng
10
10
 
11
- - ✅ Nhóm nhiều button thành một group liền kề
12
- - ✅ Quản lý trạng thái active với visual feedback rõ ràng
13
- - ✅ Two-way binding cho index button đang active
14
- - ✅ Hỗ trợ disable toàn bộ group hoặc từng button
15
- - ✅ Tích hợp popover cho từng button
16
- - ✅ Hỗ trợ icon cho button
17
- - ✅ Tự động xử border radius cho button đầu/cuối/giữa
18
- - ✅ Angular Signals cho tính phản hồi cao
19
- - ✅ OnPush Change Detection tối ưu hiệu năng
11
+ - ✅ Nhóm nhiều button thành một dải liền kề với border styling tự động
12
+ - ✅ Quản lý trạng thái active/inactive theo index với visual feedback rõ ràng
13
+ - ✅ Two-way binding `[(indexActive)]` cho index button đang active
14
+ - ✅ Disable toàn bộ group hoặc từng button riêng lẻ
15
+ - ✅ Hỗ trợ icon trái/phải và chế độ icon-only cho từng button
16
+ - ✅ Tích hợp popover cho từng button trong group
17
+ - ✅ Click vào button đang active không emit event (tránh re-render không cần thiết)
18
+ - ✅ Angular Signals + OnPush Change Detection tối ưu hiệu năng
20
19
 
21
20
  ## Khi nào sử dụng
22
21
 
23
- - Khi cần nhóm các button liên quan với nhau
24
- - Khi cần toggle giữa các options (như tab buttons)
25
- - Khi cần hiển thị một nhóm actions liền kề
26
- - Phù hợp cho filter groups, view switchers, segmented controls
22
+ - Khi cần nhóm các button liên quan thành filter group (All / Active / Completed)
23
+ - Khi cần view switcher (List View / Grid View / Card View)
24
+ - Khi cần segmented control hoặc tab-style buttons
25
+ - Khi cần toolbar formatting (Bold / Italic / Underline) dạng icon-only
27
26
 
28
27
  ## Cài đặt
29
28
 
30
29
  ```bash
31
- # npm
32
30
  npm install @libs-ui/components-buttons-group
33
-
34
- # yarn
35
- yarn add @libs-ui/components-buttons-group
36
31
  ```
37
32
 
38
33
  ## Import
@@ -40,6 +35,7 @@ yarn add @libs-ui/components-buttons-group
40
35
  ```typescript
41
36
  import { LibsUiComponentsButtonsGroupComponent } from '@libs-ui/components-buttons-group';
42
37
  import { IButton } from '@libs-ui/components-buttons-button';
38
+ import { IPopoverFunctionControlEvent } from '@libs-ui/components-popover';
43
39
 
44
40
  @Component({
45
41
  standalone: true,
@@ -49,28 +45,81 @@ import { IButton } from '@libs-ui/components-buttons-button';
49
45
  export class YourComponent {}
50
46
  ```
51
47
 
52
- ## Ví dụ
48
+ ## Ví dụ sử dụng
53
49
 
54
- ### Basic Usage
50
+ ### Basic — View Switcher
55
51
 
56
52
  ```html
57
53
  <libs_ui-components-buttons-group
58
54
  [buttons]="viewButtons"
59
- [(indexActive)]="activeIndex"
60
- (outChange)="handleChange($event)" />
55
+ [(indexActive)]="activeView"
56
+ (outChange)="handlerViewChange($event)"
57
+ />
61
58
  ```
62
59
 
63
60
  ```typescript
64
- viewButtons: IButton[] = [
65
- { label: 'List View', key: 'list' },
66
- { label: 'Grid View', key: 'grid' },
67
- { label: 'Card View', key: 'card' }
68
- ];
61
+ import { Component, signal } from '@angular/core';
62
+ import { LibsUiComponentsButtonsGroupComponent } from '@libs-ui/components-buttons-group';
63
+ import { IButton } from '@libs-ui/components-buttons-button';
64
+
65
+ @Component({
66
+ selector: 'app-view-switcher',
67
+ standalone: true,
68
+ imports: [LibsUiComponentsButtonsGroupComponent],
69
+ templateUrl: './view-switcher.component.html',
70
+ })
71
+ export class ViewSwitcherComponent {
72
+ readonly activeView = signal<number>(0);
73
+
74
+ readonly viewButtons: IButton[] = [
75
+ { label: 'List View', key: 'list' },
76
+ { label: 'Grid View', key: 'grid' },
77
+ { label: 'Card View', key: 'card' },
78
+ ];
79
+
80
+ protected handlerViewChange(event: Event): void {
81
+ event.stopPropagation();
82
+ // activeView đã được cập nhật tự động qua two-way binding
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### Filter Group
88
+
89
+ ```html
90
+ <libs_ui-components-buttons-group
91
+ [buttons]="filterButtons"
92
+ [(indexActive)]="activeFilter"
93
+ (outChange)="handlerFilterChange($event)"
94
+ />
95
+ <p>Đang lọc: {{ filterButtons[activeFilter()].key }}</p>
96
+ ```
69
97
 
70
- activeIndex = 0;
98
+ ```typescript
99
+ import { Component, signal } from '@angular/core';
100
+ import { LibsUiComponentsButtonsGroupComponent } from '@libs-ui/components-buttons-group';
101
+ import { IButton } from '@libs-ui/components-buttons-button';
71
102
 
72
- handleChange(button: IButton) {
73
- console.log('Selected:', button);
103
+ @Component({
104
+ selector: 'app-filter-group',
105
+ standalone: true,
106
+ imports: [LibsUiComponentsButtonsGroupComponent],
107
+ templateUrl: './filter-group.component.html',
108
+ })
109
+ export class FilterGroupComponent {
110
+ readonly activeFilter = signal<number>(0);
111
+
112
+ readonly filterButtons: IButton[] = [
113
+ { label: 'All', key: 'all' },
114
+ { label: 'Active', key: 'active' },
115
+ { label: 'Completed', key: 'completed' },
116
+ { label: 'Archived', key: 'archived' },
117
+ ];
118
+
119
+ protected handlerFilterChange(button: IButton): void {
120
+ button.key; // 'all' | 'active' | 'completed' | 'archived'
121
+ // Thực hiện logic lọc dữ liệu theo button.key
122
+ }
74
123
  }
75
124
  ```
76
125
 
@@ -79,79 +128,59 @@ handleChange(button: IButton) {
79
128
  ```html
80
129
  <libs_ui-components-buttons-group
81
130
  [buttons]="viewButtonsWithIcons"
82
- [(indexActive)]="activeView" />
131
+ [(indexActive)]="activeView"
132
+ />
83
133
  ```
84
134
 
85
135
  ```typescript
86
- viewButtonsWithIcons: IButton[] = [
87
- {
88
- label: 'List',
89
- key: 'list',
90
- classIconLeft: 'libs-ui-icon-list'
91
- },
92
- {
93
- label: 'Grid',
94
- key: 'grid',
95
- classIconLeft: 'libs-ui-icon-grid'
96
- },
97
- {
98
- label: 'Card',
99
- key: 'card',
100
- classIconLeft: 'libs-ui-icon-card'
101
- }
136
+ readonly viewButtonsWithIcons: IButton[] = [
137
+ { label: 'List', key: 'list', classIconLeft: 'libs-ui-icon-list' },
138
+ { label: 'Grid', key: 'grid', classIconLeft: 'libs-ui-icon-grid' },
139
+ { label: 'Card', key: 'card', classIconLeft: 'libs-ui-icon-card' },
102
140
  ];
103
-
104
- activeView = 0;
105
141
  ```
106
142
 
107
- ### Icon Only Buttons
143
+ ### Icon Only (Toolbar)
108
144
 
109
145
  ```html
110
146
  <libs_ui-components-buttons-group
111
- [buttons]="iconOnlyButtons"
112
- [(indexActive)]="selectedIndex" />
147
+ [buttons]="formatButtons"
148
+ [(indexActive)]="activeFormat"
149
+ />
113
150
  ```
114
151
 
115
152
  ```typescript
116
- iconOnlyButtons: IButton[] = [
117
- {
118
- key: 'bold',
119
- classIconLeft: 'libs-ui-icon-bold',
120
- iconOnlyType: true
121
- },
122
- {
123
- key: 'italic',
124
- classIconLeft: 'libs-ui-icon-italic',
125
- iconOnlyType: true
126
- },
127
- {
128
- key: 'underline',
129
- classIconLeft: 'libs-ui-icon-underline',
130
- iconOnlyType: true
131
- }
153
+ readonly formatButtons: IButton[] = [
154
+ { key: 'bold', classIconLeft: 'libs-ui-icon-bold', iconOnlyType: true },
155
+ { key: 'italic', classIconLeft: 'libs-ui-icon-italic', iconOnlyType: true },
156
+ { key: 'underline', classIconLeft: 'libs-ui-icon-underline', iconOnlyType: true },
132
157
  ];
133
158
  ```
134
159
 
135
- ### With Disabled State
160
+ ### Disable Toàn bộ group
136
161
 
137
162
  ```html
138
- <!-- Disable entire group -->
139
163
  <libs_ui-components-buttons-group
140
- [buttons]="buttons"
164
+ [buttons]="viewButtons"
141
165
  [disable]="true"
142
- [(indexActive)]="activeIndex" />
166
+ [(indexActive)]="activeView"
167
+ />
168
+ ```
143
169
 
144
- <!-- Disable specific buttons -->
170
+ ### Disable Từng button riêng lẻ
171
+
172
+ ```html
145
173
  <libs_ui-components-buttons-group
146
174
  [buttons]="buttonsWithDisabled"
147
- [(indexActive)]="activeIndex" />
175
+ [(indexActive)]="activeIndex"
176
+ />
148
177
  ```
149
178
 
150
179
  ```typescript
151
- buttonsWithDisabled: IButton[] = [
180
+ readonly buttonsWithDisabled: IButton[] = [
152
181
  { label: 'Option 1', key: '1' },
153
182
  { label: 'Option 2', key: '2', disable: true },
154
- { label: 'Option 3', key: '3' }
183
+ { label: 'Option 3', key: '3' },
155
184
  ];
156
185
  ```
157
186
 
@@ -161,174 +190,85 @@ buttonsWithDisabled: IButton[] = [
161
190
  <libs_ui-components-buttons-group
162
191
  [buttons]="buttonsWithPopover"
163
192
  [(indexActive)]="activeIndex"
164
- (outFunctionsControl)="handlePopoverControl($event)" />
193
+ (outFunctionsControl)="handlerPopoverControl($event)"
194
+ />
165
195
  ```
166
196
 
167
197
  ```typescript
168
- buttonsWithPopover: IButton[] = [
198
+ import { IPopoverFunctionControlEvent } from '@libs-ui/components-popover';
199
+
200
+ readonly buttonsWithPopover: IButton[] = [
169
201
  {
170
202
  label: 'Info',
171
203
  key: 'info',
172
- popover: {
173
- content: 'This is information button',
174
- mode: 'hover'
175
- }
204
+ popover: { content: 'Thông tin chi tiết về chức năng này', mode: 'hover' },
176
205
  },
177
206
  {
178
207
  label: 'Warning',
179
208
  key: 'warning',
180
- popover: {
181
- content: 'This is warning button',
182
- mode: 'hover'
183
- }
184
- }
209
+ popover: { content: 'Cảnh báo: thao tác này không thể hoàn tác', mode: 'hover' },
210
+ },
185
211
  ];
186
212
 
187
- handlePopoverControl(control: IPopoverFunctionControlEvent) {
188
- console.log('Popover control:', control);
213
+ protected handlerPopoverControl(control: IPopoverFunctionControlEvent): void {
214
+ // Lưu lại control để thao tác popover thủ công nếu cần
215
+ // control.showPopover(), control.removePopoverOverlay(), ...
189
216
  }
190
217
  ```
191
218
 
192
- ### Filter Group Example
193
-
194
- ```html
195
- <libs_ui-components-buttons-group
196
- [buttons]="filterButtons"
197
- [(indexActive)]="activeFilter"
198
- (outChange)="applyFilter($event)" />
199
- ```
200
-
201
- ```typescript
202
- filterButtons: IButton[] = [
203
- { label: 'All', key: 'all' },
204
- { label: 'Active', key: 'active' },
205
- { label: 'Completed', key: 'completed' },
206
- { label: 'Archived', key: 'archived' }
207
- ];
219
+ ## @Input()
208
220
 
209
- activeFilter = 0;
221
+ | Input | Type | Default | Mô tả | Ví dụ |
222
+ |---|---|---|---|---|
223
+ | `[buttons]` | `Array<IButton>` | **required** | Danh sách các button trong group. Không được để rỗng. | `[buttons]="viewButtons"` |
224
+ | `[disable]` | `boolean` | `false` | Disable toàn bộ button group. Từng button có thể disable riêng qua `IButton.disable`. | `[disable]="isLoading"` |
225
+ | `[(indexActive)]` | `number` | `0` | Index (0-based) của button đang active. Hỗ trợ two-way binding để đồng bộ state với component cha. | `[(indexActive)]="activeView"` |
210
226
 
211
- applyFilter(button: IButton) {
212
- // Apply filter based on button.key
213
- console.log('Filtering by:', button.key);
214
- }
215
- ```
227
+ ## @Output()
216
228
 
217
- ### Custom Styling
229
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
230
+ |---|---|---|---|---|
231
+ | `(outChange)` | `IButton` | Emit button vừa được chọn. Không emit khi click lại button đang active. | `handlerChange(button: IButton): void { button.stopPropagation?.(); /* xử lý */ }` | `(outChange)="handlerChange($event)"` |
232
+ | `(outFunctionsControl)` | `IPopoverFunctionControlEvent` | Emit API điều khiển popover khi popover của một button trong group được khởi tạo. | `handlerPopoverControl(control: IPopoverFunctionControlEvent): void { control; }` | `(outFunctionsControl)="handlerPopoverControl($event)"` |
218
233
 
219
- ```html
220
- <libs_ui-components-buttons-group
221
- [buttons]="styledButtons"
222
- [(indexActive)]="activeIndex" />
223
- ```
224
-
225
- ```typescript
226
- styledButtons: IButton[] = [
227
- {
228
- label: 'Primary',
229
- key: '1',
230
- classInclude: 'custom-button-class'
231
- },
232
- {
233
- label: 'Secondary',
234
- key: '2',
235
- classInclude: 'custom-button-class'
236
- }
237
- ];
238
- ```
239
-
240
- ## API
241
-
242
- ### libs_ui-components-buttons-group
243
-
244
- #### Inputs
245
-
246
- | Property | Type | Default | Description |
247
- | ----------------- | ---------------- | -------- | ---------------------------------------------- |
248
- | `[buttons]` | `Array<IButton>` | required | Danh sách các button trong group |
249
- | `[(indexActive)]` | `number` | `0` | Index của button đang active (two-way binding) |
250
- | `[disable]` | `boolean` | `false` | Disable toàn bộ button group |
251
-
252
- #### Outputs
253
-
254
- | Property | Type | Description |
255
- | ----------------------- | ------------------------------ | --------------------------------- |
256
- | `(outChange)` | `IButton` | Emit khi chuyển đổi button active |
257
- | `(outFunctionsControl)` | `IPopoverFunctionControlEvent` | Emit functions điều khiển popover |
258
-
259
- #### Public Methods
260
-
261
- | Method | Description |
262
- | ------------------ | --------------------------------------- |
263
- | `FunctionsControl` | Getter để lấy popover control functions |
234
+ > **Lưu ý `outChange`:** Event này không emit khi click vào button đang active (index không thay đổi). Handler không nhận `Event` DOM nên không cần `stopPropagation()` trong handler này.
264
235
 
265
236
  ## Types & Interfaces
266
237
 
267
- ### IButton Interface
238
+ ### IButton
268
239
 
269
- Button group sử dụng interface `IButton` từ `@libs-ui/components-buttons-button`:
240
+ Import từ `@libs-ui/components-buttons-button`:
270
241
 
271
242
  ```typescript
272
- export interface IButton {
273
- /** Unique key cho button */
274
- key?: string;
275
-
276
- /** Kiểu button (sẽ được override bởi component) */
277
- type?: TYPE_BUTTON;
278
-
279
- /** Kích thước button */
280
- sizeButton?: TYPE_SIZE_BUTTON;
281
-
282
- /** Chỉ hiển thị icon, ẩn label */
283
- iconOnlyType?: boolean;
284
-
285
- /** Label hiển thị trên button */
286
- label?: string;
287
-
288
- /** Disable button này */
289
- disable?: boolean;
290
-
291
- /** Class CSS bổ sung */
292
- classInclude?: string;
293
-
294
- /** Class icon bên trái */
295
- classIconLeft?: string;
296
-
297
- /** Class icon bên phải */
298
- classIconRight?: string;
299
-
300
- /** Class cho label */
301
- classLabel?: string;
302
-
303
- /** Cấu hình popover */
304
- popover?: IPopover;
305
-
306
- /** Không stop propagation event */
307
- ignoreStopPropagationEvent?: boolean;
308
-
309
- /** Z-index cho button */
310
- zIndex?: number;
311
-
312
- /** Trạng thái pending */
313
- isPending?: boolean;
314
-
315
- /** Action callback */
316
- action?: (data?: any) => Promise<void>;
317
-
318
- /** Style cho icon trái */
319
- styleIconLeft?: Record<string, any>;
320
-
321
- /** Style cho button */
322
- styleButton?: Record<string, any>;
243
+ import { IButton } from '@libs-ui/components-buttons-button';
323
244
 
324
- /** Custom color (khi type là button-custom) */
325
- buttonCustom?: IColorButton;
245
+ export interface IButton {
246
+ key?: string; // Định danh duy nhất cho button
247
+ type?: TYPE_BUTTON; // Bị override bởi component (xem Lưu ý quan trọng)
248
+ sizeButton?: TYPE_SIZE_BUTTON; // Kích thước button
249
+ iconOnlyType?: boolean; // Chỉ hiển thị icon, ẩn label
250
+ label?: string; // Nội dung text hiển thị
251
+ disable?: boolean; // Disable riêng button này
252
+ classInclude?: string; // Class CSS bổ sung cho button
253
+ classIconLeft?: string; // Class icon bên trái label
254
+ classIconRight?: string; // Class icon bên phải label
255
+ classLabel?: string; // Class CSS bổ sung cho label
256
+ popover?: IPopover; // Cấu hình popover của button
257
+ ignoreStopPropagationEvent?: boolean; // Không dừng propagation event
258
+ zIndex?: number; // z-index của button
259
+ isPending?: boolean; // Trạng thái loading/pending
260
+ action?: (data?: any) => Promise<void>; // Callback async tuỳ chỉnh
261
+ styleIconLeft?: Record<string, any>; // Inline style cho icon trái
262
+ styleButton?: Record<string, any>; // Inline style cho button
263
+ buttonCustom?: IColorButton; // Màu tuỳ chỉnh (khi type = 'button-custom')
326
264
  }
327
265
  ```
328
266
 
329
267
  ### TYPE_BUTTON
330
268
 
331
269
  ```typescript
270
+ import { TYPE_BUTTON } from '@libs-ui/components-buttons-button';
271
+
332
272
  export type TYPE_BUTTON =
333
273
  | 'button-primary'
334
274
  | 'button-primary-revert'
@@ -359,51 +299,37 @@ export type TYPE_BUTTON =
359
299
  ### TYPE_SIZE_BUTTON
360
300
 
361
301
  ```typescript
302
+ import { TYPE_SIZE_BUTTON } from '@libs-ui/components-buttons-button';
303
+
362
304
  export type TYPE_SIZE_BUTTON = 'large' | 'medium' | 'small' | 'smaller';
363
305
  ```
364
306
 
365
- ## Styling
366
-
367
- Component tự động xử lý styling cho button group:
368
-
369
- - **First button**: Border radius trái (top-left, bottom-left)
370
- - **Middle buttons**: Không có border radius, border-left bị loại bỏ
371
- - **Last button**: Border radius phải (top-right, bottom-right), border-left bị loại bỏ
372
- - **Active button**: Sử dụng `button-primary` type
373
- - **Inactive buttons**: Sử dụng `button-primary-revert` type
307
+ ### IPopoverFunctionControlEvent
374
308
 
375
- ### CSS Variables
309
+ Import từ `@libs-ui/components-popover` (chỉ cần khi xử lý `(outFunctionsControl)`):
376
310
 
377
- Component sử dụng CSS variable:
378
-
379
- ```scss
380
- --libs-ui-button-other-color-border: #226ff5; // Border color cho disabled state
311
+ ```typescript
312
+ import { IPopoverFunctionControlEvent } from '@libs-ui/components-popover';
313
+
314
+ export interface IPopoverFunctionControlEvent {
315
+ showPopover: (elementRef?: HTMLElement) => Promise<void>;
316
+ updatePopoverOverlayPosition: () => Promise<void>;
317
+ removePopoverOverlay: () => Promise<void>;
318
+ getRectContainer: () => Promise<IBoundingClientRect>;
319
+ getRectContent: () => Promise<IBoundingClientRect>;
320
+ updatePopoverOverlay: () => Promise<void>;
321
+ }
381
322
  ```
382
323
 
383
- ## Behavior
384
-
385
- ### Active State Management
386
-
387
- - Khi click vào một button, nó sẽ trở thành active
388
- - Click vào button đang active sẽ không có effect (không emit event)
389
- - Index active được quản lý qua two-way binding `[(indexActive)]`
390
- - Active button hiển thị với style `button-primary`
391
- - Inactive buttons hiển thị với style `button-primary-revert`
324
+ ## Lưu ý quan trọng
392
325
 
393
- ### Disable Behavior
326
+ ⚠️ **Type của button bị override tự động**: Component tự động đặt `type = 'button-primary'` cho button đang active và `type = 'button-primary-revert'` cho các button còn lại. Giá trị `type` trong `IButton.type` không có tác dụng khi dùng trong `buttons-group`.
394
327
 
395
- - Khi `[disable]="true"`: Toàn bộ group bị disable
396
- - Khi button cụ thể có `disable: true`: Chỉ button đó bị disable
397
- - Disabled buttons vẫn giữ border styling phù hợp với vị trí trong group
328
+ ⚠️ **Index-based active state**: Component quản lý trạng thái active theo index (vị trí trong mảng), không theo `key`. Nếu mảng `buttons` thay đổi thứ tự phần tử, `indexActive` có thể trỏ sai button. Đảm bảo thứ tự mảng ổn định khi muốn giữ nguyên trạng thái active.
398
329
 
399
- ## Công nghệ
330
+ ⚠️ **Click button đang active không emit event**: Khi người dùng click vào button có index trùng `indexActive()`, component bỏ qua và không emit `outChange`. Đây là behavior có chủ đích để tránh re-render không cần thiết.
400
331
 
401
- | Technology | Version | Purpose |
402
- | --------------- | ------- | ---------------- |
403
- | Angular | 18+ | Framework |
404
- | Angular Signals | - | State management |
405
- | SCSS | - | Styling |
406
- | OnPush | - | Change Detection |
332
+ ⚠️ **`outChange` không truyền DOM Event**: Handler của `(outChange)` nhận `IButton` trực tiếp, không phải `Event` DOM — không cần và không thể gọi `event.stopPropagation()` trong handler này.
407
333
 
408
334
  ## Demo
409
335
 
@@ -411,75 +337,4 @@ Component sử dụng CSS variable:
411
337
  npx nx serve core-ui
412
338
  ```
413
339
 
414
- Truy cập: `http://localhost:4500/buttons/group`
415
-
416
- ## Unit Tests
417
-
418
- ```bash
419
- # Chạy tests
420
- npx nx test components-buttons-group
421
-
422
- # Coverage
423
- npx nx test components-buttons-group --coverage
424
-
425
- # Watch mode
426
- npx nx test components-buttons-group --watch
427
- ```
428
-
429
- ## Dependencies
430
-
431
- - `@angular/core`: >=18.0.0
432
- - `@libs-ui/components-buttons-button`: 0.2.355-14
433
- - `@libs-ui/components-popover`: 0.2.355-14
434
- - `@ngx-translate/core`: ^15.0.0
435
-
436
- ## Important Notes
437
-
438
- ### ⚠️ Type Override
439
-
440
- Component tự động override `type` property của buttons:
441
-
442
- - Active button → `button-primary`
443
- - Inactive button → `button-primary-revert`
444
-
445
- Nếu bạn muốn custom type, cần modify component source code.
446
-
447
- ### ⚠️ Border Styling
448
-
449
- Component sử dụng `::ng-deep` để style nested components. Điều này cần thiết để style các button components bên trong nhưng có thể ảnh hưởng đến global styles nếu không cẩn thận.
450
-
451
- ### ⚠️ Index-based Active State
452
-
453
- Component sử dụng index thay vì key để quản lý active state. Đảm bảo array `buttons` không thay đổi thứ tự nếu bạn muốn maintain active state.
454
-
455
- ## Best Practices
456
-
457
- 1. **Sử dụng key duy nhất**: Luôn cung cấp `key` unique cho mỗi button để dễ identify
458
- 2. **Limit số lượng buttons**: Nên giới hạn 3-5 buttons trong một group để UX tốt
459
- 3. **Consistent labels**: Sử dụng label ngắn gọn, rõ ràng và có độ dài tương đương
460
- 4. **Icon consistency**: Nếu dùng icon, nên dùng cho tất cả buttons hoặc không dùng
461
- 5. **Responsive design**: Cân nhắc sử dụng icon-only mode trên mobile
462
-
463
- ## Troubleshooting
464
-
465
- ### Button không chuyển active state
466
-
467
- - Kiểm tra `[(indexActive)]` binding
468
- - Đảm bảo không có logic nào block event handler
469
- - Verify button không bị disable
470
-
471
- ### Border styling không đúng
472
-
473
- - Kiểm tra ViewEncapsulation của parent component
474
- - Verify CSS variables được define
475
- - Check xem có CSS conflicts không
476
-
477
- ### Popover không hoạt động
478
-
479
- - Đảm bảo đã import `@libs-ui/components-popover`
480
- - Verify popover config đúng format
481
- - Check z-index conflicts
482
-
483
- ## License
484
-
485
- MIT
340
+ Truy cập: http://localhost:4500/components/buttons/group
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, input, model, output, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { signal, input, model, output, Component, ChangeDetectionStrategy } from '@angular/core';
3
3
  import { LibsUiComponentsButtonsButtonComponent } from '@libs-ui/components-buttons-button';
4
4
  import { TranslateModule } from '@ngx-translate/core';
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"libs-ui-components-buttons-group.mjs","sources":["../../../../../../libs-ui/components/buttons/group/src/group.component.ts","../../../../../../libs-ui/components/buttons/group/src/group.component.html","../../../../../../libs-ui/components/buttons/group/src/libs-ui-components-buttons-group.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, input, model, output, signal } from '@angular/core';\nimport { IButton, LibsUiComponentsButtonsButtonComponent } from '@libs-ui/components-buttons-button';\nimport { IPopoverFunctionControlEvent } from '@libs-ui/components-popover';\nimport { TranslateModule } from '@ngx-translate/core';\n\n@Component({\n // eslint-disable-next-line @angular-eslint/component-selector\n selector: 'libs_ui-components-buttons-group',\n templateUrl: './group.component.html',\n styleUrl: './group.component.scss',\n standalone: true,\n imports: [TranslateModule, LibsUiComponentsButtonsButtonComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class LibsUiComponentsButtonsGroupComponent {\n private readonly functionsControlPopover = signal<IPopoverFunctionControlEvent | undefined>(undefined);\n\n // #region INPUT\n readonly buttons = input.required<Array<IButton>>();\n readonly indexActive = model<number>(0);\n readonly disable = input<boolean>(false);\n\n // #region OUTPUT\n readonly outChange = output<IButton>();\n readonly outFunctionsControl = output<IPopoverFunctionControlEvent>();\n\n /* FUNCTIONS */\n protected async handlerClickButton(index: number, button: IButton) {\n if (this.indexActive() === index) {\n return;\n }\n this.indexActive.set(index);\n this.outChange.emit(button);\n }\n\n protected async handlerFunctionsControl(control: IPopoverFunctionControlEvent) {\n this.outFunctionsControl.emit(control);\n this.functionsControlPopover.set(control);\n }\n\n public get FunctionsControl(): IPopoverFunctionControlEvent | undefined {\n return this.functionsControlPopover();\n }\n}\n","<div class=\"libs-ui-buttons-group\">\n @for (button of buttons(); track button) {\n <libs_ui-components-buttons-button\n [type]=\"indexActive() === $index ? 'button-primary' : 'button-primary-revert'\"\n [label]=\"button.label || ''\"\n [disable]=\"disable() || button.disable || false\"\n [classIconLeft]=\"button.classIconLeft || ''\"\n [classInclude]=\"button.classInclude || ''\"\n [classIconRight]=\"button.classIconRight || ''\"\n [popover]=\"button.popover || {}\"\n (outFunctionsControl)=\"handlerFunctionsControl($event)\"\n (outClick)=\"handlerClickButton($index, button)\" />\n }\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"libs-ui-components-buttons-group.mjs","sources":["../../../../../../libs-ui/components/buttons/group/src/group.component.ts","../../../../../../libs-ui/components/buttons/group/src/group.component.html","../../../../../../libs-ui/components/buttons/group/src/libs-ui-components-buttons-group.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, input, model, output, signal } from '@angular/core';\nimport { IButton, LibsUiComponentsButtonsButtonComponent } from '@libs-ui/components-buttons-button';\nimport { IPopoverFunctionControlEvent } from '@libs-ui/components-popover';\nimport { TranslateModule } from '@ngx-translate/core';\n\n@Component({\n // eslint-disable-next-line @angular-eslint/component-selector\n selector: 'libs_ui-components-buttons-group',\n templateUrl: './group.component.html',\n styleUrl: './group.component.scss',\n standalone: true,\n imports: [TranslateModule, LibsUiComponentsButtonsButtonComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class LibsUiComponentsButtonsGroupComponent {\n private readonly functionsControlPopover = signal<IPopoverFunctionControlEvent | undefined>(undefined);\n\n // #region INPUT\n readonly buttons = input.required<Array<IButton>>();\n readonly indexActive = model<number>(0);\n readonly disable = input<boolean>(false);\n\n // #region OUTPUT\n readonly outChange = output<IButton>();\n readonly outFunctionsControl = output<IPopoverFunctionControlEvent>();\n\n /* FUNCTIONS */\n protected async handlerClickButton(index: number, button: IButton) {\n if (this.indexActive() === index) {\n return;\n }\n this.indexActive.set(index);\n this.outChange.emit(button);\n }\n\n protected async handlerFunctionsControl(control: IPopoverFunctionControlEvent) {\n this.outFunctionsControl.emit(control);\n this.functionsControlPopover.set(control);\n }\n\n public get FunctionsControl(): IPopoverFunctionControlEvent | undefined {\n return this.functionsControlPopover();\n }\n}\n","<div class=\"libs-ui-buttons-group\">\n @for (button of buttons(); track button) {\n <libs_ui-components-buttons-button\n [type]=\"indexActive() === $index ? 'button-primary' : 'button-primary-revert'\"\n [label]=\"button.label || ''\"\n [disable]=\"disable() || button.disable || false\"\n [classIconLeft]=\"button.classIconLeft || ''\"\n [classInclude]=\"button.classInclude || ''\"\n [classIconRight]=\"button.classIconRight || ''\"\n [popover]=\"button.popover || {}\"\n (outFunctionsControl)=\"handlerFunctionsControl($event)\"\n (outClick)=\"handlerClickButton($index, button)\" />\n }\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@libs-ui/components-buttons-group",
3
- "version": "0.2.356-41",
3
+ "version": "0.2.356-43",
4
4
  "peerDependencies": {
5
5
  "@angular/core": ">=18.0.0",
6
- "@libs-ui/components-buttons-button": "0.2.356-41",
7
- "@libs-ui/components-popover": "0.2.356-41",
6
+ "@libs-ui/components-buttons-button": "0.2.356-43",
7
+ "@libs-ui/components-popover": "0.2.356-43",
8
8
  "@ngx-translate/core": "^15.0.0"
9
9
  },
10
10
  "sideEffects": false,