Creating Elements

Learn how to create new elements in Vueform.

Boilerplate

We can use the following boilerplate to create a new Vueform element:

<template>
  <ElementLayout>
    <template #element>
      <!-- ADD YOUR ELEMENT TEMPLATE HERE -->
    </template>

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

<script>
  import { ref } from 'vue' // from '@vue/composition-api' in Vue.js 2
  import VueformElement from '@vueform/vueform/element'

  export default VueformElement({
    name: 'CustomElement',
  }, {
    setup(props, { element }) {
      const defaultClasses = ref({
        container: '', // added automatically to the element's outermost DOM in ElementLayout
      })

      return {}
    }
  })
</script>

Template

First we have the <template> part where we are using ElementLayout component as a wrapper for our element and we define the actual element template in #element slot:

<template>
  <ElementLayout>
    <template #element>
      <!-- ADD YOUR ELEMENT TEMPLATE HERE -->
    </template>
    <!-- ... -->
  </ElementLayout>
</template>

Then we have a row that passes over each slot defined for the element to the ElementLayout:

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

This is how we can use inline slots for label, description, etc:

<CustomElement name="custom">
  <template #label>Foo</template>
  <template #description>Bar</template>
</CustomElement>

Options

The component should export a VueformElement, where the first argument are the element options and the second contains all the regular propeties as we'd normally define for a component.

<script>
  import VueformElement from '@vueform/vueform/element'

  export default VueformElement({
    name: 'CustomElement',
  }, /* ... /* )
</script>

The name option in the first argument must be the element name suffixed with Element using PascalCase. Once we register this element we'll be able to use it with type: 'custom'.

Setup

The second argument of VeuformElement includes all the regular properties we'd normally defined for a component. This includes setup() function, which only differentiates from the original version that it includes an element property in context (the second argument of setup()):

<script>
  import VueformElement from '@vueform/vueform/element'

  export default VueformElement({
    name: 'CustomElement',
  }, {
    setup(props, context) {
      const element = context.element
    }
  })
</script>

Using Element Properties & Methods

Using the element property we can access all the component's default properties and methods. For example if'd like to watch the element's model we can do:

<script>
  import { watch } from 'vue' // from '@vue/composition-api' in Vue.js 2
  import VueformElement from '@vueform/vueform/element'

  export default VueformElement({
    name: 'CustomElement',
  }, {
    setup(props, { element }) {
      watch(element.model, (newValue, oldValue) => {
        console.log('model changed from', oldValue, 'to', newValue)
      })
    }
  })
</script>

New Properties & Methods

We can also return new properties and methods that can be accessed in the component's template:

<template>
  <ElementLayout>
    <template #element>
      {{ foo }}
    </template>
    <!-- ... --->
  </ElementLayout>
</template>

<script>
  import { ref } from 'vue' // from '@vue/composition-api' in Vue.js 2
  import VueformElement from '@vueform/vueform/element'

  export default VueformElement({
    name: 'CustomElement',
  }, {
    setup(props, { element }) {
      const foo = ref('bar')

      return {
        foo,
      }
    }
  })
</script>

Default Classes

Elements are using a dynamic class system as described here. We can set default classes for elements using defaultClasses:

<template>
  <ElementLayout>
    <template #element>
      <div :class="classes.wrapper">...</div>
    </template>
    <!-- ... --->
  </ElementLayout>
</template>

<script>
  import { ref } from 'vue' // from '@vue/composition-api' in Vue.js 2
  import VueformElement from '@vueform/vueform/element'

  export default VueformElement({
    name: 'CustomElement',
  }, {
    setup(props, { element }) {
      const defaultClasses = ref({
        container: '', // added automatically to the element's outermost DOM in ElementLayout
        wrapper: 'element-wrapper',
      })

      return {
        defaultClasses,
      }
    }
  })
</script>

Element Properties and Methods

We can dump context.element to see the available properties and methods for the element and consult TextElement's API reference to see what they do:

<script>
  import VueformElement from '@vueform/vueform/element'

  export default VueformElement({
    name: 'CustomElement',
  }, {
    setup(props, { element }) {
      console.log(element)
    }
  })
</script>

Registering Elements

New elements can be registered in vueform.config.js:

// vueform.config.js

import CustomElement from './path/to/CustomElement.vue'

export default {
  elements: [
    CustomElement,
  ],
  // ...
}

Examples

Simple Example

Here's how we can create a very simple text input field that uses the element's model and dynamic classes:

<template>
  <ElementLayout>
    <template #element>
      <input
        v-model="model"
        :class="classes.input"
      />
    </template>

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

<script>
  import { ref } from 'vue' // from '@vue/composition-api' in Vue.js 2
  import VueformElement from '@vueform/vueform/element'

  export default VueformElement({
    name: 'CustomElement',
  }, {
    setup(props, { element }) {
      const defaultClasses = ref({
        container: '', // added automatically to the element's outermost DOM in ElementLayout
        input: 'form-text-input',
        input_danger: 'has-errors',
        $input: (classes, { isDanger }) => ([
          classes.input,
          isDanger ? classes.input_danger : null,
        ])
      })

      return {
        defaultClasses,
      }
    }
  })
</script>

<style lang="scss">
  .form-text-input {
    border: 1px solid black;
    outline: none;
    width: 100%;

    &.has-errors {
      border: 1px solid red;
    }
  }
</style>

If a class name has a counterpart prefixed with $ function, it will be dynamic. The function's first argument is the class list and the second is the component properties.

Now if we use it we'll have a simple input field with a red border if it has any errors:

<Vueform>
  <CustomElement name="custom" rules="email" />
</Vueform>

Advanced Example

If we'd like to create a more advanced element in terms of

<template>
  <ElementLayout>
    <template #element>
      <div :class="classes.wrapper">
        <select v-model="day" :class="classes.day">
          <option v-for="day, i in days" :value="day" :key="i">
            {{ day }}
          </option>
       </select>
        <select v-model="month" :class="classes.month">
          <option v-for="month, i in months" :value="month[0]" :key="i">
            {{ month[1] }}
          </option>
       </select>
        <select v-model="year" :class="classes.year">
          <option v-for="year, i in years" :value="year" :key="i">
            {{ year }}
          </option>
       </select>
      </div>
    </template>
    <template v-for="(component, slot) in elementSlots" #[slot]><slot :name="slot" :el$="el$"><component :is="component" :el$="el$"/></slot></template>
  </ElementLayout>
</template>

<script>
  import VueformElement from '@vueform/vueform/element'
  import { ref, computed } from 'vue' // from '@vue/composition-api' in Vue.js 2

  export default VueformElement({
    name: 'BirthdayElement',
  }, {
    setup(props, { element }) {
      const months = ref([
        ['01', 'January'],
        ['02', 'February'],
        ['03', 'March'],
        ['04', 'April'],
        ['05', 'May'],
        ['06', 'June'],
        ['07', 'July'],
        ['08', 'August'],
        ['09', 'September'],
        ['10', 'October'],
        ['11', 'November'],
        ['12', 'December'],
      ])
      const days = ref([...Array(31).keys()].map(i=>i<9?'0'+(i+1):String(i+1)))
      const years = ref([...Array(100).keys()].map(i=>(new Date().getFullYear())-i))

      // Creating models for day, month, year from the element's model
      const datePart = (part) => {
        return {
          get: () => {
            return (element.model.value||'').split('-')[part]||null
          },
          set: (value) => {
            let date = (element.model.value||'').split('-')
            date[part] = value

            element.model.value = `${date[0]||'0000'}-${date[1]||'00'}-${date[2]||'00'}`
          }
        }
      }

      const day = computed(datePart(2))
      const month = computed(datePart(1))
      const year = computed(datePart(0))

      return {
        day,
        month,
        year,
        months,
        days,
        years,
      }
    },
    data() {
      return {
        defaultClasses: {
          container: '',
          wrapper: 'form-input-wrapper',
          input: 'form-text-input',
          day: 'day',
          month: 'month',
          year: 'year',
          $day: (classes) => ([
            classes.input,
            classes.day,
          ]),
          $month: (classes) => ([
            classes.input,
            classes.month,
          ]),
          $year: (classes) => ([
            classes.input,
            classes.year,
          ]),
        },
      }
    },
  })
</script>

<style lang="scss">
.form-input-wrapper {
  width: 100%;
  display: flex;
  column-gap: 0.5rem;
}

.form-text-input {
  border: 1px solid #000000;
  padding: 0.25rem 0.5rem;
  appearance: auto;

  &.day {
    width: calc(3 / 12 * 100%);
  }

  &.month {
    width: calc(5 / 12 * 100%);
  }

  &.year {
    width: calc(4 / 12 * 100%);
  }
}
</style>

We can use the created element as BirthdayElement:

<Vueform>
  <BirthdayElement name="birthday" label="Birthday" rules="after:2022-02-01" />
</Vueform>