@aseansc-admin/ui
v0.6.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:
# Ví dụ sed (Linux/macOS) — backup trước khi chạy
find src -name "*.ts" -o -name "*.html" | xargs sed -i -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))/g'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
@use "@aseansc-admin/ui/tokens/preset" as *;
@use 'prismjs/themes/prism-okaidia.css';
@use "tailwindcss";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
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' },
];Picklist — AscPicklistComponent
<asc-picklist [(source)]="available" [(target)]="selected"
optionLabel="name" sourceHeader="Nguồn" targetHeader="Đã chọn" />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>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.
/* styles.scss */
@use "tailwindcss";
@custom-variant dark (&:where(.app-dark, .app-dark *));i18n
provideAscUI({ locale: 'vi-VN' }) // mặc định — Tiếng Việt
provideAscUI({ locale: 'en-US' }) // EnglishLicense
UNLICENSED — Internal use only.
