Table of Contents

JavaScript Bindings

This page documents the JavaScript / React surface exposed by the MibServer3 FrontEnd Server to custom widgets. In the React era a "custom widget" is a React component shipped through a federated remote (see Component Creation) — the host shell talks to it through props and a handful of shared hooks rather than a global MIB.Template.Manager object.

If you are migrating a legacy MVC custom component, the Legacy MIB.Template.Manager surface section at the bottom documents the old API and how each method maps to the React surface.

Related docs.
Component Creation — the whole component lifecycle, C# + React.
Rendering JavaScript — how to ship custom JS alongside your widget.

React-era surface

How a widget receives data

Every widget receives MibComponentProps<S, D>:

type MibComponentProps<S, D> = {
  componentKey: string;            // PAGE_COMPONENT.TEMPLATE_COMPONENT_KEY
  title?: string;                  // optional, from MapSchema
  schema: S;                       // what your C# MapSchema returned
  data:   D;                       // what your C# MapData returned
  pageKey: string;
  pageIds?: string;                // selected ids in edit mode
  isBulkEdit?: boolean;
  setDirty?: (dirty: boolean) => void;
  refresh?: () => Promise<void>;   // present if you implement IRefreshableComponentV2
};

schema and data are produced by your C# component (MapSchema/MapData). The shell does not transform them — what you emit on the server is what you receive on the client.

Your C# MapData must NEVER return null. The framework treats a null return as a render failure and drops the component from the response with a [Render-ComponentFailure] log line. Return an empty TData instance instead. See Component Authoring — Resilience for the contract on the C# side.

setDirty — the unsaved-changes signal

The shell maintains a page-level "unsaved changes" flag. Save / Cancel buttons and the navigation-guard prompt key off this flag. Your widget should:

  • Call setDirty(true) when the user edits something locally.
  • Call setDirty(false) when the edit is reverted or persisted by your own custom mechanism (e.g. a side API call).

If your widget participates in the page-level Save (the common case), you don't need to call setDirty(false) yourself — the shell clears the flag when its save lifecycle finishes successfully.

refresh — re-pull without a page reload

When your C# class implements IRefreshableComponentV2, the shell exposes a refresh() callback on props. Calling it triggers POST /api/v2/refresh/<pageKey>/<id>?templateComponentKey=<key> and re-mounts the widget with the new data. The most common use is to call it after the user performs a side action that changes state on the server (e.g. publishing an item):

const onPublish = async () => {
  await api.publish(item.id);
  await refresh?.();
};

refresh is undefined if your C# class doesn't implement the refresh interface — guard the call with ?..

Shared hooks (@agilecontent/mib-modules)

The shell exports a small set of React hooks for widgets to integrate with page-level state. They are listed in the shell's shared Module Federation config so every remote sees the same instance — meaning state is genuinely shared, not duplicated per remote.

import {
  useGlobalActions,
  useNotification,
  useSelection,
  useUserContext,
  // Internal (see warning below):
  useContentEdition,
  useFilters,
  useSorter,
  useTranslate,
} from '@agilecontent/mib-modules';

// react-i18next is also a shared dependency.
import { useTranslation } from 'react-i18next';

Internal hooks vs. public hooks. The four hooks documented below — useContentEdition, useFilters, useSorter, useTranslate — are not exported from @agilecontent/mib-modules's public API (packages/modules/src/index.tsx). Customer customisations have historically imported them via internal paths, and the framework code continues to use them; treat them as an unstable surface that may move without notice. The supported alternatives are listed in each section. Verify with the shell team before importing the internal path in new code.

useContentEdition()

⚠️ Internal API. Not exported from @agilecontent/mib-modules's public surface — see the warning above. Customer code that imports this through internal paths may need updating when the shell rev's.

The page-level edit lifecycle: hooks for save, cancel, dirty state, and the persist sequence.

const {
  isDirty, setDirty,
  onSave,   // (cb: () => Promise<void> | void) => unsubscribe
  onCancel, // (cb: () => Promise<void> | void) => unsubscribe
  onConfirm,// (cb: () => Promise<boolean>) => unsubscribe — pre-save gate
} = useContentEdition();

Lifecycle order on a Save click:

  1. All onConfirm callbacks run in sequence. Each returns a Promise<boolean>. If any returns false the save is aborted. Use this to ask the user for confirmation, call an external approval API, or run a precondition check.
  2. All onSave callbacks run in parallel. The shell awaits all of them; any throw aborts the save and surfaces the error in a notice.
  3. The shell's own form-level save call to the BFF runs.
  4. Once all saves resolve, the shell clears the dirty flag.

The replacement for the legacy MIB.Template.Manager.OnCollectData + OnPersistDeliverResponses + OnConfirm is this single hook.

useEffect(() => {
  const unsubSave = onSave(async () => {
    if (Object.keys(pendingEdits).length > 0)
      await api.persistMyWidgetState(pendingEdits);
  });
  const unsubConfirm = onConfirm(async () => {
    if (await needsApproval(pendingEdits))
      return confirm('Submit for approval?');
    return true;
  });
  return () => { unsubSave(); unsubConfirm(); };
}, [pendingEdits]);

Public-API alternative: render <ContentEdition> yourself or register a Save button via useGlobalActions (below).

useFilters() and useSorter()

⚠️ Internal API. Not exported from @agilecontent/mib-modules's public surface — see the warning above.

Read/write the page's list filter expression and component sorting. Mostly used by widgets that decorate a list rather than the stock list component itself.

const { filters, setFilters } = useFilters();
const { sorter, setSorter }   = useSorter();

The sorter hook is only meaningful when the page has SORTER=true in its configuration — see Page Configuration.

Public-API alternative: drive the list via the URL query string (the stock List widget syncs filter/sort there) or via the schema's own filters[] / sorter fields you can compute in MapSchema on the C# side.

useTranslate(section, key)

⚠️ Internal API. Not exported from @agilecontent/mib-modules's public surface — see the warning above. The public-API alternative (react-i18next's useTranslation()) is shown at the bottom of this section.

Pull a string from the active dictionary (MibTranslationConfig):

const t = useTranslate();
return <h3>{t('MyWidget', 'Title')}</h3>;

This is the React-side replacement for the legacy {DICT:Section/Key} markup. The dictionary source is the same; see Localization for how dictionaries are loaded and overridden.

Public-API alternative: the standard react-i18next hook — @agilecontent/mib-modules lists react-i18next as a shared dependency so the dictionary is reachable through the standard hook:

import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
return <h3>{t('MyWidget.Title')}</h3>;

useGlobalActions()

Register a button (or any action) into the shell's top-right toolbar (next to Save / Cancel):

const { register } = useGlobalActions();

useEffect(() => register({
  id: 'my-widget-export',
  label: 'Export TSV',
  icon: 'download',
  onClick: () => exportTsv(items),
  visible: () => items.length > 0,
}), [items]);

register returns an unregister callback so the action is removed when the widget unmounts.

useNotification()

Show a notice / toast / modal alert in the shell's notification region:

const { info, success, warn, error, modal } = useNotification();

success('Saved');
modal({
  title: 'Confirm deletion',
  content: 'This cannot be undone.',
  onOk: () => actuallyDelete(),
});

The replacement for the legacy MIB.Alerts.Confirmation and the auto-shown error modal on ValidationException is this hook.

useSelection() — Aside selector

Replaces MIB.Selection.Open. Programmatically open the shell's aside selector pane and receive an array of { id, name } selections:

const { open } = useSelection();

const onPickAuthor = async () => {
  const picked = await open({
    mediaType: 'AUTHORS',
    multi: false,
    initialFilters: [
      { field: 'ACTIVE', operator: 'EQUAL', value: true },
    ],
  });
  if (picked) setAuthorId(picked[0].id);
};

Filters use the same operator vocabulary as the legacy SelectionFilter (EQUAL / NOTEQUAL / GREATERTHAN / GREATEROREQUAL / LESSTHAN / LESSOREQUAL / CONTAINS / STARTSWITH / ENDSWITH / IN). Values may be static, or reference a form field via the [templateComponentKey.fieldName] template (resolved by the shell, not the BFF).

useUserContext()

Read-only access to the logged-in user. Same shape the BFF puts on UserInformation:

const { user } = useUserContext();
// user.id, user.email, user.groups, user.permissions, ...

For non-trivial permission checks prefer doing the check in C# (e.g. via IHideableComponent) so the widget never appears in the response in the first place. Use useUserContext() only for cosmetic per-user variation inside an already-visible widget.

Cross-widget communication

When two custom widgets on the same page need to coordinate (widget A's selection drives widget B's data), don't reach across with DOM events. Two clean patterns:

  1. Shared hook state. Put the shared state in a custom hook in a module both widgets import (typically a package shared via Module Federation). The hook can use a small Zustand / Jotai store, or plain React useSyncExternalStore.
  2. Page-level form coordination. When the coordination is really "react to a form field change", subscribe to the form widget's change via the form widget's own callback — most form widgets in the stock library accept a onChange prop or expose a context.

Avoid global event buses; they make Storybook hard and break hot-reload.

DevTools and debugging

Inspect the response

The BFF returns one JSON per page render at /api/v2/display/<page>/<id>. Open Chrome DevTools → Network, filter to display, and the response body is the { schema, data } your widget will receive. Errors in MapSchema show up as missing components in schema.components[] (silent on the client, paired with a [Render-ComponentFailure] line on the server — see System Overview — observability).

Inspect dispatch

The shell's component dispatcher logs to console when a type in the schema can't be resolved to a React widget:

[mib-cms] Component type 'foo' is not registered.

That's the symptom of a mismatched component-type key between C# and React. Search your federated remote's src/main.tsx for the missing key.

React DevTools

The shell uses React 18 with named components — install React DevTools and search for <MyCustomWidget> to inspect props / state directly.

Legacy MIB.Template.Manager reference

The pre-React global MIB.Template.Manager JS object, its OnCollectData / OnPersistDeliverResponses / OnConfirm callbacks, MIB.Selection.Open, MIB.Alerts.*, and the rest of the legacy client-side surface are documented under Legacy JavaScript API. New code must not use them.

See also