Skip to main content

Foundation Forms - controls

Foundation forms has a range of advanced components for entering text, numbers, floats, dates and booleans, as well as select and autocomplete input boxes. You can use these to create polished, complex forms for your application in quick time.

Examples

Text control

This is the standard renderer. This creates a text-field in your form. The input takes any characters.

const textInputJsonSchema: JSONSchema7 = {
type: 'object',
properties: {
textInput: {
type: 'string',
description: 'kotlin.String',
},
},
};

const textInputUiSchema: UiSchema = {
type: 'VerticalLayout',
elements: [{ type: 'Control', scope: '#/properties/textInput' }],
};
Applying text transformation

You can use textTransform in StringRendererOptions to normalize string input as users type and when the value is committed:

  • 'none' (default): keeps the original input casing
  • 'uppercase': transforms the value to uppercase
  • 'lowercase': transforms the value to lowercase
  • custom function: apply your own transformation logic
const transformedTextUiSchema: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/symbol',
options: {
textTransform: 'uppercase',
// Standard options can be combined with renderer-specific options
validateFn: (data, path, label) => [],
},
},
],
};

You can also pass a function:

options: {
textTransform: (value) => value.trim().toUpperCase(),
}

Number control

The number renderer creates a number-field in your form. This input only accepts numeric data. It sets a numeric value on the underlying form model.

const numberJsonSchema: JSONSchema7 = {
type: 'object',
properties: {
numberInput: {
type: 'number',
description: 'kotlin.Double',
},
},
};

const numberInputUiSchema: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/numberInput',
},
],
};
Setting decimal places

To control the number of decimal places displayed in the field, use maximumFractionDigit as a format option for the field:

options: Intl.NumberFormatOptions = {
maximumFractionDigits: 4,
minimumFractionDigits: 0,
};

For example:

{
type: 'Control',
label: 'Price',
scope: '#/properties/PRICE',
'options': <StandardRendererOptions>{
formatOptions: <Intl.NumberFormatOptions> {
maximumFractionDigits: 8
}
}
}

Boolean control

The boolean renderer creates a checkbox control. Set the type value in the JSONSchema to boolean to invoke this renderer. It sets a true or false on the underlying form model.

const booleanInputJsonSchema: JSONSchema7 = {
type: 'object',
properties: {
booleanInput: {
type: 'boolean',
description: 'kotlin.Boolean',
},
},
};

const booleanInputUiSchema: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/booleanInput',
},
],
};

Date control

The date control schema creates a date renderer with an input field and a date picker. To invoke this control, set the description property in the JSONSchema to org.joda.time.DateTime. The form only allows numbers to be input. It stores the date value in milliseconds in the underlying form model.

const dateInputJsonSchema: JSONSchema7 = {
type: 'object',
properties: {
dateInput: {
type: 'number',
description: 'org.joda.time.DateTime',
},
},
};

const dateInputUiSchema: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/dateInput',
},
],
};

Password control

The password control schema creates a password field. This is a text field with the input characters obscured. To invoke this renderer, set the isPassword property in options to be true in the JSONSchema.

const passwordInputJsonSchema: JSONSchema7 = {
type: 'object',
properties: {
textInput: {
type: 'string',
description: 'kotlin.String',
},
},
};

const passwordInputUiSchema: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/password',
options: <StringRendererOptions>{
isPassword: true,
},
},
],
};

Textarea control

The textarea control schema creates a textarea field. This is a multi-line entry field. To invoke this renderer, set the textarea property in options to be true in the JSONSchema.

const textAreaInputJsonSchema: JSONSchema7 = {
type: 'object',
properties: {
textarea: {
type: 'string',
description: 'kotlin.String',
},
},
};

const textAreaInputUiSchema: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/textarea',
options: <StringRendererOptions>{
textarea: true,
},
},
],
};

Select control

The select control schema creates a dropdown select box. Users can select only one value from the given list.

To invoke this renderer, include an options section in the UISchemaElement with an array of data. You can specify which properties of your array objects are used for the underlying option value, valueField, and for the display value, labelField.

The filtering behavior can be configured using the filterMode property. This controls how the component filters options when users type in the search field:

  • 'contains' (default): Filters options that contain the search text anywhere in the option
  • 'startsWith': Filters options that start with the search text

You can also include an optional boolean property, allowCustomOptions, which enables users to enter custom values that are not part of the predefined options list, offering more flexibility in their input.

// Select Input
const selectData = ['Miami', 'New York', 'London', 'Dublin', 'São Paulo', 'Bengaluru']

const selectInputJsonSchema: JSONSchema7 = {
type: 'object',
properties: {
selectInput: {
type: 'string',
description: 'kotlin.String',
enum: selectData,
},
},
};

const selectInputUiSchema: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/selectInput',
options: <ConnectedRenderersOptions>{
data: selectData.map((label) => ({ label })),
labelField: 'label',
valueField: 'label',
// Configure filtering behavior
filterMode: 'contains', // or 'startsWith'
},
},
],
};

Connected select

The connected select renderer creates a select input field that populates the list of dropdown options from an endpoint in your Data Server.

In the UISchemaElement options property, specify the allOptionsResourceName. In this example, we use the ALL_COUNTERPARTIES endpoint from sales-forms-examples.dataserver.kts to use the list of counterparties. The display value is the counterparty name and the underlying value is the counterparty id.

You can also configure the filtering behavior using the filterMode property. This controls how the component filters options when users type in the search field:

  • 'contains' (default): Filters options that contain the search text anywhere in the option
  • 'startsWith': Filters options that start with the search text
const selectInputJsonSchema: JSONSchema7 = {
type: 'object',
properties: {
connectedSelectInput: {
type: 'string',
description: 'kotlin.String',
},
},
};

const selectInputUiSchema: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/selectInput',
options: <ConnectedRenderersOptions>{
allOptionsResourceName: 'ALL_COUNTERPARTIES',
valueField: 'COUNTERPARTY_ID',
labelField: 'NAME',
// Use async: true for server-side search on large lists. For select/combobox markup,
// prefer options-datasource infinite-scroll — see Options datasource docs.
async: false,
// Configure filtering behavior
filterMode: 'contains', // or 'startsWith'
},
},
],
};

If you want to compose the option label from multiple fields returned by ALL_COUNTERPARTIES, use the labelRowFormatter property. It receives each row and returns the string to display, overriding the default labelField-based label.

const selectInputUiSchemaWithLabelRowFormatter: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/selectInput',
options: <ConnectedRenderersOptions>{
allOptionsResourceName: 'ALL_COUNTERPARTIES',
valueField: 'COUNTERPARTY_ID',
labelField: 'NAME',
labelRowFormatter: (row) => `${row.NAME} (${row.COUNTERPARTY_ID})`,
filterMode: 'contains',
},
},
],
};

For very large allOptionsResourceName lists, keep each fetch bounded (async: true, datasourceConfig.maxRows, stable orderBy). When you control the HTML, use options-datasource infinite scroll on Select/Combobox instead of loading the full resource on open.

Configuring the datasource (datasourceConfig)

allOptionsResourceName tells the renderer which resource to query. The datasourceConfig option controls how that resource is queried — it's passed straight through to the underlying Datasource, so it accepts the same DatasourceOptions fields you'd use when initializing a datasource directly:

  • criteria — filter criteria applied to the resource. Works with DATASERVER and REQUEST_SERVER resources.
  • maxRows — caps how many rows are fetched. Works with DATASERVER and REQUEST_SERVER resources.
  • orderBy — the field used to sort the returned rows.
  • viewNumber — the current page/view being displayed. Works with DATASERVER and REQUEST_SERVER resources.
  • fields — restricts the columns returned. DATASERVER only.
  • isSnapshot — fetch a one-off snapshot instead of a live stream.
  • disablePolling, pollingInterval, pollTriggerEvents, request, requestAutoSetup — control request/reply polling. REQUEST_SERVER only.
const selectInputUiSchemaWithDatasourceConfig: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/selectInput',
options: <ConnectedRenderersOptions>{
allOptionsResourceName: 'ALL_COUNTERPARTIES',
valueField: 'COUNTERPARTY_ID',
labelField: 'NAME',
async: true,
datasourceConfig: {
criteria: "STATUS = 'ACTIVE'",
maxRows: 200,
orderBy: 'NAME',
},
},
},
],
};

datasourceConfig applies equally to the connected multi select renderer below. See the DatasourceOptions API documentation for the full list of fields.

Complete example

The example below combines every ConnectedRenderersOptions field described above into a single example. It narrows ALL_COUNTERPARTIES down using criteria that filters across several fields at once (STATUS, COUNTRY and TYPE), sorts the results with orderBy, and layers async, filterMode, allowCustomOptions and labelRowFormatter on top:

const selectInputUiSchemaComplete: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/selectInput',
options: <ConnectedRenderersOptions>{
// Which resource to query.
allOptionsResourceName: 'ALL_COUNTERPARTIES',
// Underlying value stored on the form, and the field read to build the label.
valueField: 'COUNTERPARTY_ID',
labelField: 'NAME',
// Compose the label from more than one returned field.
labelRowFormatter: (row) => `${row.NAME} (${row.COUNTRY})`,
// Re-query the server as the user types, instead of filtering a single client-side fetch.
async: true,
// Match anywhere in the option text; use 'startsWith' to only match from the beginning.
filterMode: 'contains',
// Don't let users submit a value that isn't in the resolved option list.
allowCustomOptions: false,
// Controls how the datasource itself queries ALL_COUNTERPARTIES.
datasourceConfig: {
// Filter across multiple fields on the counterparty row.
criteria: 'STATUS == "ACTIVE" && COUNTRY == "GB" && TYPE == "CLIENT"',
// Sort the results returned by the resource.
orderBy: 'NAME',
// Cap how many rows are fetched per request.
maxRows: 200,
// Which page/view of results to fetch.
viewNumber: 0,
},
},
},
],
};

If you'd rather supply the options yourself instead of querying a resource — for a short, static list, or as a fallback while a resource is unavailable — use data in place of allOptionsResourceName; valueField/labelField and labelRowFormatter still apply to rows you provide this way:

options: <ConnectedRenderersOptions>{
data: [
{ COUNTERPARTY_ID: 1, NAME: 'Acme Corp', COUNTRY: 'GB' },
{ COUNTERPARTY_ID: 2, NAME: 'Globex Ltd', COUNTRY: 'US' },
],
valueField: 'COUNTERPARTY_ID',
labelField: 'NAME',
},

Connected multi select

The connected multi select renderer connects to your back end in the same way as the connected select renderer.

const connectedMultiSelectJsonSchema: JSONSchema7 = {
type: 'object',
properties: {
connectedMultiSelectInput: {
type: 'array',
description: 'Kotlin.String',
},
},
};

const connectedMultiSelectUISchema: UiSchemaElement = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
label: 'Connected select',
scope: '#/properties/connectedMultiSelectInput',
options: {
allOptionsResourceName: 'ALL_COUNTERPARTIES',
valueField: 'COUNTERPARTY_ID',
labelField: 'NAME',
},
},
],
};

On the server side, you typically need an event handler that can receive the selected values and process them. For example, to insert a trader and create TRADER_BOOK_ACCESS relationships for each selected book:

import global.genesis.message.core.annotation.*
import com.fasterxml.jackson.annotation.JsonUnwrapped

data class TraderWithBooks(
@Mandatory
var books: List<Long>,
) {
@JsonUnwrapped
lateinit var trader: Trader
}

eventHandler<TraderWithBooks>("TRADER_INSERT", transactional = true) {
onCommit { event ->
val details = event.details
val insertedRow = entityDb.insert(details.trader)
val traderIdValue = insertedRow.record.traderId

// Insert TRADER_BOOK_ACCESS relationships
val traderBookAccessRecords = details.books.map { bookIdValue ->
TraderBookAccess {
this.traderId = traderIdValue
this.bookId = bookIdValue
}
}
entityDb.insertAll(traderBookAccessRecords)

// return an ack response which contains a list of record IDs
ack(
listOf(
mapOf(
"TRADER_ID" to traderIdValue,
)
)
)
}
}

This pattern can be adapted to other use cases where a connected multi select drives the creation or update of related records.

Connected Multi Select

info

After you have looked at the basics here, you can find more details in our API Docs

Full source code at Controls

Divider control

The divider renderer creates a visual separator that can be placed between any form elements. It simply draws a horizontal line and doesn’t map to any form value. To enable this renderer, set the divider property in options to true. The example below shows a divider placed between two controls.

const dividerUiSchema: UiSchema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/input1',
label: 'Input 1',
},
{
type: 'Control',
options: {
divider: true,
},
},
{
type: 'Control',
scope: '#/properties/input2',
label: 'Input 2',
},
],
};