@aseansc-admin/ui
v2.10.0
Published
ASC Internal UI Component Library — Angular 20 + PrimeNG 20 + TailwindCSS 4
Maintainers
Readme
@aseansc-admin/ui
ASC Internal UI Component Library — Angular 20 · PrimeNG 20 · TailwindCSS 4
Migration — v0.5.x → v0.6.0
Breaking: Tailwind spacing scale — base unit đổi từ 0.25rem sang 1px. Tất cả numeric spacing class cần nhân ×4:
p-4 → p-16 gap-2 → gap-8 px-6 → px-24
w-12 → w-48 h-6 → h-24 mt-1 → mt-4Chạy lệnh migrate tự động trong project:
# Dùng Perl (Linux/macOS) — backup trước khi chạy
find src \( -name "*.ts" -o -name "*.html" \) | xargs perl -pi -e \
's/\b(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|w|h)-([1-9][0-9]*)\b/$1."-".($2*4)/ge'Hoặc xem CHANGELOG để biết chi tiết.
Installation
npm install @aseansc-admin/uiPeer dependencies
npm install @angular/core@^20 @angular/cdk@^20Setup
1. app.config.ts
import { ApplicationConfig, provideZoneChangeDetection,
provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideAscUI } from '@aseansc-admin/ui';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideAscUI({
theme: { darkMode: false },
locale: 'vi-VN', // 'vi-VN' | 'en-US'
table: { rows: 20 },
}),
provideAnimations(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(appRoutes),
provideHttpClient(),
],
};2. styles.scss
@import "@aseansc-admin/ui/preset.css";
@use 'prismjs/themes/prism-okaidia.css';Lưu ý: dòng import preset phải dùng cú pháp CSS @import (không phải Sass @use) —
package ship sẵn @source trỏ vào chính bundle của @asc/ui bên trong file này, để Tailwind
tự động nhận diện các class Tailwind dùng trong component của lib. Nếu dùng @use thay vì
@import, Sass sẽ inline/flatten nội dung trước khi tới Tailwind và @source sẽ không còn
resolve đúng — lúc đó bạn sẽ phải tự thêm @source '../node_modules/@aseansc-admin/ui';
thủ công trong app của mình.
3. postcss.config.json (workspace root)
{
"plugins": {
"@tailwindcss/postcss": {}
}
}4. Root component template
<asc-toast />
<asc-confirm-dialog />
<router-outlet />Components
Layout — AscLayout
import { AscLayout } from '@aseansc-admin/ui';<asc-layout [menuItems]="menuItems" [topBarLogo]="logoTpl" [breadCrumb]="breadcrumbTpl">
<router-outlet />
</asc-layout>
<ng-template #breadcrumbTpl>
<asc-breadcrumb />
</ng-template>Toggle dark mode via the sun/moon button in the topbar — adds/removes .app-dark on <html>.
Breadcrumb — AscBreadcrumb
import { AscBreadcrumb } from '@aseansc-admin/ui';Auto-router mode — khai báo data.breadcrumb trong route config, không cần truyền gì thêm:
// app.routes.ts
{
path: 'form',
data: { breadcrumb: 'Form' },
children: [
{ path: 'input', data: { breadcrumb: 'Input' }, loadComponent: ... },
],
}<asc-breadcrumb />Manual mode — truyền items thủ công:
<asc-breadcrumb
[items]="[{ label: 'Admin', routerLink: '/admin' }, { label: 'Users' }]"
[home]="{ icon: 'pi pi-home', routerLink: '/' }"
/>
<!-- Ẩn home icon -->
<asc-breadcrumb [items]="items" [home]="null" />Form
Auto Complete — AscAutoCompleteComponent
<!-- Single -->
<asc-form-field label="Nhân viên">
<asc-auto-complete
[formControl]="ctrl"
[suggestions]="results"
optionLabel="fullName"
placeholder="Tìm kiếm..."
(completeMethod)="onSearch($event)"
/>
</asc-form-field>
<!-- Multiple (chip mode) -->
<asc-auto-complete
[formControl]="ctrl"
[suggestions]="results"
optionLabel="name"
[multiple]="true"
(completeMethod)="onSearch($event)"
/>results = signal<Employee[]>([]);
onSearch(query: string) {
this.results.set(
this.employees.filter(e => e.fullName.toLowerCase().includes(query.toLowerCase()))
);
}| Input | Type | Default | Mô tả |
|-----------------|-----------|---------|----------------------------------------------------------|
| suggestions | any[] | [] | Danh sách gợi ý — cập nhật từ (completeMethod) |
| optionLabel | string | '' | Property hiển thị khi value là object |
| optionValue | string | '' | Property dùng làm value (mặc định: cả object) |
| minLength | number | 1 | Số ký tự tối thiểu trước khi tìm kiếm |
| delay | number | 300 | Delay (ms) trước khi emit |
| multiple | boolean | false | Chọn nhiều — hiển thị dạng chip |
| forceSelection| boolean | false | Bắt buộc chọn từ danh sách |
| dropdown | boolean | false | Hiển thị nút dropdown |
| showClear | boolean | false | Hiển thị nút xoá |
| Output | Mô tả |
|------------------|------------------------------------------|
| completeMethod | Emit text user đang gõ (string) |
Input Mask — AscInputMaskComponent
Input với định dạng cố định. Ký tự mask: 9=chữ số · a=chữ cái · *=alphanumeric · ?=optional từ ký tự đó.
<!-- Số điện thoại -->
<asc-form-field label="Số điện thoại">
<asc-input-mask [formControl]="ctrl" mask="0999 999 999" placeholder="0xxx xxx xxx" />
</asc-form-field>
<!-- Ngày tháng -->
<asc-input-mask [formControl]="ctrl" mask="99/99/9999" placeholder="dd/mm/yyyy" />
<!-- CCCD 12 số -->
<asc-input-mask [formControl]="ctrl" mask="999999999999" />
<!-- MST doanh nghiệp -->
<asc-input-mask [formControl]="ctrl" mask="9999999999-999" />
<!-- unmask=true → model lưu chuỗi thuần, không có ký tự phân cách -->
<asc-input-mask [formControl]="ctrl" mask="0999 999 999" [unmask]="true" />| Input | Type | Default | Mô tả |
|---------------|-----------|----------|-----------------------------------------------------------------------|
| mask | string | required | Pattern định dạng. |
| placeholder | string | '' | Placeholder khi chưa nhập. |
| slotChar | string | '_' | Ký tự đại diện cho slot trống. |
| autoClear | boolean | true | Tự xoá nếu blur mà chưa điền đủ mask. |
| showClear | boolean | false | Hiển thị nút ×. |
| readonly | boolean | false | Chỉ đọc. |
| unmask | boolean | false | true → model value là chuỗi thuần không có ký tự phân cách. |
| Output | Mô tả |
|------------|--------------------------------|
| complete | Emit khi user điền đủ mask. |
Button — AscButtonComponent
<asc-button label="Lưu" icon="pi pi-save" (clicked)="onSave()" />
<asc-button label="Huỷ" variant="outlined" severity="secondary" />
<asc-button label="Xoá" severity="danger" [loading]="loading" />Input — AscInputComponent
<asc-form-field label="Email">
<asc-input formControlName="email" placeholder="Nhập email..." />
</asc-form-field>Textarea — AscTextareaComponent
<asc-textarea formControlName="note" [rows]="4" [maxLength]="500" [showCounter]="true" />Input Number — AscInputNumberComponent
<asc-input-number formControlName="amount" mode="currency" currency="VND" />Password — AscPasswordComponent
<asc-password formControlName="password" [feedback]="true" />Select — AscSelectComponent
<asc-select formControlName="status" [options]="statusOptions" optionLabel="label" optionValue="value" />Datepicker — AscDatepickerComponent
<asc-datepicker formControlName="date" dateFormat="dd/MM/yyyy" />Checkbox — AscCheckboxComponent
<asc-checkbox formControlName="agree" label="Tôi đồng ý với điều khoản" />Radio — AscRadioComponent
<asc-radio formControlName="gender" [options]="genderOptions" optionLabel="label" optionValue="value" />Upload — AscUpload
<!-- Image (default) -->
<asc-upload formControlName="avatar" uploadMode="img" [maxSizePerFile]="2000000">
<div empty>Kéo thả ảnh vào đây</div>
</asc-upload>
<!-- Excel / CSV -->
<asc-upload formControlName="sheet" uploadMode="excelFile" />
<!-- Word / Text -->
<asc-upload formControlName="doc" uploadMode="docFile" />
<!-- PDF only -->
<asc-upload formControlName="report" uploadMode="pdfFile" />
<!-- All document types (.pdf .doc .docx .xls .xlsx .csv .txt) -->
<asc-upload formControlName="attachment" uploadMode="file" />
<!-- Any file -->
<asc-upload formControlName="misc" uploadMode="all" />| uploadMode | Accept |
|---------------|-----------------------------------------|
| img | image/* |
| pdfFile | .pdf |
| docFile | .doc, .docx, .txt |
| excelFile | .xls, .xlsx, .csv |
| file | .pdf, .doc, .docx, .xls, .xlsx, .csv, .txt |
| all | */* |
Data
Table — AscTableComponent
<asc-table [data]="rows" [columns]="cols" [loading]="loading" (lazyLoad)="onLoad($event)">
<!-- type='custom': dùng [ascTableCell]="key" để render tự do -->
<ng-template [ascTableCell]="'status'" let-value let-row="row">
<p-tag [value]="value" />
</ng-template>
</asc-table>Column types:
columns: AscTableColumn[] = [
{ field: 'name', header: 'Tên', type: 'avatar' },
{ field: 'email', header: 'Email', type: 'text' },
{ field: 'active', header: 'Hoạt động', type: 'boolean' },
{ field: 'joinDate', header: 'Ngày vào', type: 'date' },
{ field: 'lastSeen', header: 'Lần cuối', type: 'datetime' },
// Number — thousandSep + locale kiểm soát ký tự phân cách
{ field: 'score', header: 'Điểm', type: 'number',
format: { thousandSep: true } }, // 8.000 (vi-VN mặc định)
{ field: 'score', header: 'Điểm', type: 'number',
format: { thousandSep: true, locale: 'en-US' } }, // 8,000
{ field: 'score', header: 'Điểm', type: 'number',
format: { thousandSep: false } }, // 8000
// Currency
{ field: 'salary', header: 'Lương', type: 'currency',
format: { locale: 'vi-VN' } },
// Badge — map value → label + severity
{ field: 'status', header: 'Trạng thái', type: 'badge',
badgeMap: {
active: { label: 'Hoạt động', severity: 'success' },
inactive: { label: 'Tạm dừng', severity: 'warn' },
},
},
// Custom template
{ field: 'progress', header: 'Tiến độ', type: 'custom', templateKey: 'progress' },
];Lazy load — pageNumber/pageSize: khi [lazy]="true", (lazyLoad) emit AscTableLazyLoadEvent gồm first/rows (chuẩn PrimeNG) và pageNumber/pageSize (0-based, pageNumber = Math.floor(first / rows)) — dùng thẳng cho BE theo convention pageNumber/pageSize, khỏi phải tự tính lại ở service layer:
<asc-table [data]="rows" [columns]="cols" [lazy]="true" [totalRecords]="total" (lazyLoad)="onLoad($event)" />onLoad({ pageNumber, pageSize }: AscTableLazyLoadEvent) {
this.api.post({ authenType: 'getAllMeeting', data: { pageNumber, pageSize }, endpoint: 'feApi' })
.subscribe(res => { this.rows = res.data; this.total = res.total; });
}Icon — AscIconComponent
Render icon SVG inline (không phải <img>) bằng cách fetch qua HTTP từ app consume — cho phép style màu qua CSS currentColor và kích thước tuỳ ý. Khác với PrimeIcon (font class như pi pi-user), dùng khi cần icon SVG tuỳ chỉnh của riêng app.
<asc-icon src="/assets/icons/user.svg" size="24px" />
<asc-icon src="/assets/icons/star.svg" size="24px" (loadError)="onIconError($event)" />onIconError(err: unknown) {
console.warn('Không tải được icon', err);
}| Input | Type | Default | Mô tả |
|-------------|-----------|-----------|--------------------------------------------------------------------|
| src | string | required | URL tới file .svg — fetch qua HTTP. |
| size | string | '1em' | Kích thước CSS áp cho width/height (vd '24px', '1.5rem'). |
| Output | Mô tả |
|-------------|-------------------------------------------------|
| loadError | Emit khi fetch SVG lỗi (404, network...). |
SVG được fetch và cache theo src qua AscIconService (providedIn: 'root') — nhiều <asc-icon> cùng src chỉ gọi HTTP đúng 1 lần. File SVG nên dùng fill="currentColor"/stroke="currentColor" để kế thừa màu từ CSS color của phần tử cha.
Yêu cầu: app phải gọi provideHttpClient() (thường đã có sẵn nếu dùng @aseansc-admin/sea-http).
⚠️ Bảo mật: nội dung SVG được render qua DomSanitizer.bypassSecurityTrustHtml (bỏ qua sanitize) để giữ style/currentColor. Chỉ dùng src trỏ tới icon do chính app kiểm soát (vd thư mục assets nội bộ) — không truyền URL do user nhập, vì sẽ mở lỗ hổng XSS.
Best practice — đừng hardcode src rải rác khắp template. Gom hết đường dẫn icon vào 1 file const duy nhất, giống cách app thường tổ chức route path hay i18n key — lợi ích: đổi thư mục/CDN icon chỉ sửa 1 chỗ, gõ nhầm tên bị TypeScript bắt lỗi ngay, và IDE autocomplete được tên icon.
// src/app/shared/constants/app-icons.const.ts
const ICON_BASE = '/assets/icons';
export const AppIcons = {
user: `${ICON_BASE}/user.svg`,
star: `${ICON_BASE}/star.svg`,
bell: `${ICON_BASE}/bell.svg`,
} as const;
export type AppIconName = keyof typeof AppIcons;// some.component.ts
import { AppIcons } from '../shared/constants/app-icons.const';
@Component({ ... })
export class SomeComponent {
protected readonly icons = AppIcons; // expose cho template dùng
}<!-- some.component.html — không còn chuỗi path rải rác -->
<asc-icon [src]="icons.user" size="24px" />
<asc-icon [src]="icons.star" size="24px" />Nếu app có nhiều icon và muốn tách theo nhóm chức năng (thay vì 1 file phẳng), chia theo domain — nav-icons.const.ts, action-icons.const.ts — rồi gộp lại nếu cần:
// shared/constants/icons/nav-icons.const.ts
export const NavIcons = { home: '/assets/icons/nav/home.svg', ... } as const;
// shared/constants/icons/action-icons.const.ts
export const ActionIcons = { delete: '/assets/icons/action/delete.svg', ... } as const;Picklist — AscPicklistComponent
<asc-picklist [(source)]="available" [(target)]="selected"
optionLabel="name" sourceHeader="Nguồn" targetHeader="Đã chọn" />Avatar — AscAvatarComponent / AscAvatarGroupComponent
<!-- Ảnh -->
<asc-avatar image="/assets/user.jpg" size="large" />
<!-- Chữ viết tắt + màu nền -->
<asc-avatar label="NT" [style]="{ background: '#F74036', color: '#fff' }" />
<!-- Icon mặc định (pi-user) -->
<asc-avatar />
<!-- Nhóm avatar chồng nhau -->
<asc-avatar-group>
<asc-avatar image="/u1.jpg" size="large" />
<asc-avatar image="/u2.jpg" size="large" />
<asc-avatar label="+3" size="large" [style]="{ background: '#dee2e6', color: '#495057' }" />
</asc-avatar-group>| Input | Type | Default | Mô tả |
|----------|-----------------------------------|-------------|----------------------------------------------------|
| image | string | '' | URL ảnh — ưu tiên cao nhất |
| label | string | '' | Chữ viết tắt — fallback khi không có image |
| icon | string | '' | PrimeIcon — fallback, mặc định pi pi-user |
| size | 'normal'\|'large'\|'xlarge' | 'normal' | 2rem / 3rem / 4rem |
| shape | 'circle'\|'square' | 'circle' | Hình dạng |
| style | Record<string, string> | {} | Inline style (màu nền/chữ) |
Timeline — AscTimelineComponent
<asc-timeline [items]="history" />
<asc-timeline [items]="history" align="alternate" />
<asc-timeline [items]="steps" layout="horizontal" align="top" />import { AscTimelineItem } from '@aseansc-admin/ui';
history: AscTimelineItem[] = [
{ title: 'Tạo cuộc họp', date: '01/06/2026', status: 'info', icon: 'pi pi-plus' },
{ title: 'Gửi lời mời', date: '01/06/2026', status: 'info', icon: 'pi pi-send',
description: '12 thành viên được mời' },
{ title: 'Đang diễn ra', date: '08/06/2026', status: 'warn', icon: 'pi pi-spin pi-spinner' },
{ title: 'Hoàn thành', date: '08/06/2026', status: 'success', icon: 'pi pi-check' },
];| Input | Type | Default | Mô tả |
|----------|---------------------------------------------|--------------|--------------------------------------|
| items | AscTimelineItem[] | [] | Danh sách sự kiện |
| align | 'left'\|'right'\|'alternate'\|'top'\|'bottom' | 'left' | Căn chỉnh content |
| layout | 'vertical'\|'horizontal' | 'vertical' | Hướng timeline |
AscTimelineItem: title (required), description?, date?, icon?, status? (success/info/warn/danger/secondary).
Overlay
Toast & Confirm (programmatic)
import { AscToastService, AscConfirmService } from '@aseansc-admin/ui';
// Toast
this.toast.success('Lưu thành công');
this.toast.error('Có lỗi xảy ra');
// Confirm dialog — preset
this.confirm.delete('Xoá bản ghi này?', () => this.delete(id));
this.confirm.save('Lưu thay đổi?', () => this.save());
this.confirm.leave(() => this.router.navigate(['/']));
// Confirm dialog — generic
this.confirm.show({ message: 'Bạn có chắc?', accept: () => this.doIt() });
// Confirm popup (anchor to click event)
onDelete(event: MouseEvent) {
this.confirm.popup(event, {
message: 'Xoá bản ghi này?',
accept: () => this.delete(),
});
}Đặt <asc-confirm-popup /> trong layout để dùng confirm.popup():
<asc-confirm-popup />Dialog — AscDialogComponent
<asc-dialog [(visible)]="showDialog" header="Tiêu đề">
<p>Nội dung dialog</p>
<ng-template ascDialogFooter>
<asc-button label="Đóng" severity="secondary" (clicked)="showDialog = false" />
</ng-template>
</asc-dialog>Sidebar — AscSidebarComponent
<asc-sidebar [(visible)]="showSidebar" position="right">
<p>Nội dung sidebar</p>
</asc-sidebar>Popover — AscPopoverComponent
<asc-button label="Mở popover" (clicked)="pop.toggle($event)" />
<asc-popover #pop>
<p>Nội dung popover</p>
</asc-popover>Tooltip — AscTooltipDirective
<asc-button label="Lưu" ascTooltip="Lưu thay đổi" tooltipPosition="top" />
<span ascTooltip="Thông tin thêm">Hover vào đây</span>Panel
Tabs — AscTabsComponent / AscTabComponent
<asc-tabs [(value)]="activeTab">
<asc-tab value="info" label="Thông tin" icon="pi pi-user">
<p>Nội dung thông tin...</p>
</asc-tab>
<asc-tab value="settings" label="Cài đặt" icon="pi pi-cog">
<p>Nội dung cài đặt...</p>
</asc-tab>
</asc-tabs>Stepper — AscStepperComponent / AscStepComponent
<asc-stepper [(value)]="activeStep">
<asc-step [value]="1" label="Thông tin">
<p>Bước 1...</p>
<asc-button label="Tiếp theo" icon="pi pi-arrow-right" iconPos="right"
(clicked)="activeStep.set(2)" />
</asc-step>
<asc-step [value]="2" label="Xác nhận">
<p>Bước 2...</p>
<asc-button label="Quay lại" severity="secondary" (clicked)="activeStep.set(1)" />
<asc-button label="Hoàn tất" severity="success" (clicked)="activeStep.set(3)" />
</asc-step>
<asc-step [value]="3" label="Hoàn thành">
<p>Hoàn tất!</p>
</asc-step>
</asc-stepper>Feedback
Spinner — AscSpinnerComponent
Loading indicator tròn dùng để hiển thị trạng thái chờ.
<asc-spinner />
<asc-spinner size="lg" />
<asc-spinner size="xl" strokeWidth="2" />| Input | Type | Default | Mô tả |
|---------------|-----------------------------------|----------|------------------------|
| size | 'xs'\|'sm'\|'md'\|'lg'\|'xl' | 'md' | Kích thước (16–80px) |
| strokeWidth | string | '4' | Độ dày nét SVG (1–10) |
Badge — AscBadgeComponent
Label nhỏ dùng để đánh dấu trạng thái, danh mục.
<asc-badge label="Mới" />
<asc-badge label="Thành công" severity="success" />
<asc-badge label="Cảnh báo" severity="warn" [rounded]="true" />
<asc-badge label="Lỗi" severity="danger" icon="pi pi-times" />| Input | Type | Default | Mô tả |
|------------|---------------------------------------------------------------------------|-------------|----------------------|
| label | string | (required)| Nội dung badge |
| severity | 'primary'\|'success'\|'warn'\|'danger'\|'info'\|'secondary'\|'contrast' | 'primary' | Màu sắc |
| rounded | boolean | false | Bo tròn hoàn toàn |
| icon | string | — | PrimeIcon class |
Alert — AscAlertComponent
Thông báo inline dạng banner, hỗ trợ đóng.
<asc-alert severity="success" title="Lưu thành công" text="Dữ liệu đã được cập nhật." />
<asc-alert severity="warn" title="Cảnh báo" [closable]="true" (closed)="onDismiss()">
<p>Nội dung <strong>phức tạp</strong> với HTML.</p>
</asc-alert>| Input | Type | Default | Mô tả |
|------------|----------------------------------------|----------|------------------------------------|
| severity | 'success'\|'info'\|'warn'\|'danger' | 'info' | Loại alert |
| title | string | — | Tiêu đề |
| text | string | — | Nội dung văn bản |
| closable | boolean | false | Hiển thị nút đóng |
| Output | Mô tả |
|----------|-----------------------------|
| closed | Emit khi user bấm đóng |
Skeleton — AscSkeletonComponent
Placeholder hiển thị trong khi data đang load.
<!-- Preset nhanh -->
<asc-skeleton preset="text" />
<asc-skeleton preset="avatar" />
<asc-skeleton preset="button" />
<!-- Tuỳ chỉnh -->
<asc-skeleton shape="rectangle" width="200px" height="120px" />
<asc-skeleton shape="circle" width="64px" height="64px" />| Input | Type | Mô tả |
|----------------|---------------------------------------------------|----------------------------------------|
| preset | 'text'\|'avatar'\|'button'\|'image'\|'thumbnail'| Shortcut định kích thước sẵn |
| shape | 'rectangle'\|'circle' | Hình dạng (default: 'rectangle') |
| width | string | Ghi đè chiều rộng từ preset |
| height | string | Ghi đè chiều cao từ preset |
| borderRadius | string | Border radius tuỳ chỉnh |
Progress Bar — AscProgressBarComponent
Thanh tiến trình cho thấy % hoàn thành hoặc trạng thái đang xử lý.
<!-- Xác định giá trị -->
<asc-progress-bar [value]="75" />
<asc-progress-bar [value]="uploadPercent" unit="%" [showValue]="true" />
<!-- Indeterminate — xử lý không biết bao lâu -->
<asc-progress-bar mode="indeterminate" />| Input | Type | Default | Mô tả |
|-------------|-----------------------------------|------------------|-----------------------------|
| value | number | — | Giá trị (0–100) |
| mode | 'determinate'\|'indeterminate' | 'determinate' | Chế độ hiển thị |
| showValue | boolean | false | Hiển thị % bên trong thanh |
| unit | string | '%' | Đơn vị hiển thị |
| color | string | — | Màu thanh (hex/CSS var) |
Empty State — AscEmptyStateComponent
Giao diện khi không có dữ liệu để hiển thị.
<asc-empty-state title="Không có dữ liệu" description="Chưa có bản ghi nào được tạo." />
<!-- Với action button -->
<asc-empty-state title="Chưa có cuộc họp nào" icon="pi pi-calendar">
<asc-button label="Tạo cuộc họp" icon="pi pi-plus" (clicked)="openCreate()" />
</asc-empty-state>| Input | Type | Mô tả |
|---------------|----------|----------------------------------------------------|
| title | string | (required) Tiêu đề |
| icon | string | PrimeIcon class (default: 'pi pi-inbox') |
| description | string | Mô tả bổ sung |
ng-content dùng cho action buttons hoặc nội dung custom.
Loading Bar — AscLoadingBarComponent
Thanh loading mỏng 3px cố định ở đầu trang, dùng kết hợp với AscLoadingService từ @aseansc-admin/sea-http để hiện/ẩn tự động theo trạng thái API call.
// app.component.ts
import { AscLoadingService } from '@aseansc-admin/sea-http';
loading = inject(AscLoadingService);<!-- app.component.html — đặt trước mọi content -->
<asc-loading-bar [visible]="loading.isLoading()" />
<p-toast />
<p-confirmdialog />
<router-outlet />| Input | Type | Default | Mô tả |
|-----------|-----------|---------|----------------------------------------------|
| visible | boolean | false | Hiển thị/ẩn bar. Thường bind với isLoading() |
Loading Overlay — AscLoadingOverlayComponent
Overlay toàn màn hình với spinner ở giữa, block hoàn toàn tương tác người dùng trong khi xử lý tác vụ nặng. Có backdrop-filter: blur(2px) và z-index 10000.
// app.component.ts
import { AscLoadingService } from '@aseansc-admin/sea-http';
loading = inject(AscLoadingService);<!-- app.component.html -->
<asc-loading-overlay [visible]="loading.isLoading()" />
<!-- hoặc kèm message -->
<asc-loading-overlay [visible]="loading.isLoading()" message="Đang xử lý..." />
<p-toast />
<p-confirmdialog />
<router-outlet />| Input | Type | Default | Mô tả |
|-----------|-----------|---------|-----------------------------------------------------|
| visible | boolean | false | Hiển thị/ẩn overlay. |
| message | string | '' | Text tuỳ chọn hiển thị dưới spinner (vd: 'Đang xử lý...'). |
Validation Error Messages — AscMessageErrorPipe
AscMessageErrorPipe chuyển Angular validator error key thành human-readable message theo locale. Pipe này được AscFormFieldComponent dùng tự động — bạn không cần gọi trực tiếp khi dùng <asc-form-field>. Tuy nhiên bạn có thể dùng độc lập trong bất kỳ template nào.
Cú pháp
{{ errorKey | ascMessageError : label : control.errors }}| Tham số | Type | Mô tả |
|----------------|--------------------|----------------------------------------------------|
| errorKey | string | Angular validator error key: 'required', 'email', ... |
| label | string | Tên field hiển thị trong message: 'Email', 'Họ tên' |
| control.errors | ValidationErrors \| null | AbstractControl.errors — để pipe đọc metadata (vd: minlength.requiredLength) |
Dùng trực tiếp trong template
import { AscMessageErrorPipe } from '@aseansc-admin/ui';
@Component({
imports: [AscMessageErrorPipe, ReactiveFormsModule, KeyValuePipe],
template: `
<input [formControl]="emailCtrl" />
@if (emailCtrl.errors && emailCtrl.touched) {
@for (err of emailCtrl.errors | keyvalue; track err.key) {
<span class="error">
{{ err.key | ascMessageError : 'Email' : emailCtrl.errors }}
</span>
}
}
`,
})
export class MyForm {
emailCtrl = new FormControl('', [Validators.required, Validators.email]);
}Kết quả:
| Validator | Output |
|-------------------------|-------------------------------------------|
| Validators.required | Email là bắt buộc |
| Validators.email | Email không đúng định dạng email |
| Validators.minLength(6) | Email phải có ít nhất 6 ký tự |
| Validators.maxLength(100) | Email không được vượt quá 100 ký tự |
| Validators.min(0) | Email phải lớn hơn hoặc bằng 0 |
| Validators.max(100) | Email phải nhỏ hơn hoặc bằng 100 |
| Validators.pattern(...) | Email không đúng định dạng |
Built-in error keys
| Key | Validator tương ứng |
|-----------------|----------------------------------------------|
| required | Validators.required |
| requiredTrue | Validators.requiredTrue |
| email | Validators.email |
| minlength | Validators.minLength(n) |
| maxlength | Validators.maxLength(n) |
| min | Validators.min(n) |
| max | Validators.max(n) |
| pattern | Validators.pattern(...) |
| phone | custom validator |
| url | custom validator |
| dateRange | custom validator |
| passwordMatch | custom validator |
| unique | custom validator |
| _fallback | mọi key không có trong map → "{label} không hợp lệ" |
Override message của built-in key
Truyền validation.messages vào provideAscUI() — chỉ cần khai báo key muốn đổi, các key còn lại giữ nguyên mặc định:
// app.config.ts
provideAscUI({
locale: 'vi-VN',
validation: {
messages: {
required: (label) => `${label} không được bỏ trống`,
minlength: (label, errors) => {
const min = (errors?.['minlength'] as any)?.requiredLength;
return `${label} tối thiểu ${min} ký tự`;
},
},
},
})Thêm error key cho custom validator
Tạo validator trả về object với key tùy chọn, sau đó đăng ký message tương ứng:
// validators/phone.validator.ts
export function phoneValidator(): ValidatorFn {
return (control) => {
const valid = /^(0[3|5|7|8|9])\d{8}$/.test(control.value);
return valid ? null : { phone: true }; // key = 'phone'
};
}// validator với metadata (để pipe đọc thêm thông tin)
export function passwordStrengthValidator(minScore: number): ValidatorFn {
return (control) => {
const score = calcScore(control.value);
return score >= minScore ? null : { passwordStrength: { minScore, actualScore: score } };
};
}// app.config.ts
provideAscUI({
validation: {
messages: {
// Key khớp với key trả về từ validator
phone: (label) =>
`${label} không đúng định dạng (vd: 0912345678)`,
passwordStrength: (label, errors) => {
const { minScore, actualScore } = (errors?.['passwordStrength'] ?? {}) as any;
return `${label} chưa đủ mạnh (điểm: ${actualScore}/${minScore})`;
},
},
},
})// dùng trong form
password = new FormControl('', [
Validators.required,
passwordStrengthValidator(3),
]);Template hiển thị tự động qua <asc-form-field>:
<asc-form-field label="Mật khẩu">
<asc-password formControlName="password" />
</asc-form-field>Dark Mode
The library uses .app-dark on <html> to toggle dark mode. PrimeNG components and Tailwind utilities both respond to this class automatically — the dark variant is already registered by @aseansc-admin/ui/preset.css (see Setup), no extra config needed.
i18n
provideAscUI({ locale: 'vi-VN' }) // mặc định — Tiếng Việt
provideAscUI({ locale: 'en-US' }) // EnglishĐổi ngôn ngữ runtime — không cần reload
locale truyền vào provideAscUI() chỉ set giá trị khởi tạo. Để đổi ngôn ngữ khi app đang chạy (vd: dropdown chọn ngôn ngữ trên topbar), dùng AscLocaleService — mọi label, message, và cả translation nội bộ của PrimeNG (tên tháng, filter operator trong table, aria label...) sẽ tự cập nhật ngay lập tức, không cần reload trang.
import { AscLocaleService } from '@aseansc-admin/ui';
@Component({ ... })
export class LanguageSwitcherComponent {
private locale = inject(AscLocaleService);
switchToEnglish() {
this.locale.setLocale('en-US');
}
// Đọc locale hiện tại (signal) — dùng để highlight nút đang active, v.v.
protected readonly currentLocale = this.locale.locale;
}License
UNLICENSED — Internal use only.
