Layout
By default every property of an object gets its own row. Add a layout to the object to put fields side by side.
layout only changes how the form looks. The data keeps the same shape as properties, so you can rearrange a form without touching the code that consumes its values.
Rows
layout is a list of rows. A list of property names renders those fields on one row, sharing the width equally.
<script setup lang="ts">
const schema = ref({
type: 'object',
required: [
'firstname'
],
layout: [
[
'firstname',
'lastname'
]
],
properties: {
firstname: {
title: 'First name',
type: 'string',
default: ''
},
lastname: {
title: 'Last name',
type: 'string',
default: ''
},
email: {
title: 'Email',
type: 'string',
default: ''
}
}
})
const model-value = ref({
firstname: '',
lastname: '',
email: ''
})
</script>
<template>
<SForm :model-value="model-value" :schema="schema" />
</template>
Properties that layout doesn't mention are rendered after the layout rows, full width, in their normal order. That's why email above needs no entry.
Widths
Use { "key": "...", "width": n } instead of a plain name to give a field a bigger share of its row. Widths are relative and default to 1, so 3 and 1 split a row ¾ and ¼.
<script setup lang="ts">
const schema = ref({
type: 'object',
required: [
'street',
'city'
],
layout: [
[
{
key: 'street',
width: 3
},
'number'
],
[
'postcode',
{
key: 'city',
width: 2
}
]
],
properties: {
street: {
title: 'Street',
type: 'string',
default: ''
},
number: {
title: 'Number',
type: 'string',
default: ''
},
postcode: {
title: 'Postcode',
type: 'string',
default: ''
},
city: {
title: 'City',
type: 'string',
default: ''
}
}
})
const model-value = ref({
street: '',
number: '',
postcode: '',
city: ''
})
</script>
<template>
<SForm :model-value="model-value" :schema="schema" />
</template>
Nested objects
layout applies to the object it's declared on, so each nested object can have its own.
{
"type": "object",
"required": ["guardian"],
"properties": {
"guardian": {
"type": "object",
"required": ["firstname", "email"],
"layout": [["firstname", "lastname"], ["email", "phone"]],
"properties": {
"firstname": { "type": "string" },
"lastname": { "type": "string" },
"email": { "type": "string" },
"phone": { "type": "string" }
}
}
}
}
Rules
- Each entry in
layoutis one row: either a list of fields, or a single field for a full-width row. - A field is a property name, or
{ "key": "<property>", "width": <number> }. - Rows render in
layoutorder. Fields not inlayoutfollow, full width, inpropertiesorder. - Unknown or repeated property names are skipped, with a warning in the console during development.
- On small screens (below the
smbreakpoint) rows stack vertically.
Migrating from SRow
Wrapping fields in an object with "renderer": "SRow" still works, but it's deprecated: the wrapper object also nests the fields' data, so firstname ends up at name.firstname.
{
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "object",
"renderer": "SRow",
"required": ["firstname"],
"properties": {
"firstname": { "type": "string" },
"lastname": { "type": "string" }
}
}
}
}
{
"type": "object",
"required": ["firstname"],
"layout": [["firstname", "lastname"]],
"properties": {
"firstname": { "type": "string" },
"lastname": { "type": "string" }
}
}
Move the fields up into the parent object, merge the wrapper's required into the parent's, and list the fields as a row in layout. Update any code that reads the old nested values.