Embedded components
A Workspace UI Project can register components next to pages. A registered
component is an ordinary Vue single-file component under src/components that
Moltaro mounts read-only inside its own Entity UI surfaces. The native screen
keeps its navigation, record header, lifecycle actions, history, forms and
data loading; you add only the block that the standard surfaces cannot express.
Three placements are supported:
| Placement | Where it appears | What the host owns |
|---|---|---|
Section | A section of the Details page and a section of the Drawer (the same hosted layout Constructor uses for related data and module blocks) | Section order, title, navigation, lazy activation, loading and unavailable states |
CardItem | An item inside a Card surface, wherever that card renders (Details, Drawer, related-data card blocks) | Label, tab, order, responsive column span, spacing, loading and unavailable states |
TableCell | A synthetic column of a Table surface rendered by the standard grid (Data Explorer, related tables, pickers) | Header, width, visibility, order, virtualization, selection and keyboard, compact loading and unavailable states |
Everything else stays where it was: data authorization on the server, the
record and its fields loaded by the host, forms and saving in the native
form. An embedded component never receives host stores, drafts, tokens or a
save path. When the block must edit fields, use the FormItem placement
described in Form components.
1. Write the component
Section titled “1. Write the component”<template> <div> <p v-if="risk" class="text-body-2"> {{ t('components.caseRisk.level') }}: <strong>{{ risk.Level }}</strong> </p> <p v-else class="text-body-2 text-medium-emphasis">{{ t('components.caseRisk.none') }}</p> </div></template>
<script setup lang="ts"> import type { WorkspaceUiComponentContext, WorkspaceUiComponentState } from '@moltaro/workspace-ui' import { MoltaroApiClient, useComponentContext } from '@moltaro/workspace-ui' import { ref, watch } from 'vue' import { useI18n } from 'vue-i18n'
const props = defineProps<{ context: WorkspaceUiComponentContext }>() const emit = defineEmits<{ 'state-change': [value: WorkspaceUiComponentState] }>() const { t } = useI18n() // The same read-only context is injected for child components; do not name the local `context`. const component = useComponentContext() const client = new MoltaroApiClient() const risk = ref<{ Level: string } | null>(null)
async function load () { // Fields the host already loaded are available without a request. const caseNumber = String(component.fields.Number?.value ?? '') emit('state-change', { status: 'loading', error: '' }) try { const response = await client.requestData<{ Level: string } | null>( `/api/workspace/functions/case-risk/${encodeURIComponent(component.record.instanceId)}?number=${encodeURIComponent(caseNumber)}`, { signal: props.context.signal }, ) risk.value = response ?? null emit('state-change', { status: risk.value ? 'ready' : 'empty', error: '', text: risk.value?.Level ?? '' }) } catch { if (props.context.signal.aborted) { return } emit('state-change', { status: 'error', error: t('components.caseRisk.failed') }) } }
// The host aborts `context.signal` on unmount and on a record change; no manual cleanup is needed. watch(() => [props.context.record.instanceId, props.context.refreshKey], () => { void load() }, { immediate: true })</script>The contract is one prop and one event:
context: WorkspaceUiComponentContextis also available throughuseComponentContext()in any child component. It carries the component key, theplacement, thehostSurface(DetailsPage,Drawer,CardorTable), therecordidentity (entityIdOrKey,instanceId,displayName,number), thefieldsthe host already loaded for that record or row ({ state: 'Value' | 'Restricted', value }), arefreshKeythat changes when the host refreshes the record, anAbortSignalinsignal, theoverlayTargetto pass asattachto every dialog, and the artifact identity.state-changetells the host what you are doing. The host draws the spinner forloading, the message and a retry action forerror, and the neutral placeholder forunavailable; your own DOM stays visible forreadyandempty. In a table cell the optionaltextbecomes the accessible value of the cell.
usePageContext() throws inside a component and useComponentContext()
throws inside a page. Every other @moltaro/workspace-ui composable and every
@moltaro/ui export works unchanged, including the reference picker and the
record components, which need attach: context.overlayTarget.
fields contains what the host loaded for its own rendering: the whole
record in a section or card item, the queried row in a table cell. Restricted
values are marked, never dropped. Anything else is read through
MoltaroApiClient with ordinary server authorization; the component runs
under the current user and cannot widen access.
2. Register it
Section titled “2. Register it”Register the component in UI Studio → Components, or in
workspace-ui.json for ZIP and API authoring:
{ "FormatVersion": 1, "Pages": [], "Components": [ { "Key": "case-risk-summary", "Component": "src/components/CaseRiskSummary.vue", "TitleKey": "components.caseRisk.title", "Icon": "mdi-shield-alert-outline", "Placements": ["Section", "CardItem", "TableCell"], "RequiredPermissions": [] } ]}Keyis lowercase kebab-case, at most 64 characters, and unique across pages and components. Constructor stores this key; renaming it breaks existing placements.Componentmust be an existing.vuefile undersrc/components.TitleKeymust exist in all locale files; Constructor and the runtime show the translated title.Placementslists where Constructor may offer the component. Add a placement only when the component is designed for that space: a section can be tall, a card item sits next to fields, a table cell must stay compact and single-row.RequiredPermissionsgate rendering exactly like page permissions.
Then Check → Build → Activate and refresh the browser. Components use the
same scoped-CSS pipeline as pages: <style scoped> in a component and in
anything it imports is scoped to that component’s mounts.
3. Place it in Constructor
Section titled “3. Place it in Constructor”- Details or Drawer section: Constructor → entity → Details page or Drawer → sections. The component appears in the block picker under Workspace UI component. It is a normal section: order it, hide it, give it an optional title.
- Card item: Constructor → entity → Card surface → Add item → Workspace UI component. The item has a label, hint, tab, order and column span like any other item; the component draws the value area.
- Table column: Constructor → entity → Table surface → Add column → Workspace UI components. The column has a label, width, order and visibility. It is not sortable or filterable, is not shown in the mobile card list, and is not exported by the server; users copy the accessible text the component reports.
The same registered component may be placed several times on different surfaces. Placements stay stored when the component is later removed from the artifact or the artifact is deactivated: the surface shows an explicit Component unavailable state until a matching artifact is active again. Authoring APIs are stricter and reject keys that the active artifact does not register for that placement.
Section placement through the Configuration API
Section titled “Section placement through the Configuration API”A Section component requires BindingPath even though it renders the current
record. Read the host layout’s AvailableBlocks, find the option whose Key
is workspace-ui-component:case-risk-summary, and copy its root path from
Paths. That path has no steps, and both entity IDs identify the host entity.
Do not use a related-record path or omit the binding.
For example, this is one item in the Details or Drawer host layout’s Items
array (replace case-definition-id with the actual definition ID and use the
path label returned by the host):
{ "Id": "case-risk-section", "ItemKind": 0, "BlockKind": 3, "StandardBlock": null, "ExtensionKey": "case-risk-summary", "BindingPath": { "RootEntityDefinitionId": "case-definition-id", "TargetEntityDefinitionId": "case-definition-id", "Label": "Cases", "Steps": [] }, "Settings": null, "SettingsVersion": 1, "Title": "Risk summary", "Subtitle": null, "SortOrder": 20, "Visible": true, "CollapsedByDefault": false}ItemKind: 0 means Section; BlockKind: 3 means Workspace UI component. Retain
the other layout items when replacing the layout. The active artifact must
register the component for Section; the same component key may appear only
once in one layout. Confirm the host’s current request contract in the
installation’s Configuration API reference before saving.
Behavior to design for
Section titled “Behavior to design for”- Lifecycle. The host mounts on first display, remounts when the record
changes, replays through
refreshKeyafter the native refresh of the same record, and abortssignalon unmount and when the grid recreates a cell. Do not keep state you expect to survive a hidden section. - Errors are contained. A render error or a thrown factory disables that mount only and is reported to Workspace UI diagnostics with the component key. The rest of the record, card or table keeps working.
- Table cells are many. A visible grid page mounts one instance per
rendered row and re-creates them while scrolling. Prefer values already in
context.fields; when you must call the API per row, keep the request small, honorsignal, and expect dozens of concurrent mounts. Batching across rows is not provided by this release. - Overlays. Dialogs opened from a component must pass
context.overlayTargetasattach; nested pickers and record dialogs then keep scoped styles and focus handling. - Portability. Entity YAML and installation packages carry the component key with the placement. Deliver the Workspace UI project before or after the entity configuration; the placement renders the unavailable state until the artifact that registers the component is activated.
Limits
Section titled “Limits”- These three placements are read-only. An editable block inside the
standard record form is the
FormItemplacement described in Form components. - No mobile card list or Board card placements.
- Package-owned (
NET) surfaces cannot reference workspace components. - No per-placement settings; configuration lives in the registration.
- No server-side sorting, filtering or export of
TableCellvalues. - Registering components changes the host contract fingerprint: existing artifacts require Check → Build → Activate after upgrading Moltaro.