Field Metadata

Entities in Remult are not just a single source of truth for storage, APIs, and authentication—they can serve as a central point for managing any entity-related aspect across your application. Let’s explore how to utilize field metadata for consistent UI labeling and data formatting.

Setting Captions

To ensure consistent captions across your app, set the caption attribute directly in the field definition. This way, the same caption is automatically used wherever the field appears.

shared/Task.ts
6 collapsed lines
import { Entity, Fields } from 'remult'
import { TaskPriorities, type TaskPriority } from './TaskPriority'
@Entity('tasks', {
allowApiCrud: true,
})
export class Task {
@Fields.uuid()
id = ''
@Fields.string({
caption: 'The Task Title',
})
title = ''
16 collapsed lines
@Fields.literal<Task>(() => TaskPriorities, {
caption: 'Priority Level',
displayValue: (task) =>
task.priority.charAt(0).toUpperCase() + task.priority.slice(1), // Capitalizes the first letter for display
})
priority: TaskPriority = 'low'
@Fields.boolean<Task>()
completed = false
@Fields.createdAt({
caption: 'Task Creation Date',
displayValue: (_, value) => value?.toLocaleDateString(),
})
createdAt?: Date
}

Accessing Captions

Field metadata, like caption, can be accessed using the fields property of a repository:

const titleCaption = repo(Task).fields.title.caption
console.log(titleCaption) // Outputs: "The Task Title"

Using captions this way allows for a unified UI. For example, in TodoItem.tsx:

frontend/TodoItem.tsx
import { repo } from 'remult'
import { Task } from '../shared/Task.js'
const taskRepo = repo(Task)
export function TodoItem({ task }: { task: Task }) {
const titleField = taskRepo.fields.title
const priorityField = taskRepo.fields.priority
const createdAtField = taskRepo.fields.createdAt
return (
<div>
<div>
<div>
{titleField.caption}: <strong>{task.title}</strong>
</div>
12 collapsed lines
<div>
{priorityField.caption}:
<strong> {priorityField.displayValue(task)}</strong>
</div>
<div>
{createdAtField.caption}:
<strong> {createdAtField.displayValue(task)}</strong>
</div>
</div>
</div>
)
}

Try changing the caption of title in Task and observe how the UI updates automatically!

Display Consistency with displayValue

To ensure consistent display formatting, especially for literals or dates, use the displayValue property.

Example 1: Displaying a Literal Field

For fields with literal values, like priority, displayValue can ensure consistent capitalization:

shared/Task.ts
6 collapsed lines
import { Entity, Fields } from 'remult'
import { TaskPriorities, type TaskPriority } from './TaskPriority'
@Entity('tasks', {
allowApiCrud: true,
})
export class Task {
7 collapsed lines
@Fields.uuid()
id = ''
@Fields.string({
caption: 'The Task Title',
})
title = ''
@Fields.literal<Task>(() => TaskPriorities, {
caption: 'Priority Level',
displayValue: (task) =>
task.priority.charAt(0).toUpperCase() + task.priority.slice(1), // Capitalizes the first letter for display
})
priority: TaskPriority = 'low'
9 collapsed lines
@Fields.boolean<Task>()
completed = false
@Fields.createdAt({
caption: 'Task Creation Date',
displayValue: (_, value) => value?.toLocaleDateString(),
})
createdAt?: Date
}

In TodoItem.tsx, access and use this display formatting:

frontend/TodoItem.tsx
14 collapsed lines
import { repo } from 'remult'
import { Task } from '../shared/Task.js'
const taskRepo = repo(Task)
export function TodoItem({ task }: { task: Task }) {
const titleField = taskRepo.fields.title
const priorityField = taskRepo.fields.priority
const createdAtField = taskRepo.fields.createdAt
return (
<div>
<div>
<div>
{titleField.caption}: <strong>{task.title}</strong>
</div>
<div>
{priorityField.caption}:
<strong> {priorityField.displayValue(task)}</strong>
</div>
8 collapsed lines
<div>
{createdAtField.caption}:
<strong> {createdAtField.displayValue(task)}</strong>
</div>
</div>
</div>
)
}

Example 2: Displaying Dates with Reusable displayValue

Let’s take a closer look at defining displayValue for dates:

shared/Task.ts
6 collapsed lines
import { Entity, Fields } from 'remult'
import { TaskPriorities, type TaskPriority } from './TaskPriority'
@Entity('tasks', {
allowApiCrud: true,
})
export class Task {
18 collapsed lines
@Fields.uuid()
id = ''
@Fields.string({
caption: 'The Task Title',
})
title = ''
@Fields.literal<Task>(() => TaskPriorities, {
caption: 'Priority Level',
displayValue: (task) =>
task.priority.charAt(0).toUpperCase() + task.priority.slice(1), // Capitalizes the first letter for display
})
priority: TaskPriority = 'low'
@Fields.boolean<Task>()
completed = false
@Fields.createdAt({
caption: 'Task Creation Date',
displayValue: (_, value) => value?.toLocaleDateString(),
})
createdAt?: Date
}

In this example, the displayValue function is designed to ignore the first parameter (representing the entity) and only use the second parameter, the date value. By focusing on the value alone, this displayValue function can be refactored into a standalone utility that works for any date field, not just createdAt.

Refactoring displayValue for Reusability

You can create a reusable displayDate function and use it across different date fields:

utils/displayValueHelpers.ts
export const displayDate = (_: unknown, date?: Date) =>
date?.toLocaleDateString()

Now, any date field can use this displayDate function for consistent date formatting, regardless of the entity:

import { displayDate } from './utils/displayValueHelpers'
@Fields.createdAt({
caption: 'Task Creation Date',
displayValue: displayDate,
})
createdAt?: Date

This approach ensures consistency in date formatting across your application and keeps your code clean and maintainable. You can define similar reusable functions for other field types, ensuring that formatting stays uniform across different entities and fields.

Extending Field Options with Custom Options

Beyond the standard options, fields can also be enhanced with custom options tailored to your application’s specific needs. These options allow you to store and access any metadata you might need directly within the field definition, making your entity models even more powerful and adaptable.

For example, you might want to add a custom validation message, tooltip, or any other metadata to a field. This added flexibility helps you centralize and standardize additional properties that can be useful in various parts of your application, from dynamic UI rendering to custom business logic.

To explore more about how to define and use custom options, check out Enhancing Field and Entity Definitions with Custom Options.

Leveraging Metadata for Dynamic UI

With field metadata, you can abstract your UI components for consistent display across your app. Here’s an example of a dynamic component that uses field metadata:

frontend/TodoItem.tsx
import { repo } from 'remult'
import { Task } from '../shared/Task.js'
const taskRepo = repo(Task)
export function TodoItem({ task }: { task: Task }) {
const fields = [
taskRepo.fields.title,
taskRepo.fields.priority,
taskRepo.fields.createdAt,
]
return (
<div>
<div>
{fields.map((field) => (
<div key={field.key}>
{field.caption}: <strong>{field.displayValue(task)}</strong>
</div>
))}
</div>
</div>
)
}

Click “Solve” at the top right of the code editor to see this abstraction in action. This dynamic UI approach ensures your fields are displayed with the same metadata-defined captions and formatting throughout the app.


you can use the getValueList function to get the values of a literal field

import { repo, getValueList } from 'remult'
return (
<div>
<div>
{fields.map((field) => (
<div key={field.key}>
{field.caption}: <strong>{field.displayValue(task)}</strong>{' '}
{getValueList(field as any)
? `(options: ${getValueList(field as any)})`
: ''}
</div>
))}
</div>
</div>
)

You can use these capabilities, together with the structured error model to create dynamic forms, dynamic grid and other dynamic uis that leverage

Powered by WebContainers
Files
Preparing Environment
  • Installing dependencies
  • Starting http server