Creating a custom element containing multiple elements

Learn how to create a container element with multiple elements.

To create a custom element that contains multiple elements, just like an ObjectElement or GroupElement we can copy them and extend as we want:

vue
<!-- NameElement.vue -->

<script>
import { ref, markRaw } from 'vue'
import { ObjectElement } from '@vueform/vueform'
import theme from '@vueform/vueform/dist/tailwind'

export default {
  // Copy all properties of ObjectElement (eg. emits, mixins, ...)
  ...ObjectElement,
  name: 'NameElement',
  props: {
    // Copy props from ObjectElement
    ...ObjectElement.props,

    // Define child elements for our ObjectElement
    schema: {
      required: false,
      type: Object,
      default: () => ({
        first_name: {
          type: 'text',
          placeholder: 'First name',
          columns: 6,
        },
        last_name: {
          type: 'text',
          placeholder: 'Last name',
          columns: 6,
        },
      })
    }
  },
  setup(props, context) {
    // Copy all props / methods from ObjectElement
    const NameElement = ObjectElement.setup(props, context)

    // Copy default classes from ObjectElement's theme
    const defaultClasses = ref({
      ...theme.classes.ObjectElement,
    })

    // Copy template from ObjectElement's theme
    const template = markRaw(theme.templates.ObjectElement)

    return {
      ...NameElement,
      defaultClasses,
      template,
    }
  },
}
</script>
vue
<!-- NameElement.vue -->

<script>
import { ref, markRaw } from 'vue'
import { ObjectElement } from '@vueform/vueform'
import theme from '@vueform/vueform/dist/vueform'

export default {
  // Copy all properties of ObjectElement (eg. emits, mixins, ...)
  ...ObjectElement,
  name: 'NameElement',
  props: {
    // Copy props from ObjectElement
    ...ObjectElement.props,

    // Define child elements for our ObjectElement
    schema: {
      required: false,
      type: Object,
      default: () => ({
        first_name: {
          type: 'text',
          placeholder: 'First name',
          columns: 6,
        },
        last_name: {
          type: 'text',
          placeholder: 'Last name',
          columns: 6,
        },
      })
    }
  },
  setup(props, context) {
    // Copy all props / methods from ObjectElement
    const NameElement = ObjectElement.setup(props, context)

    // Copy default classes from ObjectElement's theme
    const defaultClasses = ref({
      ...theme.templates.ObjectElement.data().defaultClasses,
    })

    // Copy template from ObjectElement's theme
    const template = markRaw(theme.templates.ObjectElement)

    return {
      ...NameElement,
      defaultClasses,
      template,
    }
  },
}
</script>

After, we can register the element in vueform.config.js:

js
// vueform.config.js

import { defineConfig } from '@vueform/vueform'
import NameElement from './NameElement.vue'

export default defineConfig({
  elements: [
    NameElement,
  ],
})

If we would like to customize the <template> part of the component as well, we can define it in our component instead of exporting template property, using the original template of ObjectElement as a starting point:

vue
<!-- NameElement.vue -->

<template>
  <component :is="elementLayout" :multiple="true" ref="container">
    <template #element>
      <div :class="classes.wrapper" role="group" :aria-labelledby="labelId">
        <slot>
          <component :is="component(element)"
            v-for="(element, name) in children"
            v-bind="element"
            :embed="embed"
            :name="name"
            :key="name"
            @remove="(e) => $emit('remove', e)"
         />
        </slot>
      </div>
    </template>

    <!-- Default element slots -->
    <template v-for="(component, slot) in elementSlots" #[slot]><slot :name="slot" :el$="el$"><component :is="component" :el$="el$"/></slot></template>
  </component>
</template>

<script>
import { ref, markRaw } from 'vue'
import { ObjectElement } from '@vueform/vueform'

export default {
  // Copy all properties of ObjectElement (eg. emits, mixins, ...)
  ...ObjectElement,
  name: 'NameElement',
  props: {
    // Copy props from ObjectElement
    ...ObjectElement.props,

    // Define child elements for our ObjectElement
    schema: {
      required: false,
      type: Object,
      default: () => ({
        first_name: {
          type: 'text',
          placeholder: 'First name',
          columns: 6,
        },
        last_name: {
          type: 'text',
          placeholder: 'Last name',
          columns: 6,
        },
      })
    }
  },
  setup(props, context) {
    // Copy all props / methods from ObjectElement
    const NameElement = ObjectElement.setup(props, context)

    // ObjectElement's classes from Tailwind Theme's source
    // https://github.com/vueform/vueform/blob/main/themes/tailwind/classes.js#L712
    const defaultClasses = ref({
      container: '',
      wrapper: 'grid grid-cols-12',
      wrapper_sm: 'form-gap-gutter-sm',
      wrapper_md: 'form-gap-gutter',
      wrapper_lg: 'form-gap-gutter-lg',
      wrapper_embed: '!block',
      $wrapper: (classes, { Size, embed }) => ([
        classes.wrapper,
        classes[`wrapper_${Size}`],
        embed ? classes.wrapper_embed : null,
      ]),
    })

    return {
      ...NameElement,
      defaultClasses,
      template,
    }
  },
}
</script>
vue
<!-- NameElement.vue -->

<template>
  <component :is="elementLayout" :multiple="true" ref="container">
    <template #element>
      <div :class="classes.wrapper" role="group" :aria-labelledby="labelId">
        <slot>
          <component :is="component(element)"
            v-for="(element, name) in children"
            v-bind="element"
            :embed="embed"
            :name="name"
            :key="name"
            @remove="(e) => $emit('remove', e)"
         />
        </slot>
      </div>
    </template>

    <!-- Default element slots -->
    <template v-for="(component, slot) in elementSlots" #[slot]><slot :name="slot" :el$="el$"><component :is="component" :el$="el$"/></slot></template>
  </component>
</template>

<script>
import { ref, markRaw } from 'vue'
import { ObjectElement } from '@vueform/vueform'

export default {
  // Copy all properties of ObjectElement (eg. emits, mixins, ...)
  ...ObjectElement,
  name: 'NameElement',
  props: {
    // Copy props from ObjectElement
    ...ObjectElement.props,

    // Define child elements for our ObjectElement
    schema: {
      required: false,
      type: Object,
      default: () => ({
        first_name: {
          type: 'text',
          placeholder: 'First name',
          columns: 6,
        },
        last_name: {
          type: 'text',
          placeholder: 'Last name',
          columns: 6,
        },
      })
    }
  },
  setup(props, context) {
    // Copy all props / methods from ObjectElement
    const NameElement = ObjectElement.setup(props, context)

    // ObjectElement's classes from Vueform Theme's source
    // https://github.com/vueform/vueform/blob/main/themes/vueform/templates/elements/ObjectElement.vue#L10
    const defaultClasses = ref({
      container: '',
      wrapper: 'vf-row',
      wrapper_sm: 'vf-row-sm',
      wrapper_md: '',
      wrapper_lg: 'vf-row-lg',
      wrapper_embed: 'vf-row-embed',
      $wrapper: (classes, { Size, embed }) => ([
        classes.wrapper,
        classes[`wrapper_${Size}`],
        embed ? classes.wrapper_embed : null,
      ]),
    })

    return {
      ...NameElement,
      defaultClasses,
    }
  },
}
</script>

... and register it for the templates as well in vueform.config.js:

js
// vueform.config.js

import { defineConfig } from '@vueform/vueform'
import NameElement from './NameElement.vue'

export default defineConfig({
  elements: [
    NameElement,
  ],
  templates: {
    NameElement,
  }
})

If we would like to have non-nested data we can replace ObjectElement with GroupElement in the examples above.

  
👋 Hire Vueform team for form customizations and developmentLearn more