Bỏ qua, đến nội dung

@digiforce-nc/ui-core

Engine UI cốt lõi - widget system, reactive context, behavior execution, design mode, và JavaScript sandbox.

typescript
import { UiCore, UiWidget, observer } from '@digiforce-nc/ui-core';

Tổng quan

Hãy hình dung ui-core như hệ điều hành cho giao diện:

  • UiCore = kernel - điều phối widget, context, event, và behavior.
  • UiWidget = process - mỗi block/component trên UI là một widget instance với state riêng.
  • UiContext = bộ nhớ chia sẻ - hệ thống property động (computed, cached, observable) xuyên suốt component tree.
  • UiConfig = bảng điều khiển - quản lý design mode (bật/tắt chỉnh sửa giao diện).
  • UiBehavior = automation - logic tự động chạy trước/sau khi widget render hoặc khi event xảy ra.
  • Observer = reactive wrapper - tự động re-render component khi dữ liệu thay đổi.
  • RunJS = sandbox - chạy JavaScript tùy chỉnh trong môi trường cách ly an toàn.

Vị trí trong hệ thống

Dependencies chính

PackageVai trò
@formily/reactiveObservable state, define, observable
@formily/antd-v5UI components cho settings form
@digiforce-nc/sdkAPI client base
@digiforce-nc/sharedUtilities dùng chung
ahooksReact hooks (useRequest, etc.)
slateRich text editor engine
sesSecure ECMAScript sandbox
pinoLogging

UiCore - engine trung tâm

UiCore là class trung tâm, quản lý toàn bộ widget, action, event, behavior, và context.

Constructor

typescript
const uiCore = new UiCore();

Constructor không nhận tham số - khởi tạo theo thứ tự:

Thuộc tính và API chính

NhómAPIMô tả
ContextcontextUiCoreContext (lazy - tạo lần đầu truy cập)
ConfiguiConfigDesign mode, toolbar, settings dialog
RenderreactViewReact mount / render system
BehaviorexecutorUiBehaviorExecutor - chạy automation
LoggerloggerPino logger instance
EmitteremitterEvent emitter (sync + async)

Widget class registry

UiCore quản lý class registry - đăng ký widget class trước, rồi tạo instance khi cần:

typescript
// Đăng ký widget class
uiCore.registerUiWidgets({ TableWidget, FormWidget, ChartWidget });

// Đăng ký lazy loader (chỉ load khi cần)
uiCore.registerUiWidgetLoaders({
  KanbanWidget: () => import('./widgets/KanbanWidget'),
});

// Lấy class
const TableClass = uiCore.getUiWidgetClass('TableWidget');
const KanbanClass = await uiCore.getUiWidgetClassAsync('KanbanWidget');

// Lọc theo parent class
const dataWidgets = uiCore.getChildClassesOf(DataWidget);
MethodMô tả
registerUiWidgets(map)Đăng ký widget classes (eager)
registerUiWidgetLoaders(map)Đăng ký lazy loaders (dynamic import)
getUiWidgetClass(name)Lấy class (sync - chỉ eager)
getUiWidgetClassAsync(name)Lấy class (async - hỗ trợ lazy)
getUiWidgetClasses()Tất cả classes đã đăng ký
getChildClassesOf(parent)Lọc classes kế thừa từ parent
preloadUiWidgetLoaders()Tải trước tất cả lazy widgets

Widget instance management

typescript
// Tạo widget instance
const table = uiCore.createUiWidget({
  use: 'TableWidget',
  uid: 'table-1',
  props: { rowKey: 'id', bordered: true },
});

// Tìm widget
const widget = uiCore.getUiWidget('table-1');

// Tìm trong cả view stack (global)
const widget = uiCore.getUiWidget('table-1', true);

// Xóa widget
uiCore.removeUiWidget('table-1');

// Lưu widget (persist qua repository)
await uiCore.saveUiWidget(widget);

// Duplicate, replace, move
await uiCore.duplicateUiWidget(widget, targetParent);
await uiCore.replaceUiWidget(widget, newWidgetOptions);
await uiCore.moveUiWidget(widget, newParent, index);
MethodMô tả
createUiWidget(options)Tạo instance mới
createUiWidgetAsync(options)Tạo async (hỗ trợ lazy class)
getUiWidget(uid, global?)Tìm instance theo UID
forEachUiWidget(callback)Duyệt tất cả instances
removeUiWidget(uid)Xóa instance
loadUiWidget(data)Load từ serialized data
saveUiWidget(widget)Persist qua repository
destroyUiWidget(widget)Hủy hoàn toàn (cleanup)
duplicateUiWidget(widget, parent)Nhân bản widget
replaceUiWidget(widget, options)Thay thế bằng widget mới
moveUiWidget(widget, parent, idx)Di chuyển vị trí

Action & Event registry

typescript
// Đăng ký action (logic có thể gọi)
uiCore.registerActions({
  refreshData: {
    label: 'Refresh data',
    handler: async (ctx) => { await ctx.resource.refresh(); },
  },
});

// Đăng ký event (sự kiện widget phát ra)
uiCore.registerEvents({
  rowClick: {
    label: 'Row clicked',
    schema: { rowId: { type: 'string' } },
  },
});

View stack

UiCore hỗ trợ view stack cho navigation giữa các view - mỗi view có UiCore riêng, liên kết qua previousUiCore / nextUiCore:

Khi gọi getUiWidget(uid, true), hệ thống tìm từ view hiện tại ngược lại qua stack.

Resource system

typescript
// Đăng ký resource types
uiCore.registerResources({
  CustomResource: MyCustomResource,
});

// Tạo resource instance
const resource = uiCore.createResource('APIResource', {
  url: '/api/users',
});

UiConfig - design mode

UiConfig quản lý chế độ thiết kế giao diện - cho phép user chỉnh sửa layout, cấu hình widget trực tiếp trên browser.

State machine

MethodHành vi
enable()Preload tất cả widget loaders → enabled = true
disable()enabled = false (no-op nếu forceEnabled)
forceEnable()Preload → set forced flag → enabled = true
forceDisable()Clear forced flag → enabled = false

enabledFormily observable - mọi component observe sẽ tự re-render khi toggle.

Đồng bộ với context

typescript
uiCore.context.defineProperty('uiConfigEnabled', {
  get: () => this.enabled,
  cache: false,  // Luôn đọc giá trị mới nhất
});

Widget kiểm tra context.uiConfigEnabled để quyết định hiển thị toolbar, settings menu, hay widget đang ẩn.

Toolbar

UiConfig quản lý toolbar items (nút trên thanh công cụ design mode):

typescript
uiConfig.addToolbarItem({
  key: 'my-button',
  sort: 100,
  Component: MyToolbarButton,
});

// Mặc định có sẵn: settings-menu (DefaultUiConfigIcon)
const items = uiConfig.getToolbarItems(); // sorted by `sort`

Component & scope registry

UiConfig quản lý Formily components và scopes cho settings form:

typescript
// Đăng ký components cho settings UI
uiConfig.registerComponents({
  ColorPicker: MyColorPicker,
});

// Lazy loading
uiConfig.registerComponentLoaders({
  RichEditor: () => import('./RichEditor'),
});

// Đăng ký scope (biến dùng trong schema expression)
uiConfig.registerScopes({
  t: uiCore.translate,
  customHelper: myHelper,
});

Settings dialog

uiConfig.open(options) mở dialog cấu hình cho widget:


UiWidget - widget instance

Mỗi block, form, table, chart trên UI là một UiWidget instance - có state riêng, lifecycle, và reactive rendering.

Cấu trúc

Observable state

Widget sử dụng Formily define để biến thuộc tính thành reactive:

typescript
define(this, {
  hidden: observable,
  props: observable,          // shallow
  childWidgets: observable,   // shallow
  stepParams: observable,
});

Khi props, stepParams, hay hidden thay đổi → component tự re-render (nhờ observer wrapper).

Tạo widget - luồng chi tiết

Render - reactive wrapping

Mỗi widget có method render() trả về React element. Khi tạo, setupReactiveRender() tự động wrap method này bằng observer:

Observer wrapper đảm bảo:

  • Auto re-render khi props, stepParams, hidden thay đổi.
  • Hidden widget không render (tiết kiệm tài nguyên), trừ khi đang ở design mode (hiện mờ để user thấy).
  • Lifecycle events: onMount, onUnmount và emit uiWidget:mounted, uiWidget:unmounted.

Events & behaviors

Widget phát ra event → behavior executor chạy automation:

typescript
// Phát event
widget.dispatchEvent('rowClick', { rowId: '123' });

// beforeRender - event đặc biệt, chạy trước mỗi lần render
widget.dispatchEvent('beforeRender');
// → sequential, cached (không chạy lại nếu params không đổi)

// Apply behavior thủ công
await widget.applyBehavior('fetchData', { url: '/api/users' });

// Force re-render (xóa cache beforeRender)
widget.rerender();

Widget context

Mỗi widget có UiWidgetContext riêng - delegate lên uiCore.context:

Khi đọc property từ UiWidgetContext, nếu không tìm thấy → tự động tìm trong delegate (UiCoreContext).


UiContext - hệ thống context thông minh

UiContext là hệ thống property động mạnh mẽ nhất trong ui-core - mỗi property có thể static, computed, cached, observable, hoặc async.

Ý tưởng

Thay vì React context truyền thống (một object cố định), UiContext cho phép define property bất kỳ lúc nào, với getter/setter, caching, và reactivity:

typescript
const ctx = uiCore.context;

// Static value
ctx.defineProperty('appName', { value: 'My App' });

// Computed (cached)
ctx.defineProperty('currentUser', {
  get: async () => {
    const res = await api.request({ url: 'auth:check' });
    return res.data;
  },
  cache: true,       // Chỉ gọi API lần đầu, sau đó dùng cache
  observable: true,   // Re-render component khi giá trị thay đổi
});

// Computed (không cache)
ctx.defineProperty('uiConfigEnabled', {
  get: () => uiConfig.enabled,
  cache: false,  // Luôn đọc giá trị mới nhất
});

// Define once (không ghi đè)
ctx.defineProperty('immutableProp', {
  value: 42,
  once: true,  // Lần define sau sẽ bị bỏ qua
});

PropertyOptions

typescript
interface PropertyOptions {
  value?: any;              // Giá trị static
  get?: (ctx) => T;         // Getter (sync hoặc async)
  cache?: boolean;          // Cache kết quả (mặc định: true)
  observable?: boolean;     // Dùng observable cache (trigger re-render)
  once?: boolean;           // Chỉ define 1 lần
  meta?: object;            // Metadata cho variable picker UI
  info?: object;            // API documentation
}

Caching - 3 chế độ

Chế độCacheObservableDùng cho
cache: falseKhôngKhôngGiá trị thay đổi liên tục (uiConfigEnabled)
cache: true_cache (plain)KhôngDữ liệu tĩnh hoặc ít thay đổi
cache: true, observable: true_observableCache (reactive)Dữ liệu cần trigger re-render (themeToken)

Async getter - deduplication

Khi getter trả về Promise, UiContext deduplicate - nhiều component đọc cùng lúc chỉ gọi getter 1 lần:

Delegation chain

UiContext hỗ trợ delegation - khi property không tìm thấy, tìm tiếp trong delegate:

Thứ tự tìm:

  1. Property trên chính context
  2. Depth-first search qua _delegates[]

Proxy implementation

UiContext dùng ES Proxy để intercept mọi property access:

typescript
// Bên trong constructor
return createProxy(); // → new Proxy(target, handler)

// handler.get(target, key):
// 1. Own property trên instance → trả về
// 2. _props[key] → _getOwnProperty(key)
// 3. _methods[key] → _getOwnMethod(key)
// 4. Tìm trong _delegates (depth-first)

UiCoreContext - context mặc định

UiCoreContext kế thừa UiContext, tự động define nhiều property hữu ích:

Properties có sẵn

PropertyKiểuMô tả
uiCoreUiCoreReference đến UiCore
dataSourceManagerDataSourceManagerQuản lý data source
sqlfunctionSQL query helper
tfunctioni18n translation
resolveJsonTemplatefunctionTemplate engine ( resolution)
getVarfunctionĐọc biến từ context path
tokenstringAuth token
rolestringCurrent role
urlSearchParamsobjectURL query params
loggerLoggerPino logger
authobjectAuth utilities
dateobjectDate utilities
aclobjectACL helper
getAction / getActionsfunctionLấy registered actions
getEventsfunctionLấy registered events
runActionfunctionChạy action
createResourcefunctionTạo resource instance
requireAsync / importAsyncfunctionDynamic import
createJSRunnerfunctionTạo RunJS sandbox instance

Properties do host inject

Những property sau được Application (host) inject sau khi mount:

PropertyInjected byMô tả
appApplication constructorApplication instance
apiApplication constructorAPIClient
routerApplication constructorReact Router instance
routeApplication constructorCurrent route info
locationApplication constructorBrowser location (observable)
viewerUiCoreGlobalsContextProviderDialog/drawer/page opener
modalUiCoreGlobalsContextProviderAntd Modal instance
messageUiCoreGlobalsContextProviderAntd Message instance
notificationUiCoreGlobalsContextProviderAntd Notification instance
themeTokenUiCoreGlobalsContextProviderAntd theme token (observable)
antdConfigUiCoreGlobalsContextProviderAntd ConfigProvider

Observer - reactive rendering

Vấn đề

React component bình thường không tự re-render khi Formily observable thay đổi. Cần wrapper.

Giải pháp: observer()

typescript
import { observer } from '@digiforce-nc/ui-core';

const MyComponent = observer(() => {
  const widget = useUiWidget();
  // Tự động re-render khi widget.props thay đổi
  return <div>{widget.props.title}</div>;
});

Cơ chế bên trong

observer của ui-core wrap @formily/reactive-react observer với custom scheduler thông minh:

Tại sao cần custom scheduler? Khi user chuyển tab hoặc navigate sang trang khác, component không visible - re-render lúc này lãng phí và có thể gây infinite loop. Scheduler kiểm tra pageActivetabActive trước khi cho phép update.


UiWidgetRenderer - render pipeline

UiWidgetRenderer là React component render một widget instance theo pipeline đầy đủ:

beforeRender - behavior đặc biệt

beforeRender là event chạy trước mỗi lần render, dùng để fetch data, tính toán, chuẩn bị state:

typescript
// Behavior đăng ký trên widget
widget.behaviorRegistry.add('fetchUsers', {
  event: 'beforeRender',
  handler: async (ctx) => {
    const users = await ctx.api.resource('users').list();
    ctx.uiWidget.setProps({ dataSource: users });
  },
});

Đặc điểm:

  • Sequential - các behavior chạy tuần tự (không song song).
  • Cached - nếu stepParams không đổi, không chạy lại.
  • Force rerun - gọi widget.rerender() để xóa cache và chạy lại.

UiBehavior - automation engine

Behavior là logic tự động gắn với widget event:

Hai cấp behavior registry

CấpRegistryPhạm vi
GlobalGlobalUiBehaviorRegistryTất cả instance của widget class
InstanceInstanceUiBehaviorRegistryChỉ widget instance cụ thể

dispatchEvent flow


Emitter - event system

Emitter nhẹ, hỗ trợ cả sync và async:

typescript
import { Emitter } from '@digiforce-nc/ui-core';

const emitter = new Emitter();

// Đăng ký listener
emitter.on('dataChanged', (data) => {
  console.log('Data:', data);
});

// Phát event (sync)
emitter.emit('dataChanged', { id: 1 });

// Phát event (async - chờ tất cả listener hoàn tất)
await emitter.emitAsync('dataChanged', { id: 1 });

// Tạm dừng events
emitter.setPaused(true);
// ... events bị giữ lại ...
emitter.setPaused(false);
// → events được phát ra
EmitterVị tríEvents tiêu biểu
uiCore.emitterUiCore leveluiWidget:created, uiWidget:destroyed
uiWidget.emitterWidget leveluiWidget:mounted, uiWidget:unmounted
uiConfig.#emitterUiConfig (private)beforeOpen

RunJS - JavaScript sandbox

Cho phép user viết logic JavaScript tùy chỉnh chạy trong môi trường cách ly (dùng ses - Secure ECMAScript):

typescript
import { compileRunJs, registerRunJSLib } from '@digiforce-nc/ui-core';

// Đăng ký thư viện cho sandbox
registerRunJSLib('lodash', _);
registerRunJSLib('dayjs', dayjs);

// Biên dịch code thành function
const fn = compileRunJs(`
  const name = lodash.capitalize(input.name);
  const date = dayjs().format('YYYY-MM-DD');
  return { name, date };
`);

// Chạy với context
const result = fn({ input: { name: 'john' } });
// → { name: 'John', date: '2026-04-08' }
APIMô tả
compileRunJs(code)Biên dịch JS string thành callable function
registerRunJSLib(name, lib)Đăng ký thư viện cho sandbox access
JSRunnerClass quản lý vòng đời sandbox instance
createJSRunnerFactory (trên context)

Providers - kết nối vào React tree

Provider hierarchy

UiCoreProvider

Cung cấp UiCore instance cho toàn bộ React tree:

tsx
<UiCoreProvider uiCore={uiCore}>
  {children}
</UiCoreProvider>

Bên trong wrap UiContextProvider với uiCore.context.

UiCoreGlobalsContextProvider

Phải nằm dưới Antd App component - sử dụng App.useApp() để lấy modal, message, notification, rồi inject vào context:

typescript
// Bên trong UiCoreGlobalsContextProvider:
context.defineProperty('viewer', {
  cache: false,
  get: () => new UiViewer(/* dialog, drawer, page, popover */),
});

context.defineProperty('modal', { value: antdApp.modal });
context.defineProperty('message', { value: antdApp.message });
context.defineProperty('notification', { value: antdApp.notification });

context.defineProperty('themeToken', {
  observable: true,
  cache: true,
  get: () => antdThemeToken,
});

UiWidgetProvider

Wrap mỗi widget, cung cấp uiWidget.context (delegate lên uiCore.context):

tsx
<UiWidgetProvider uiWidget={widget}>
  {/* Widget render output */}
</UiWidgetProvider>

Hooks

HookMô tả
useUiCore()Lấy UiCore instance (từ UiCoreProvider)
useUiCoreContext()Shortcut cho useUiCore().context
useUiContext()Lấy UiContext hiện tại (core hoặc widget level)
useUiWidget()Lấy UiWidget instance (từ UiWidgetProvider)
useUiWidgetContext()Lấy widget context
useUiWidgetById(uid)Tìm hoặc tạo widget theo UID
useApplyAutoBehaviors()Chạy beforeRender behaviors (dùng trong renderer)
useUiConfigContext()Context trong settings dialog
useUiStep()Step context trong UiConfig settings flow
useUiCoreView()Lấy current view từ context

Ví dụ sử dụng hooks

tsx
import { useUiCore, useUiWidget, useUiContext, observer } from '@digiforce-nc/ui-core';

const MyBlock = observer(() => {
  const uiCore = useUiCore();
  const ctx = useUiContext();

  // Đọc property từ context (có thể async, cached, observable)
  const user = ctx.currentUser;
  const theme = ctx.themeToken;

  return (
    <div style={{ color: theme?.colorPrimary }}>
      Hello, {user?.name}
    </div>
  );
});

const WidgetContent = observer(() => {
  const widget = useUiWidget();

  return (
    <div>
      <h3>{widget.props.title}</h3>
      {widget.childWidgets.map(child => (
        <UiWidgetRenderer key={child.uid} uiWidget={child} />
      ))}
    </div>
  );
});

View scoped UiCore

Cho phép tạo UiCore riêng cho mỗi view hoặc block, tránh xung đột state:

typescript
import { createViewScopedUiCore, createBlockScopedUiCore } from '@digiforce-nc/ui-core';

// UiCore riêng cho một view (chia sẻ state giữa blocks trong view)
const viewCore = createViewScopedUiCore('dashboard-view');

// UiCore cô lập cho một block (state hoàn toàn độc lập)
const blockCore = createBlockScopedUiCore('chart-block-1');
FactoryPhạm viUse case
createViewScopedUiCore(viewId)Toàn viewDashboard có nhiều block chia sẻ filter
createBlockScopedUiCore(blockId)Một blockWidget embed độc lập

Kiến trúc tổng hợp - luồng render widget


Đọc thêm