@phuoc2409/univer-toolkit
v1.0.18
Published
Lightweight wrapper for Univer spreadsheet with templates, tags, and data API support
Maintainers
Readme
Univer Toolkit – Nhúng Univer Spreadsheet + Data APIs (Select2) + Auto Form + Templates
Package: @phuoc2409/univer-toolkit
Univer Toolkit là một “bộ khung nhúng” (embed toolkit) dành cho Univer Spreadsheet với mục tiêu:
Biến việc “kết nối API → lọc dữ liệu → đổ vào sheet” thành cấu hình (config) + UI có sẵn, không cần viết UI tay cho từng API.
Bạn sẽ có:
- Spreadsheet (Univer) hiển thị trong
#sheet - Sidebar trong
#sidebarđể:- Chọn nguồn dữ liệu bằng Select2
- Tự sinh form input theo
fields(text/number/date/select/range…) - Thay thế biến
@paramtrong URL/body/headers - Gọi API và chèn dữ liệu vào sheet theo
mapping.columns
Ngoài ra còn có:
- Templates API để upload/insert template
- Tags để lọc theo template 👉 Đối với phần Template, khuyến nghị sử dụng cấu trúc API được xây dựng bằng NestJS (theo thiết kế sẵn của dự án) để đảm bảo tính mở rộng và đồng bộ giữa các service. Bạn có thể tải cấu trúc về để xem sử dụng hoặc là viết lại theo backend của bạn, nó chỉ đơn giản là API về templates và tags https://github.com/phuoc2426/samplate-template-api-nestjs.git
Mục lục
- 1. Dùng nhanh theo CDN (unpkg) – chạy ở mọi dự án
- 2. Dùng theo NPM (ESM/CJS) – phù hợp Vite/Webpack/Framework
- 3. Cấu hình quan trọng nhất:
window.USER_UNIVER_CONFIG - 4. Data APIs: Cấu hình nguồn dữ liệu + filter + mapping
- 5. Fields: Tự sinh UI input (text/select/date/range) + validate
- 6. Cơ chế
@param: Ghép URL / body để lọc dữ liệu - 7. FAQ (Hỏi – Đáp) + ví dụ trả lời sẵn
- 8. Triển khai trên mọi loại dự án (Front framework & Fullstack)
- 9. Troubleshooting nhanh
1. Dùng nhanh theo CDN (unpkg) – chạy ở mọi dự án
1.1. Load đúng thứ tự (rất quan trọng)
- React + ReactDOM (Univer UMD cần)
- RxJS
- jQuery + Select2 (phải trước toolkit)
- Univer presets UMD + locales
- CSS của Univer
- CSS của Toolkit
- Toolkit UMD
window.USER_UNIVER_CONFIG = {...}UniverToolkit.mount()
1.2. HTML mẫu (copy chạy ngay)
Bắt buộc có 2 div:
#sidebarvà#sheet.
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8" />
<title>Univer Toolkit UMD Demo (unpkg)</title>
<!-- React -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- RxJS -->
<script src="https://unpkg.com/rxjs@7/dist/bundles/rxjs.umd.min.js"></script>
<!-- jQuery + Select2 (PHẢI load trước toolkit) -->
<script src="https://unpkg.com/jquery@3/dist/jquery.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/js/select2.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/select2.min.css" />
<!-- Univer core -->
<script src="https://unpkg.com/@univerjs/presets/lib/umd/index.js"></script>
<script src="https://unpkg.com/@univerjs/preset-sheets-core/lib/umd/index.js"></script>
<script src="https://unpkg.com/@univerjs/preset-sheets-core/lib/umd/locales/en-US.js"></script>
<!-- Univer styles -->
<link rel="stylesheet" href="https://unpkg.com/@univerjs/preset-sheets-core/lib/index.css" />
<!-- Univer Toolkit styles -->
<link rel="stylesheet" href="https://unpkg.com/@phuoc2409/univer-toolkit/styles/univer-embed.css" />
<!-- Optional: SheetJS -->
<script src="https://unpkg.com/[email protected]/dist/xlsx.full.min.js"></script>
<!-- Univer Toolkit UMD -->
<script src="https://unpkg.com/@phuoc2409/univer-toolkit/dist/univer-toolkit.umd.min.js"></script>
</head>
<body>
<div id="page">
<div id="sidebar"></div>
<div id="sheet"></div>
</div>
<!-- CONFIG -->
<script>
const TEMPLATE_API_BASE = "http://127.0.0.1:8020";
const DATA_APIS = [
{
id: "all_students",
name: "👥 Tất cả sinh viên",
method: "GET",
url: "http://127.0.0.1:8000/users",
mapping: {
columns: [
{ key: "id", header: "ID" },
{ key: "name", header: "Họ tên" },
],
},
},
{
id: "products",
name: "🛒 Sản phẩm",
method: "GET",
url: "https://dummyjson.com/products/search",
responseSource: "products",
body: {
filters: {
q: "@q",
},
},
fields: {
q: {
label: "Từ khóa tìm kiếm",
type: "text",
placeholder: "Nhập từ khóa...",
},
},
mapping: {
columns: [
{ key: "id", header: "ID" },
{ key: "title", header: "Tên sản phẩm" },
{ key: "price", header: "Giá" },
{ key: "description", header: "Mô tả" },
{ key: "category", header: "Danh mục" },
],
},
},
];
window.USER_UNIVER_CONFIG = {
workbookName: "Demo Workbook",
dataApis: DATA_APIS,
templateApi: {
baseUrl: TEMPLATE_API_BASE,
headers: {},
},
defaultTemplateCategory: "default",
};
</script>
<!-- BOOT -->
<script>
window.addEventListener("load", () => {
requestAnimationFrame(() => {
if (window.UniverToolkit && typeof window.UniverToolkit.mount === "function") {
window.UniverToolkit.mount();
} else {
console.error("UniverToolkit not loaded! Check CDN loading order.");
}
});
});
</script>
</body>
</html>2. Dùng theo NPM (ESM/CJS) – phù hợp Vite/Webpack/Framework
Cài đặt:
npm i @phuoc2409/univer-toolkitImport CSS + mount:
import "@phuoc2409/univer-toolkit/styles/univer-embed.css";
import { mount } from "@phuoc2409/univer-toolkit";
(window as any).USER_UNIVER_CONFIG = {
workbookName: "My Workbook",
dataApis: [],
};
mount();Nếu bạn chạy SSR (Next.js/Nuxt): chỉ gọi
mount()ở client-side (useEffect,onMounted,<client-only>).
3. Cấu hình quan trọng nhất: window.USER_UNIVER_CONFIG
Toolkit đọc config từ biến toàn cục:
window.USER_UNIVER_CONFIG = { ... };3.1. Bảng tham số trong config tổng
| Tham số | Bắt buộc | Ý nghĩa | Ví dụ |
|---|---:|---|---|
| workbookName | ✅ | Tên workbook hiển thị | "Demo Workbook" |
| dataApis | ✅ | Danh sách nguồn dữ liệu để chọn và chèn vào sheet | DATA_APIS |
| templateApi | ❌ | Cấu hình backend template (tuỳ dự án) | { baseUrl, headers } |
| defaultTemplateCategory | ❌ | Nhãn category default khi browse templates | "default" |
| globalContext | ❌ | Biến toàn cục dùng để resolve @param | { token, orgId } |
3.2. globalContext dùng để làm gì?
globalContext là “context chung” (session context) dùng thay biến @param mà không cần user nhập mỗi lần.
Ví dụ:
window.USER_UNIVER_CONFIG = {
// ...
globalContext: {
token: "Bearer abc...",
orgId: 123,
},
};4. Data APIs: Cấu hình nguồn dữ liệu + filter + mapping
Mỗi phần tử trong dataApis đại diện cho một nguồn dữ liệu xuất hiện trong dropdown Select2.
4.1. Bảng tham số của một DataApi
| Tham số | Bắt buộc | Ý nghĩa |
|---|---:|---|
| id | ✅ | Định danh duy nhất |
| name | ✅ | Tên hiển thị (có thể kèm emoji) |
| method | ✅ | GET/POST/PUT… |
| url | ✅ | Endpoint (có thể chứa @param trong query) |
| headers | ❌ | Headers riêng cho API |
| body | ❌ | Payload gửi lên (có thể chứa @param) |
| fields | ❌ | Schema để toolkit tự sinh UI nhập liệu |
| responseSource | ❌ | Nếu response bọc array trong object (vd products, rows) |
| mapping.columns | ✅ | Mapping key → header để đổ vào sheet |
4.2. API đơn giản (GET không filter)
{
id: "all_students",
name: "Tất cả sinh viên",
method: "GET",
url: "http://127.0.0.1:8000/users",
mapping: {
columns: [
{ key: "id", header: "ID" },
{ key: "name", header: "Họ tên" },
],
},
}4.3. API có filter (body.filters + @param)
{
id: "products",
name: "Sản phẩm",
method: "GET",
url: "https://dummyjson.com/products/search",
responseSource: "products",
body: {
filters: {
q: "@q", // <-- placeholder
},
},
fields: {
q: {
label: "Từ khóa tìm kiếm",
type: "text",
placeholder: "Nhập từ khóa...",
pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
},
},
mapping: {
columns: [
{ key: "id", header: "ID" },
{ key: "title", header: "Tên sản phẩm" },
{ key: "price", header: "Giá" },
],
},
}Flow chạy:
- User chọn “Sản phẩm”
- Toolkit render input
q - User nhập
q = phone - Toolkit tạo context
{ q: "phone" } - Resolve
@q→"phone"trongbody.filters.q - Gọi API
- Lấy list ở
responseSource="products" - Insert vào sheet theo
mapping.columns
5. Fields: Tự sinh UI input (text/select/date/range) + validate
fields là phần quan trọng để sidebar biết cần hiển thị input nào.
5.1. Các type thường dùng
Text
q: { label: "Từ khóa", type: "text", placeholder: "Nhập..." }Number (tuỳ dự án)
limit: { label: "Giới hạn", type: "number", min: 1, max: 100 }Date
fromDate: { label: "Từ ngày", type: "date" }
toDate: { label: "Đến ngày", type: "date" }Select – dữ liệu tĩnh
status: {
label: "Trạng thái",
type: "select",
options: {
source: "static",
items: [
{ value: "", label: "-- Tất cả --" },
{ value: "active", label: "Đang học" },
{ value: "graduated", label: "Tốt nghiệp" },
],
},
}Select – load options từ API
termId: {
label: "Học kỳ",
type: "select",
required: true,
options: {
source: "api",
url: "http://127.0.0.1:8000/terms",
valueKey: "id",
labelKey: "name",
},
}Range – 2 input → 2 biến
amountRange: {
label: "Khoảng số tiền (VNĐ)",
type: "range",
rangeKeys: ["amountFrom", "amountTo"],
placeholder: "Số tiền",
}5.2. Validate (cách hiểu)
required: true→ bắt buộc nhậppattern(nếu bạn dùng) → regex validatemin/max(number) → validate giá trị
6. Cơ chế @param: Ghép URL / body để lọc dữ liệu
Toolkit resolve biến theo nguyên tắc:
- Mọi chuỗi dạng
@xxxsẽ được thay thế bằng:- giá trị từ
fieldsuser nhập (ưu tiên), hoặc - giá trị trong
globalContext, hoặc - nếu không có → coi là thiếu tham số
- giá trị từ
6.1. Ghép URL query
Ví dụ:
url: "http://127.0.0.1:8000/grades?courseId=@courseId&semester=@semester"Khi user nhập courseId=101, semester=2024-1
→ URL thực tế sẽ thành:
http://127.0.0.1:8000/grades?courseId=101&semester=2024-16.2. Ghép body.filters
Ví dụ:
body: {
filters: {
keyword: "@keyword",
status: "@status",
fromDate: "@fromDate",
toDate: "@toDate"
}
}Khi user nhập các field tương ứng → toolkit thay vào body trước khi gọi API.
7. FAQ (Hỏi – Đáp) + ví dụ trả lời sẵn
7.1. “Nếu API có tham số đầu vào để lọc thì sao?”
Không sao. Bạn cấu hình body.filters (hoặc URL query) và đặt @param đúng tên.
Sau đó cấu hình fields để toolkit hiển thị UI nhập tham số.
Ví dụ kiểu “báo cáo”:
{
id: "student_report",
name: "Báo cáo sinh viên",
method: "POST",
url: "http://127.0.0.1:8000/reports/students",
body: {
filters: {
keyword: "@keyword",
termId: "@termId",
departmentId: "@departmentId",
status: "@status",
fromDate: "@fromDate",
toDate: "@toDate",
// range example
amountFrom: "@amountFrom",
amountTo: "@amountTo",
},
},
fields: {
keyword: { label: "Từ khóa", type: "text" },
termId: {
label: "Học kỳ",
type: "select",
required: true,
options: { source: "api", url: "http://127.0.0.1:8000/terms", valueKey: "id", labelKey: "name" },
},
status: {
label: "Trạng thái",
type: "select",
options: {
source: "static",
items: [
{ value: "", label: "-- Tất cả --" },
{ value: "active", label: "Đang học" },
{ value: "graduated", label: "Tốt nghiệp" },
],
},
},
amountRange: {
label: "Khoảng số tiền (VNĐ)",
type: "range",
rangeKeys: ["amountFrom", "amountTo"],
placeholder: "Số tiền",
},
fromDate: { label: "Từ ngày", type: "date" },
toDate: { label: "Đến ngày", type: "date" },
},
mapping: {
columns: [
{ key: "id", header: "ID" },
{ key: "name", header: "Tên" },
{ key: "balance", header: "Số dư" },
],
},
}Chốt lại (nhớ 3 bước):
- Liệt kê tham số lọc trong
body.filtershoặc URL query - Gắn
@paramvào đúng chỗ bạn muốn thay - Khai
fields.paramđể có UI nhập và validate
7.2. “API trả về { products: [...] } chứ không phải array thì sao?”
Nhiều trường hợp response trả về là data, rows, items,... thì ta đặt responseSource: "rows" (hoặc key chứa mảng).
7.3. “Làm dropdown select lấy options từ API?”
Trong field select:
options: {
source: "api",
url: "http://.../terms",
valueKey: "id",
labelKey: "name",
}8. Triển khai trên mọi loại dự án (Front framework & Fullstack)
Cách triển khai nhanh nhất là làm theo y như index.html mẫu ở mục 1, chỉ cần import theo unpkg là có thể sử dụng được
8.1. HTML thuần / CMS / Fullstack render server
- Render trang HTML
- Nhúng script CDN
- Set
window.USER_UNIVER_CONFIG - Gọi
UniverToolkit.mount()
8.2. React/Vue/Angular (SPA)
npm i- Import CSS
- Set config trong runtime (hoặc fetch config từ backend rồi gán)
mount()trong lifecycle (useEffect,onMounted,ngAfterViewInit)
8.3. Next.js / Nuxt (SSR)
- Chỉ mount phía client
- Không gọi mount ở server
9. Troubleshooting nhanh
UniverToolkit not loaded- Sai thứ tự load script CDN
- UMD URL sai
- Toolkit load trước jQuery/Select2
Không hiện dropdown/select2
- Thiếu
select2.min.css - Thiếu
jqueryhoặcselect2script
- Thiếu
Chọn API nhưng không hiện input
- Chưa khai
fields - Bạn dùng
@paramnhưng chưa tạofields.param(hoặc không cóglobalContext)
- Chưa khai
API gọi ok nhưng sheet rỗng
- Sai
responseSource mapping.columns.keykhông khớp dữ liệu trả về
- Sai
License
MIT
