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

enabled là Formily 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)Có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 pageActive và tabActive 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 ​