Skip to content

Layout components ​

A definition's layout puts its fields in sections, rows of a 12-column grid and steps, and leaves slots for your content. <JsonFields> renders it with a component of yours for each layout type. Below you will learn how to write them.

Sections and rows ​

A section gets the section as written, and renders its items. A row gets the span of each cell, and renders each cell:

vue
<script setup lang="ts">
import type {
  SectionProps,
  SectionSlots,
} from '@whatsaaaa/form-builder-vue'

defineProps<SectionProps>()
defineSlots<SectionSlots>()
</script>

<template>
  <fieldset>
    <legend>{{ section.label }}</legend>
    <p v-if="section.description">{{ section.description }}</p>
    <slot />
  </fieldset>
</template>
vue
<script setup lang="ts">
import type { RowProps, RowSlots } from '@whatsaaaa/form-builder-vue'

defineProps<RowProps>()
defineSlots<RowSlots>()
</script>

<template>
  <div class="row">
    <div
      v-for="(span, index) in spans"
      :key="index"
      :style="{ gridColumn: `span ${span}` }"
    >
      <slot :index="index" />
    </div>
  </div>
</template>

<style scoped>
.row {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 1rem;
}

/* The spans are for a wide screen. On a narrow one, each cell takes the whole row. */
@media (max-width: 640px) {
  .row > * {
    grid-column: 1 / -1 !important;
  }
}
</style>
tsx
function LayoutSection({ section, children }: SectionProps) {
  return (
    <fieldset>
      <legend>{section.label}</legend>
      {section.description && <p>{section.description}</p>}
      {children}
    </fieldset>
  )
}

// One line of a 12-column grid. The .row class is in the CSS below.
function LayoutRow({ spans, children }: RowProps) {
  return (
    <div className="row">
      {spans.map((span, index) => (
        <div key={index} style={{ gridColumn: `span ${span}` }}>
          {children(index)}
        </div>
      ))}
    </div>
  )
}

export const layoutComponents: LayoutComponents = {
  section: LayoutSection,
  row: LayoutRow,
}
css
.row {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 1rem;
}

/* The spans are for a wide screen. On a narrow one, each cell takes the whole row. */
@media (max-width: 640px) {
  .row > * {
    grid-column: 1 / -1 !important;
  }
}
Layout typePropsContent
sectionsection, as written, with its id, label, description and uithe section's items
rowrow, as written, with its id and ui, and spans, one per cellthe cell at each index

In Vue, the content is the default slot, with { index } for a row. In React, it is children, and children(index) for a row.

Rendering the layout ​

Pass the components as layoutComponents, and fill the layout's slots: with named slots in Vue, or with slots in React:

vue
<script setup lang="ts">
import { Form } from '@formisch/vue'
import type { JsonForm } from '@whatsaaaa/form-builder-schema'
import { JsonFields, useJsonForm } from '@whatsaaaa/form-builder-vue'
import { components } from './all-fields'
import LayoutRow from './LayoutRow.vue'
import LayoutSection from './LayoutSection.vue'

const { definition } = defineProps<{ definition: JsonForm }>()

const jsonForm = useJsonForm(definition, {
  components,
  layoutComponents: { section: LayoutSection, row: LayoutRow },
})

function save(output: unknown) {
  console.log(output)
}
</script>

<template>
  <Form :of="jsonForm.form" @submit="save">
    <JsonFields :of="jsonForm">
      <template #submit>
        <button type="submit">Place order</button>
      </template>
    </JsonFields>
  </Form>
</template>
tsx
export function CheckoutForm({ definition }: { definition: JsonForm }) {
  const jsonForm = useJsonForm(definition, { components, layoutComponents })

  function save(output: unknown) {
    console.log(output)
  }

  return (
    <Form of={jsonForm.form} onSubmit={save}>
      <JsonFields
        of={jsonForm}
        slots={{ submit: <button type="submit">Place order</button> }}
      />
    </Form>
  )
}
  • A layout type that the layout uses without a component, or a slot without content, throws a TypeError that names each one when <JsonFields> first renders, for every step, not only the current one.
  • A definition without sections or rows needs no layout components, and one without a layout renders its fields in order.
  • Display elements written in the layout render with your field components, like those in fields.
  • A section or a row whose fields are all hidden or left out with except drops out. The other cells of a row keep their spans.

Screen sizes ​

The spans are for a wide screen. The layout says nothing about narrow ones, so your row component decides. The rows above give each cell the whole row below 640 pixels. Read row.ui or section.ui for hints of your own, like { "variant": "card" }.

Next steps ​

Continue to multi-step forms to render a layout with steps.