Custom Modes
Custom modes let you build an entirely new layout and rendering strategy for your dataset, then ship it as a named value in content.mode.
When to Use a Custom Mode
| Situation | Prefer custom mode? | Why |
|---|---|---|
| Built-in table/grid/board/carousel/chart cannot express the layout | Yes | The entire content renderer needs to change |
| A field needs a new visual treatment | No | Use custom renderAs instead |
| A reusable domain field type is needed | No | Use a custom field type |
| The requirement is mostly configuration of a built-in mode | No | Stay with built-in config |
1. Build the Mode Component
A mode component receives a standard props contract:
// src/modes/TimelineMode.tsx
import React from 'react';
import type { Entity } from '@widgemo/widgemo-core';
interface TimelineModeProps {
data: Entity[];
config?: Record<string, unknown>;
actions?: unknown[];
onInteractionEvent?: (ctx: unknown) => void;
}
export function TimelineMode({ data, config = {} }: TimelineModeProps) {
const { dateField = 'date', sortOrder = 'desc' } = config as {
dateField?: string;
sortOrder?: 'asc' | 'desc';
};
const sorted = [...data].sort((a, b) => {
const d =
new Date(String(a[dateField])).getTime() -
new Date(String(b[dateField])).getTime();
return sortOrder === 'asc' ? d : -d;
});
return (
<ol
style={{
listStyle: 'none',
padding: 0,
margin: 0,
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
}}
>
{sorted.map((entity, i) => (
<li
key={i}
style={{ display: 'flex', gap: '0.75rem', alignItems: 'flex-start' }}
>
<time style={{ minWidth: 90, color: '#6b7280', fontSize: '0.8rem' }}>
{String(entity[dateField])}
</time>
<span style={{ fontWeight: 500 }}>
{String(entity.title ?? entity.name ?? '—')}
</span>
</li>
))}
</ol>
);
}
Mode Props Contract
| Prop | Type | Description |
|---|---|---|
data | Entity[] | Data in the current render scope |
config | Record<string, unknown> | Mode-specific config from modeConfig.<name> |
actions | ActionConfig[] | Forwarded item actions |
gestures | GestureConfig[] | Forwarded gestures |
onInteractionEvent | (ctx) => void | Interaction emitter for standardized contexts |
2. Register at App Boot
import { widgemoRegistry } from '@widgemo/widgemo-core';
import { TimelineMode } from './modes/TimelineMode';
widgemoRegistry.registerWidgemoMode({
name: 'timeline',
component: TimelineMode,
defaultConfig: {
dateField: 'date',
sortOrder: 'desc',
orientation: 'vertical',
showLines: true,
color: '#007bff',
},
});
3. Use in Config
const config = {
zones: {
header: { title: 'Project Timeline' },
content: {
mode: 'timeline',
modeConfig: {
timeline: {
dateField: 'created_at',
sortOrder: 'asc',
},
},
item: { fields: […], layout: { type: 'auto' } },
},
},
};
Interactions in Custom Modes
Built-in gesture types available through config:
item-clickitem-drag-startitem-drop
Gesture handler flow:
- If the matching gesture defines
onTrigger, Widgemo callsonTrigger. - If
onTriggeris omitted, Widgemo falls back tointeractions.onEvent.
For gesture configuration details, see Content Config: Gestures.
const config = {
interactions: {
onEvent: (ctx) => {
console.log('fallback interaction', ctx.kind, ctx.entity?.id);
},
},
zones: {
content: {
mode: 'timeline',
gestures: [
{
type: 'item-click',
onTrigger: (ctx) => {
openEntity(ctx.entity?.id);
},
},
{ type: 'item-drag-start' },
{ type: 'item-drop' },
],
item: { fields: [{ key: 'name' }], layout: { type: 'auto' } },
},
},
};
Mode-Local Interactions
You can implement additional mode-specific UI interactions directly in your custom mode component (for example hover effects, drag-over highlights, and scroll-driven behaviors).
Use built-in gestures for standardized interaction handling. Use mode-local event handlers for mode-specific interactions.
export function TimelineMode({ data = [], onInteractionEvent }: TimelineModeProps) {
return (
<ol>
{data.map((entity, index) => (
<li
key={String(entity.id ?? index)}
onMouseEnter={() => highlightRow(entity.id)}
onWheel={(event) => syncTimelineScroll(event.deltaY)}
onDoubleClick={() => onInteractionEvent?.({
kind: 'item-click',
zone: 'content',
entity,
data,
interactionId: 'timeline-double-click',
interactionLabel: 'Timeline Double Click',
})}
>
{String(entity.name ?? entity.title ?? index)}
</li>
))}
</ol>
);
}
For lifecycle interception guidance, see Extension API: When to Use Which.
Best Practices
- Start from a built-in mode and move to a custom mode only when the layout strategy truly differs.
- Keep mode names namespaced when they are application-specific (
acme.timeline). - Keep
defaultConfiglimited to mode-local defaults. - Forward standardized interactions through
onInteractionEventwhen possible.
Collision Behavior
Collision semantics are defined in Extension API: Registration Collisions.
Use namespaced names (for example, acme.timeline) to reduce accidental collisions.