@sdcorejs/angular
v21.0.7
Published
> Angular UI library built on Angular Material - supports Angular 19 / 20 / 21.
Downloads
1,017
Readme
@sdcorejs/angular
Angular UI library built on Angular Material - supports Angular 19 / 20 / 21.
🚀 Showcase · 📖 Storybook — code samples, props & API
Table of Contents / Mục lục
- Getting Started / Cài đặt
- Theming / SCSS Customization
- Components
- Form Components
- CRUD Patterns / Code mẫu CRUD
- Contributing Guide / Hướng dẫn đóng góp
Getting Started / Cài đặt
Prerequisites / Yêu cầu
| Dependency | Version |
|---|---|
| @angular/core | ^19.0.0 \|\| ^20.0.0 \|\| ^21.0.0 |
| @angular/material | ^19.0.0 \|\| ^20.0.0 \|\| ^21.0.0 |
| @angular/material-date-fns-adapter | ^19.0.0 \|\| ^20.0.0 \|\| ^21.0.0 |
| date-fns | ^3 \|\| ^4 |
Installation / Cài đặt
npm install @sdcorejs/angularSetup
1. Import global styles / Import style toàn cục
Thêm vào angular.json (hoặc styles.scss của app):
// angular.json
{
"styles": [
"node_modules/@sdcorejs/angular/assets/scss/sd-core.scss"
]
}hoặc trong styles.scss:
@use '@sdcorejs/angular/assets/scss/sd-core';2. Import Material Icons font / Font icon
Thêm vào index.html:
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600&display=swap" rel="stylesheet" />3. Configure providers / Cấu hình providers
// app.config.ts
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const appConfig: ApplicationConfig = {
providers: [
provideAnimationsAsync(),
// ... các providers khác
],
};Theming / SCSS Customization
CSS Variables
@sdcorejs/angular sử dụng CSS custom properties (variables) để quản lý màu sắc. Mỗi màu được expose dưới dạng --sd-<color>.
Available color tokens / Các biến màu sắc:
| Variable | Default | Description |
|---|---|---|
| --sd-primary | #2A66F4 | Màu chính |
| --sd-primary-light | #EAF1FF | Màu chính nhạt |
| --sd-primary-dark | #1C4AD9 | Màu chính đậm |
| --sd-secondary | #212121 | Màu phụ |
| --sd-secondary-light | #E9E9E9 | Màu phụ nhạt |
| --sd-success | #4CAF50 | Thành công |
| --sd-success-light | #DBEFDC | Thành công nhạt |
| --sd-warning | #FF9600 | Cảnh báo |
| --sd-warning-light | #FFEACC | Cảnh báo nhạt |
| --sd-error | #F82C13 | Lỗi |
| --sd-error-light | #FED5D0 | Lỗi nhạt |
| --sd-info | #2962FF | Thông tin |
| --sd-info-light | #E7E9FF | Thông tin nhạt |
| --sd-black500 | #212121 | Xám đậm nhất |
| --sd-black400 | #757575 | Xám đậm |
| --sd-black300 | #BFBFBF | Xám trung |
| --sd-black200 | #E6E6E6 | Xám nhạt |
| --sd-black100 | #F2F2F2 | Xám nhạt nhất |
Custom Theme / Tuỳ chỉnh theme
Ghi đè theme mặc định bằng cách truyền map SCSS vào mixin theme():
// styles.scss
@use '@sdcorejs/angular/assets/scss/themes/default' as default;
html {
@include default.theme((
primary: #7C3AED,
primary-light: #EDE9FE,
primary-dark: #5B21B6,
success: #10B981,
error: #EF4444,
));
}Chỉ cần override các màu muốn thay đổi — các màu còn lại giữ nguyên giá trị mặc định.
Utility Classes / Các class tiện ích
Thư viện cung cấp sẵn các utility class:
<!-- Text color -->
<span class="text-primary">Text màu primary</span>
<span class="text-error">Text màu error</span>
<!-- Background -->
<div class="bg-primary-light">Background nhạt</div>
<!-- Spacing (đơn vị px, từ 0–200) -->
<div class="mt-16 mb-8 px-24">margin-top: 16px, padding: 0 24px</div>
<!-- Gap -->
<div class="d-flex gap-8">gap: 8px</div>
<!-- Grid -->
<div class="sd-grid-container grid-cols-3">
<div class="col-span-2">Chiếm 2 cột</div>
<div class="col-span-1">Chiếm 1 cột</div>
</div>
<!-- Bootstrap grid -->
<div class="row">
<div class="col-6">50%</div>
<div class="col-6">50%</div>
</div>Components
Tất cả component đều là standalone và sử dụng Angular Signals API.
All components are standalone and use Angular Signals API.
SdButton
import { SdButton } from '@sdcorejs/angular/components/button';Inputs:
| Input | Type | Default | Description |
|---|---|---|---|
| type | 'fill' \| 'light' \| 'outline' \| 'link' | 'light' | Kiểu nút |
| color | Color | 'secondary' | Màu sắc |
| size | 'sm' \| 'md' \| 'lg' | 'sm' | Kích thước |
| title | string | — | Nhãn nút |
| prefixIcon | string | — | Icon Material trước text |
| suffixIcon | string | — | Icon Material sau text |
| disabled | boolean | false | Vô hiệu hoá |
| loading | boolean | false | Trạng thái loading (tự chặn click) |
| tooltip | string | — | Tooltip khi hover |
| width | string | — | CSS width tuỳ chỉnh |
Output: (click): EventEmitter<Event> — có throttle 300ms, tự chặn khi disabled hoặc loading.
<sd-button type="fill" color="primary" title="Lưu" prefixIcon="save" (click)="onSave()" />
<sd-button type="outline" color="error" prefixIcon="delete" tooltip="Xoá" />
<sd-button type="light" title="Huỷ" (click)="modal.close()" />
<sd-button type="fill" color="primary" title="Đang xử lý" [loading]="true" />SdBadge
import { SdBadge } from '@sdcorejs/angular/components/badge';Inputs:
| Input | Type | Default | Description |
|---|---|---|---|
| type | 'tag' \| 'round' \| 'icon' | 'icon' | Kiểu badge |
| color | Color | 'secondary' | Màu sắc |
| title | string \| number | — | Nội dung hiển thị |
| icon | string | — | Icon Material |
| size | Size | 'sm' | Kích thước |
| tooltip | string | — | Tooltip |
Shorthand color inputs (boolean): primary, secondary, success, info, warning, error.
<sd-badge type="tag" color="success" title="Hoạt động" />
<sd-badge type="round" [warning]="true" title="Chờ duyệt" />
<sd-badge type="icon" color="error" icon="close" title="Từ chối" />SdSection
import { SdSection } from '@sdcorejs/angular/components/section';Inputs:
| Input | Type | Default | Description |
|---|---|---|---|
| title | string | required | Tiêu đề section |
| subTitle | string | — | Tiêu đề phụ |
| icon | string | — | Icon Material |
| iconColor | Color | 'primary' | Màu icon |
| collapsable | boolean | false | Cho phép thu gọn |
| collapsed | boolean | false | Trạng thái ban đầu thu gọn |
| hideHeader | boolean | false | Ẩn phần header |
<sd-section title="Thông tin cơ bản" icon="person" iconColor="primary">
<!-- nội dung -->
</sd-section>
<sd-section title="Cài đặt nâng cao" icon="settings" collapsable [collapsed]="true">
<!-- nội dung ẩn mặc định -->
</sd-section>SdModal
import { SdModal } from '@sdcorejs/angular/components/modal';Inputs:
| Input | Type | Default | Description |
|---|---|---|---|
| title | string | — | Tiêu đề modal |
| color | Color | 'primary' | Màu header |
| width | 'sx' \| 'sm' \| 'md' \| 'lg' \| string | 'md' | Độ rộng (md = 60vw) |
| height | string | 'auto' | Chiều cao |
| view | 'dialog' \| 'bottom-sheet' | auto | Tự động bottom-sheet trên mobile |
| lazyLoadContent | boolean | true | Lazy render nội dung |
Output: (sdClosed): EventEmitter — phát ra khi modal đóng.
Methods (dùng qua @ViewChild):
modal.open()— mở modalmodal.close()— đóng modal
⚠️ Nội dung modal phải đặt trong
<ng-template>.
<sd-modal #myModal title="Thêm mới" width="md" (sdClosed)="onClosed()">
<ng-template>
<div class="modal-body p-16">
<!-- nội dung form -->
</div>
<div class="modal-footer d-flex justify-content-end gap-8 p-16">
<sd-button title="Huỷ" (click)="myModal.close()" />
<sd-button type="fill" color="primary" title="Lưu" (click)="onSave()" />
</div>
</ng-template>
</sd-modal>
<sd-button title="Mở modal" prefixIcon="add" (click)="myModal.open()" />SdTable
import { SdTable } from '@sdcorejs/angular/components/table';
import type { SdTableOption, SdTableColumn } from '@sdcorejs/angular/components/table';SdTable nhận một object option duy nhất kiểu SdTableOption<T>.
Column types / Kiểu cột:
| type | Mô tả |
|---|---|
| 'string' | Văn bản |
| 'number' | Số (tự format) |
| 'boolean' | True/False |
| 'date' | Ngày |
| 'datetime' | Ngày giờ |
| 'time' | Giờ |
| 'values' | Enum từ danh sách cố định |
| 'lazy-values' | Enum load async |
| 'children' | Cột nhóm (multi-header) |
Local table:
option: SdTableOption<Product> = {
type: 'local',
items: () => this.products,
columns: [
{ field: 'code', type: 'string', title: 'Mã', width: '120px' },
{ field: 'name', type: 'string', title: 'Tên', sortable: true },
{ field: 'price', type: 'number', title: 'Đơn giá', align: 'right' },
{ field: 'active', type: 'boolean', title: 'Kích hoạt',
useBadge: (val) => ({ color: val ? 'success' : 'secondary', title: val ? 'Có' : 'Không' })
},
],
paginate: { pageSize: 20 },
reload: { visible: true },
};Server-side table:
option: SdTableOption<Product> = {
type: 'server',
items: async (filterRequest, pagingReq) => {
const res = await this.service.search({
keyword: filterRequest.keyword,
page: pagingReq.page,
pageSize: pagingReq.pageSize,
});
return { items: res.data, total: res.total };
},
columns: [...],
command: {
align: 'right',
commands: [
{ icon: 'edit', color: 'primary', title: 'Sửa', click: (row) => this.onEdit(row) },
{ icon: 'delete', color: 'error', title: 'Xoá', click: (row) => this.onDelete(row) },
],
},
};<sd-table #sdTable [option]="option" />Tree rows / Dòng cây:
tree là discriminated union theo loadType. Icon expand (chevron_right / expand_more) nằm ở cột đầu — cột STT khi bật index, ngược lại cột data đầu tiên — thụt lề theo cấp.
// loadType: 'static' — children embedded sẵn trong mỗi row
tree: { loadType: 'static', childrenKey: 'children', defaultExpanded: 1 }
// loadType: 'lazy' — nạp con khi bung (Promise); hasChildren gate icon expand
tree: {
loadType: 'lazy',
hasChildren: (row) => row.type === 'Folder', // chỉ Folder mới có icon expand
onExpandChildren: (row) => api.getChildren(row.id), // () => Promise<T[]>
}- Child-level search (
type: 'local'+loadType: 'static'): lọc inline tìm cả cấp con — giữ nhánh cha của node khớp, ẩn sibling không khớp, tự bung tới node khớp. Clear filter khôi phục cây. - Lazy loading: spinner hiện trong ô chevron khi
onExpandChildrenđang chạy;hasChildrenquyết định dòng nào hiện icon (không truyền = mọi node đều hiện).
SdAvatar
import { SdAvatar } from '@sdcorejs/angular/components/avatar';<sd-avatar src="/api/avatar/123" name="Nguyễn Văn A" size="md" />
<sd-avatar name="NVA" color="primary" size="lg" />Other Components / Các component khác
| Component | Import | Mô tả |
|---|---|---|
| SdTabRouter | @sdcorejs/angular/components/tab-router | Tab navigation với Angular Router |
| SdSideDrawer | @sdcorejs/angular/components/side-drawer | Drawer layout trái/phải |
| SdUploadFile | @sdcorejs/angular/components/upload-file | Upload file |
| SdQuickAction | @sdcorejs/angular/components/quick-action | Nút action dạng icon |
| SdHistory | @sdcorejs/angular/components/history | Lịch sử thay đổi |
| SdImportExcel | @sdcorejs/angular/components/import-excel | Wizard import Excel |
| SdQueryBuilder | @sdcorejs/angular/components/query-builder | Visual query builder |
| SdCodeEditor | @sdcorejs/angular/components/code-editor | Code editor (PrismJS) |
| SdMiniEditor | @sdcorejs/angular/components/mini-editor | Rich text editor nhỏ |
| SdDocumentBuilder | @sdcorejs/angular/components/document-builder | Document builder |
| SdAnchorMain | @sdcorejs/angular/components/anchor | Anchor / mục lục cuộn trang |
| SdView | @sdcorejs/angular/components/view | View wrapper read-only |
Form Components
import {
SdInput, // Text input
SdInputNumber, // Number input
SdSelect, // Dropdown
SdAutocomplete, // Autocomplete
SdDate, // Date picker
} from '@sdcorejs/angular/forms';<sd-input [(model)]="form.name" label="Họ tên" [required]="true" />
<sd-input-number [(model)]="form.price" label="Đơn giá" [min]="0" suffix="VNĐ" />
<sd-select [(model)]="form.status" label="Trạng thái"
[items]="statusList" valueField="value" displayField="label" />
<sd-date [(model)]="form.birthday" label="Ngày sinh" />CRUD Patterns / Code mẫu CRUD
List Component
// product-list.component.ts
import { Component, OnInit, ViewChild, signal } from '@angular/core';
import { SdTable, SdTableOption } from '@sdcorejs/angular/components/table';
import { SdButton } from '@sdcorejs/angular/components/button';
import { SdModal } from '@sdcorejs/angular/components/modal';
import { SdSection } from '@sdcorejs/angular/components/section';
import { SdInput, SdSelect } from '@sdcorejs/angular/forms';
interface Product {
id: number;
code: string;
name: string;
price: number;
status: 'ACTIVE' | 'INACTIVE';
}
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
standalone: true,
imports: [SdTable, SdButton, SdModal, SdSection, SdInput, SdSelect],
})
export class ProductListComponent implements OnInit {
@ViewChild('formModal') formModal!: SdModal;
@ViewChild('sdTable') sdTable?: SdTable<Product>;
selectedItem: Product | null = null;
formData: Partial<Product> = {};
isSaving = signal(false);
readonly STATUS_LIST = [
{ value: 'ACTIVE', label: 'Hoạt động' },
{ value: 'INACTIVE', label: 'Dừng' },
];
option!: SdTableOption<Product>;
constructor(private service: ProductService) {}
ngOnInit() {
this.option = {
type: 'server',
items: async (filter, paging) => this.service.search(filter, paging),
columns: [
{ field: 'code', type: 'string', title: 'Mã', width: '120px' },
{ field: 'name', type: 'string', title: 'Tên', sortable: true },
{ field: 'price', type: 'number', title: 'Đơn giá', align: 'right' },
{
field: 'status', type: 'values', title: 'Trạng thái',
option: { items: this.STATUS_LIST, valueField: 'value', displayField: 'label' },
useBadge: (val) => ({
color: val === 'ACTIVE' ? 'success' : 'secondary',
title: this.STATUS_LIST.find(s => s.value === val)?.label,
}),
},
],
command: {
align: 'right',
commands: [
{ icon: 'edit', color: 'primary', title: 'Sửa', click: (row) => this.openForm(row) },
{ icon: 'delete', color: 'error', title: 'Xoá', click: (row) => this.onDelete(row) },
],
},
paginate: { pageSize: 20 },
reload: { visible: true },
};
}
openForm(item?: Product) {
this.selectedItem = item || null;
this.formData = item ? { ...item } : { status: 'ACTIVE' };
this.formModal.open();
}
async onSave() {
this.isSaving.set(true);
try {
if (this.selectedItem) {
await this.service.update(this.selectedItem.id, this.formData);
} else {
await this.service.create(this.formData);
}
this.formModal.close();
this.sdTable?.reload?.();
} finally {
this.isSaving.set(false);
}
}
async onDelete(item: Product) {
if (!confirm(`Xoá "${item.name}"?`)) return;
await this.service.delete(item.id);
this.sdTable?.reload?.();
}
}Template
<!-- product-list.component.html -->
<div class="d-flex justify-content-between align-items-center mb-16">
<h2>Danh sách sản phẩm</h2>
<sd-button type="fill" color="primary" title="Thêm mới"
prefixIcon="add" (click)="openForm()" />
</div>
<sd-table #sdTable [option]="option" />
<sd-modal #formModal [title]="selectedItem ? 'Chỉnh sửa' : 'Thêm mới'" width="md">
<ng-template>
<div class="modal-body p-16">
<sd-section title="Thông tin sản phẩm" icon="inventory">
<div class="row">
<div class="col-6">
<sd-input [(model)]="formData.code" label="Mã" [required]="true" />
</div>
<div class="col-6">
<sd-select [(model)]="formData.status" label="Trạng thái"
[items]="STATUS_LIST" valueField="value" displayField="label" />
</div>
<div class="col-12">
<sd-input [(model)]="formData.name" label="Tên sản phẩm" [required]="true" />
</div>
<div class="col-6">
<sd-input-number [(model)]="formData.price" label="Đơn giá" suffix="VNĐ" />
</div>
</div>
</sd-section>
</div>
<div class="modal-footer d-flex justify-content-end gap-8 p-16">
<sd-button title="Huỷ" (click)="formModal.close()" />
<sd-button type="fill" color="primary" title="Lưu"
prefixIcon="save" [loading]="isSaving()" (click)="onSave()" />
</div>
</ng-template>
</sd-modal>Contributing Guide / Hướng dẫn đóng góp
Cấu trúc thư viện / Project structure
sdcorejs-angular/
├── src/
│ └── public-api.ts # Entry point chính
├── assets/
│ └── scss/
│ ├── sd-core.scss # SCSS entry (import vào app)
│ ├── core/ # Base utilities (color, grid, form, ...)
│ └── themes/ # Theme mặc định + Material theme
├── components/ # UI Components
│ ├── button/
│ ├── table/
│ ├── modal/
│ └── ...
├── forms/ # Form components
│ ├── input/
│ ├── select/
│ └── ...
├── directives/ # Angular directives
├── pipes/ # Angular pipes
├── services/ # Shared services
├── utilities/ # Types, models, helpers
└── modules/ # Feature modules (layout, permission, ...)Thêm component mới / Adding a new component
1. Tạo thư mục component:
components/
└── my-component/
├── index.ts # Export public API
├── ng-package.json # ng-packagr entry
└── src/
├── my-component.component.ts
├── my-component.component.html
└── my-component.component.scss2. ng-package.json:
{
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "index.ts"
}
}3. index.ts:
export * from './src/my-component.component';4. Component template:
// my-component.component.ts
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { SdBaseSecureComponent } from '@sdcorejs/angular/components/base';
import { Color } from '@sdcorejs/angular/utilities';
@Component({
selector: 'sd-my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [],
})
export class SdMyComponent extends SdBaseSecureComponent {
color = input<Color, Color | undefined | null>('primary', {
transform: (value) => value || 'primary',
});
title = input<string | undefined | null>(undefined);
}5. Export từ components/index.ts:
// components/index.ts
export * from '@sdcorejs/angular/components/my-component';Quy ước / Conventions
| Mục | Quy ước |
|---|---|
| Selector | sd-<tên-component> |
| Class name | Sd<TênComponent> (Pascal) |
| Input | Dùng input<T>() signal, không dùng @Input() decorator |
| Null safety | Input transform phải handle null/undefined |
| Base class | Extend SdBaseSecureComponent cho component có permission |
| Change detection | Luôn dùng ChangeDetectionStrategy.OnPush |
| Standalone | Luôn standalone: true |
| Colors | Dùng Color type, không hardcode màu |
Build
# Build toàn bộ thư viện
ng-packagr -p ng-package.json
# Watch mode
ng-packagr -p ng-package.json --watchVersioning
Scheme: <angular-major>.0.<release>. Major digit khoá theo Angular line (19.x = Angular 19, 20.x = Angular 20, 21.x = Angular 21) — KHÔNG dùng để báo breaking. Mỗi release publish đồng thời 3 major cùng nội dung feature, chỉ khác Angular shim.
- Luôn pin theo Angular line của bạn:
npm i @sdcorejs/angular@^19(hoặc@^20/@^21). - Breaking change được ghi rõ ở
CHANGELOG.md, mục Changed (BREAKING for consumers), kèm migration.
QA / E2E
Core UI components expose runtime state via lowercase data-* attributes for e2e selectors. The full catalog, component matrix, selector cookbook, and YAML schema for AI agents live in docs/E2E-ATTRIBUTES.md in the source repository.
License
MIT - see the LICENSE file.
