Skip to content

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:

PlacementWhere it appearsWhat the host owns
SectionA 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
CardItemAn 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
TableCellA 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.

src/components/CaseRiskSummary.vue
<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: WorkspaceUiComponentContext is also available through useComponentContext() in any child component. It carries the component key, the placement, the hostSurface (DetailsPage, Drawer, Card or Table), the record identity (entityIdOrKey, instanceId, displayName, number), the fields the host already loaded for that record or row ({ state: 'Value' | 'Restricted', value }), a refreshKey that changes when the host refreshes the record, an AbortSignal in signal, the overlayTarget to pass as attach to every dialog, and the artifact identity.
  • state-change tells the host what you are doing. The host draws the spinner for loading, the message and a retry action for error, and the neutral placeholder for unavailable; your own DOM stays visible for ready and empty. In a table cell the optional text becomes 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.

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": []
}
]
}
  • Key is lowercase kebab-case, at most 64 characters, and unique across pages and components. Constructor stores this key; renaming it breaks existing placements.
  • Component must be an existing .vue file under src/components.
  • TitleKey must exist in all locale files; Constructor and the runtime show the translated title.
  • Placements lists 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.
  • RequiredPermissions gate 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.

  • 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.

  • Lifecycle. The host mounts on first display, remounts when the record changes, replays through refreshKey after the native refresh of the same record, and aborts signal on 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, honor signal, and expect dozens of concurrent mounts. Batching across rows is not provided by this release.
  • Overlays. Dialogs opened from a component must pass context.overlayTarget as attach; 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.
  • These three placements are read-only. An editable block inside the standard record form is the FormItem placement 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 TableCell values.
  • Registering components changes the host contract fingerprint: existing artifacts require Check → Build → Activate after upgrading Moltaro.