Extend field roles
Convert and display a field value through a custom role.
A field kind defines the value, such as text or a number. A role defines how
the server prepares that value for a use. A variant selects an option within
the role. Backlit includes the display and input roles.
Add a custom role when several blocks need the same data conversion. Use a
variant or client styling for a display change that does not change the data.
This example adds a summary role for text and shortens long notes on the server.
Protocol and server
Augment FieldRoleRegistry in @backlit/contract. Map each supported field kind
to its variants. Use never for a supported kind with no variants. A missing
kind does not support the role.
Register a complete role map with remappers.registerRole. The example calls
the extension directly to keep the resource example self-contained. In an app,
pass the extension to defineApp({ extensions: [summary], areas }).
import { defineResource, fields, table } from '@backlit/sdk'
import { summary } from './summary.ts'
summary()
const orders = defineResource('orders', [
fields.text('title'),
fields.text('note', { summaryVariant: 'excerpt' }),
])
orders.view('list', () =>
table([
{
title: 'Order A',
note: 'This note contains more than forty characters and is shortened on the server.',
},
])
.setFields(orders.pick('title', 'note'))
.setRoles({ note: 'summary' })
)
export default ordersimport type {} from '@backlit/contract'
declare module '@backlit/contract' {
interface FieldRoleRegistry {
summary: { text: 'excerpt' }
}
}import './protocol.ts'
import { remappers } from '@backlit/sdk'
import type {
BacklitExtension,
FieldRemapper,
} from '@backlit/sdk/types'
const text: FieldRemapper<'summary', 'text'> = (
value,
_field,
_record,
_parent,
report
) => {
if (value === null || value === undefined) return null
if (typeof value !== 'string')
return report('expected a string')
return value.length > 40
? `${value.slice(0, 37)}...`
: value
}
export const summary: BacklitExtension = () => {
remappers.registerRole('summary', { text })
return { name: 'summary', version: '1.0.0' }
}{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "list",
"slots": {
"content": [
{
"kind": "table",
"fields": [
{
"kind": "text",
"name": "title",
"label": ""
},
{
"kind": "text",
"name": "note",
"label": "",
"summaryVariant": "excerpt"
}
],
"roles": {
"note": "summary"
},
"data": [
{
"title": "Order A",
"note": "This note contains more than forty ch..."
}
]
}
]
}
}
}Import the protocol module from both server and client extension modules.
FieldRemapper and BacklitExtension come from @backlit/sdk/types.
A remapper returns the value sent to the client. It can call report(reason)
to omit an invalid value. The client does not receive the original record.
Client
Supply a matching field primitive through the client extension. The key joins
the field kind and the capitalized role, so this role uses textSummary.
Use the same extension name and version on both sides.
import './protocol.ts'
import type { TextField } from '@backlit/contract'
import type {
BacklitClientExtension,
FieldPrimitiveProps,
FieldRolePrimitiveMap,
} from '@backlit/ui/types'
function TextSummary({ value }: FieldPrimitiveProps<TextField>) {
return <span>{String(value ?? '')}</span>
}
const primitives = {
textSummary: TextSummary,
} satisfies FieldRolePrimitiveMap<'summary'>
export const summaryClient = {
name: 'summary',
version: '1.0.0',
primitives,
} satisfies BacklitClientExtension
Pass summaryClient in the extensions prop of BacklitApp. The app host must
also supply its link component, navigation function, search adapter, and children.
The client compares the extension identities reported by the server.
Select a role
Use setRoles({ fieldName: 'summary' }) on a table, datalist, lookup, form field
block, or repeater. A field without a selection uses the block default:
display for data blocks, and input for form fields and repeaters.
Role selections travel beside the fields and data.
A role map must contain every kind that its declaration supports. Use
registerRemappers only to replace entries in a registered role. A missing
runtime handler reports a defect and uses the fallback role. The client also
has a fallback when a matching primitive is missing.
Relations delegate conversion to their declared display field. Do not register
a has remapper to calculate a relation count. Fetch the count as a number and
show it through a number field. Groups also use their members to convert values.
See Extensions for registry scope and builder contracts.