npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@phuoc2409/univer-toolkit

v1.0.18

Published

Lightweight wrapper for Univer spreadsheet with templates, tags, and data API support

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 @param trong 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

1.1. Load đúng thứ tự (rất quan trọng)

  1. React + ReactDOM (Univer UMD cần)
  2. RxJS
  3. jQuery + Select2 (phải trước toolkit)
  4. Univer presets UMD + locales
  5. CSS của Univer
  6. CSS của Toolkit
  7. Toolkit UMD
  8. window.USER_UNIVER_CONFIG = {...}
  9. UniverToolkit.mount()

1.2. HTML mẫu (copy chạy ngay)

Bắt buộc có 2 div: #sidebar#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-toolkit

Import 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 @paramkhô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:

  1. User chọn “Sản phẩm”
  2. Toolkit render input q
  3. User nhập q = phone
  4. Toolkit tạo context { q: "phone" }
  5. Resolve @q"phone" trong body.filters.q
  6. Gọi API
  7. Lấy list ở responseSource="products"
  8. 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ập
  • pattern (nếu bạn dùng) → regex validate
  • min/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 @xxx sẽ được thay thế bằng:
    • giá trị từ fields user nhập (ưu tiên), hoặc
    • giá trị trong globalContext, hoặc
    • nếu không có → coi là thiếu tham số

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-1

6.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):

  1. Liệt kê tham số lọc trong body.filters hoặc URL query
  2. Gắn @param vào đúng chỗ bạn muốn thay
  3. 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 jquery hoặc select2 script
  • Chọn API nhưng không hiện input

    • Chưa khai fields
    • Bạn dùng @param nhưng chưa tạo fields.param (hoặc không có globalContext)
  • API gọi ok nhưng sheet rỗng

    • Sai responseSource
    • mapping.columns.key không khớp dữ liệu trả về

License

MIT