@enonic/ui-utils

中文文档
0.7.0发布于 2026/11/04Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 2336

Framework-free helpers for Enonic UIs: the request transport, phrase lookup, principal keys, task polling and the admin events hub

原始使用文档
文档目录3 节
  1. What is here
  2. The theme
  3. The notifications

@enonic/ui-utils

Everything useful without a view layer: the request transport, the i18n core, the values an XP form edits with the helpers that parse and format them, principal keys, a task followed to its end, the browser side of the admin events hub, the theme the admin applications share, and the store behind a toast stack.

Nothing here imports a component or a framework, and nothing reads state it did not create. That is the package's whole value — it can be used from a store, a worker, a test, or code that has no DOM at all, and a function's result depends on its arguments alone. Phrases come from the consumer, which fetches them from its own server and passes them in. The exceptions read the environment by nature: the theme reads its storage and the OS preference through the window it is handed, the page's by default; documentVisibility reads the document it is handed; parseRelativeTime reads the clock unless it is handed now, and dateFromTime answers a time of today; the admin events hub imports its client from the url it is given.

pnpm add @enonic/ui-utils              # the errors, the i18n core and the values, no peers
pnpm add @enonic/ui-utils neverthrow   # plus the transport, from '@enonic/ui-utils/request'

The hub client is its own entry too, @enonic/ui-utils/admin-events, with no peer: it is the one code here that loads a module from a runtime url — the client XP serves under the hub's api. So is the XP menu, @enonic/ui-utils/admin-menu: the one code here that writes to document.head, where it puts the loader XP serves as the menu-loader extension of com.enonic.xp.app.main. And so is the task polling, @enonic/ui-utils/task, for the transport's reason: it answers with its ResultAsync.

The root entry resolves nothing outside the package. The transport is its own entry, @enonic/ui-utils/request, and the only code that needs neverthrow: every request answers with its ResultAsync, so a consumer of the transport installs it and shares one copy of the class. neverthrow is therefore an optional peer.

What is here

ExportWhat it does
AppErrorthe base of every failure the package reports — name is a literal, cause rides the native Error option
RequestErrora response with an error status: status, and the body's message when the body is JSON and has one, else the status text
RequestAbortedErrora request its caller gave up on through signal
requestJson<T>(url, options?) *fetch a JSON body as ResultAsync<T, AppError>; options carries method, a JSON-serialized body, headers and signal
requestOptionalJson<T>(url, options?) *the same, with a 204 or an empty or null body resolving to undefined
localize(phrases, key, ...values)the phrase behind key with {0}-style placeholders filled by position; a missing key is #key#
Translatewhat an application hands @enonic/ui's I18nProvider: the key and the package's English in, the application's text or that English out
passthroughthe Translate of an application that translates nothing
fromPhrases(read)a Translate over a phrase map, read at call time
fromLookup(has, get)a Translate over a has/get pair, as a Messages-style source offers
bindPhrases(translate, fragment)the typed t of one fragment, for a store or anything else outside a render
mergePhrases(fragments)one catalogue from a package's fragments, keys kept as a type; a key declared twice throws
comparePhrases(catalog, bundle)the catalogue keys a bundle lacks, and those whose placeholder set differs
LocalDate, LocalTime, LocalDateTimeXP's zone-less date, time and date-time: fromString and fromDate in, toString and toDate out, equals by value
DateTimeXP's instant, 2015-04-17T06:00:30.250Z; its fields are UTC
GeoPoint, Reference, BinaryReference, Linkthe other values a property holds, each its string and an equals
parseTime, parseDateTime06:00[:30[.250]], and 2015-04-17 joined with it — undefined for anything else, a rolled-over day included
formatDate, formatTimethe same strings from a Date or from hours, minutes and seconds
pad, dateFromTimethe small helpers behind them
parseRelativeTime(expression, now?)the Date a form's default names: now, +1d, -2h +30m; isRelativeTime says whether a string is one
userKey, groupKey, roleKeya PrincipalKey from its parts
isPrincipalKey(value), parsePrincipalKey(value)whether a string is a key, and the key or a throw
principalType, principalId, idProviderOfa key's parts back; a role has no id provider
toPrincipal(record)a PrincipalRecord as the Principal its key says, type filled in — once, where the response is read: requestJson<PrincipalRecord[]>(url).map((rs) => rs.map(toPrincipal))
isUserRecord, isGroupRecord, isRoleRecorda record told apart by its key, where only the key and the name are needed
principalPath(key)where the Users application shows it: /roles/<id>, /<idProvider>/users/<id>
isSystemPrincipal(key)su, anonymous, the system. roles and the project roles
SUPER_USER_KEY, ANONYMOUS_USER_KEY, *_ROLE, SYSTEM_ID_PROVIDERthe principals XP defines: role:system.admin, role:system.everyone, role:cms.admin and the rest
pollTask(tasksUrl, taskId, options?) §<tasksUrl>/<taskId> read every intervalMs until FINISHED, its TaskInfo, or FAILED, an error; onProgress, signal and readResult
taskProgressPercent(task) §current of total as a whole percentage, 0 while the total is unknown
TaskFailedError §a task that ended in FAILED: message is what it reported, task its final info
createAdminEvents(hubUrl) †the hub at hubUrl: connect() loads its client once; subscribeTopic, subscribeConnection, subscribeSessionEnd until unsubscribed
createTopicReaction(options) †one topic's messages parsed and gathered into a batch, dropped while visible is false with a refresh on reveal, and a refresh on loss
documentVisibility() †the page's visibilityState as the Readable<boolean> a reaction takes, for an application no host hands one
loadAdminMenu(loaderUrl) ‡the XP menu's loader on the page once, auto-mount off, and its api — mount(options) answering the <xp-menu> host, and unmount()
mountAdminMenu(loaderUrl, options?) ‡the loader where the page has none, then one mount; autoOpen and hideButtonWhenOpen — the menu follows the stored theme on its own
readTheme(), writeTheme(theme)the theme the user picked, system where none is stored; writing stores it and tells the page
subscribeTheme(listener)the picked theme changing — on this page, in another tab, removed — until unsubscribed
readResolvedTheme(), subscribeResolvedTheme(listener)the theme to paint, system answered by the OS, and its changes whether the user switched or the OS did
THEME_STORAGE_KEY, THEME_CHANGE_EVENTthe localStorage entry and the window event the theme travels by
createNotifications()a toast stack's store: notify(tone, message, options?) answering its dismiss, dismiss, dismissOwned, clear, pause, resume, and notifications, the Readable of what is on screen
NotificationOptions, NotificationDatawhat a toast is raised with — the host contract's autoHide and lifetimeMs, plus actions, owner and onClose — and one entry on the stack
admit, remove, findDuplicate, lifetimeFor, isUrgentthe pure pieces of the policy, with VISIBLE_LIMIT, SHORT_LIFETIME_MS, LONG_LIFETIME_MS and MAX_LIFETIME_MS, for a host that composes its own
* From @enonic/ui-utils/request. † From @enonic/ui-utils/admin-events. ‡ From @enonic/ui-utils/admin-menu. § From @enonic/ui-utils/task.

The theme

XP's admin applications — Settings, Content Studio, the menu — are released separately and share one theme: the enonic:theme entry in localStorage, light, dark or system. These functions are that contract, so that no application spells it out by hand.

  • The entry is the truth. An event is only a cue to read it again: what a listener hears is what readTheme answers, and a missing or invalid entry is system.
  • Every page hears a switch. storage fires only in the other tabs, so writeTheme also dispatches THEME_CHANGE_EVENT on the window, which every bundle on the page listens to.
  • Listeners hear changes. Never on subscribe, and not for a write that leaves the answer as it was; each subscribe* returns its unsubscribe, and the caller decides how long it listens.
  • Blocked storage reads as system, and a write warns.

Content Studio's pre-paint script reads enonic:theme inline, before any bundle loads, and is the one copy of the key outside this package.

The notifications

createNotifications() is the store behind Host.notify, and behind an application's own toasts: what NotificationList from @enonic/ui-kit renders. It is a factory, not an instance: the application creates one, exports it from its own module, and hands it to the list and to every Host it creates for a mount — a toast an extension raises through host.notify lands in the host's stack, never in a second one. The policy it keeps:

  • One just like it is not shown twice. Same tone, message and owner is the toast already up, whose dismiss comes back; its lifetime is not restarted, and the duplicate's own options — its onClose among them — are dropped, since it was never shown. Two owners saying the same thing each keep their own.
  • Three on screen, the rest queued in the order raised; a promoted toast starts its full lifetime then, not when it was raised.
  • A warning or an error stays thirty seconds, the rest five, unless lifetimeMs says otherwise or autoHide is off; a lifetime past what setTimeout can wait, MAX_LIFETIME_MS, is held there. pause and resume hold and continue what is left — the list calls them on hover and focus.
  • owner is what dismissOwned takes down when a mount is revoked; onClose is called once when a toast leaves, whichever way, and one that throws is reported and costs no other its close.
export const notifications = createNotifications();

const dismiss = notifications.notify('success', 'Saved');
notifications.notify('info', 'Moved to /site/news', {
  actions: [{ label: '/site/news', onClick: () => select('/site/news') }],
});
notifications.notify('error', 'Live updates unavailable', { autoHide: false, owner: 'app:users' });
notifications.dismissOwned('app:users');

Part of the Enonic UI Toolkit.

版本与发布记录

@enonic/ui-utils 共发布 8 个版本,首次发布于 2026/09/07;过去 12 个月发布了 8 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.7.0latest2026/11/049 天前
0.6.02026/10/2222 天前
0.5.22026/10/1530 天前
0.5.12026/10/1430 天前
0.5.02026/10/091 个月前
0.4.02026/09/281 个月前

关于 @enonic/ui-utils 的常见问题

@enonic/ui-utils 是什么?

Framework-free helpers for Enonic UIs: the request transport, phrase lookup, principal keys, task polling and the admin events hub

如何安装 @enonic/ui-utils?

在项目目录运行 npm install @enonic/ui-utils,或使用 pnpm add / yarn add / bun add。

@enonic/ui-utils 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@enonic/ui-utils 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@enonic/ui-utils 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.12.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@enonic/ui-utils 可以商用吗?

@enonic/ui-utils 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@enonic/ui-utils 的下载量有多少?

根据 npm 官方下载统计,@enonic/ui-utils 在 2026/10/14 – 2026/11/12 这 30 天内被下载 2,336 次,日均约 78 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@enonic/ui-utils 还在维护吗?

最新版本 0.7.0 发布于 2026/11/04(9 天前);至今共发布 8 个版本,其中过去 12 个月发布了 8 个;首次发布于 2026/09/07。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。