# Custom input types

If the input type you're looking for is not part of the built-in fields (mostly HTML5 input types), and you haven't found a plugin that adds the functionality you need — you can create input types of your own.

# Preamble: Input composition

A core concept in Vue Formulate is the ability for developers to compose their forms using a dead-simple API with a single component. True to that goal, when developing a custom input you should endeavour to maintain that same ease of composition by create your own type of FormulateInput, ensuring the syntax for creating forms and fields stays consistent even when using custom inputs.

This is notably different than many Vue libraries that make heavy use of scoped slots as the primary mechanism of extension. Vue Formulate also has robust support for scoped slots, but these should be used more for the occasional override rather than the customization of the entire utility.

A tell-tale sign that you could implement this principle better is if you find yourself doing a lot of copy-paste of scoped slots, or wrapping FormulateInput in wrapper components. If these patterns are creeping into your code consider using “Slot Components” or creating a custom inputs.

# Structure of an input

Before diving into code, let’s take a high-level look at how a FormulateInput component is structured:

FormulateInput internal structure

# Custom types

Let’s tackle an autocomplete field. Our goal is to extend Vue Formulate to allow for new inputs that look like this:

<FormulateInput
  type="autocomplete"
  name="user"
  label="Search for a user"
  :options="[
    { value: 1, label: 'Jon Doe'},
    { value: 2, label: 'Jane Roe'},
    { value: 3, label: 'Bob Foe'},
    { value: 4, label: 'Ben Cho'},
  ]"
/>

To do this, we need to write a custom component that handles the "autocomplete" input logic. Each type is designated as a component and a classification. Both components and classifications can be shared across multiple types.

# What is a classification?

A classification is just a helpful way to group logic and styling rules around similar input types, but just because you create a new input type doesn’t necessarily mean you would create a new grouping classification. In fact, our example autocomplete would fit well under the text classification.

# What is an input component?

The input component, on the other hand, is a Vue component that is passed a context object prop (the context object is responsible nearly all of the component’s functionality — it’s a good idea to familiarize yourself with it). Additionally your custom input can be passed it’s own props defined as slotProps. This custom component can be used for entirely new input systems, business logic, or custom UI.

# The model

If you want field validation, form aggregation, hydration and the other benefits of Vue Formulate there is only one requirement: the value of the field should be read from context.model and written to context.model. This is a special getter/setter property bound to the root <FormulateInput />.

# Custom events

Your custom component can also emit events on the root <FormulateInput> by using context.rootEmit() exactly the same as you would use $emit on any other Vue component.

View example autocomplete source

File: MyFormulateAutocomplete.vue

<template>
  <div
    :class="`formulate-input-element formulate-input-element--${context.class}`"
    :data-type="context.type"
  >
    <input
      type="text"
      v-model="context.model"
      v-bind="context.attributes"
      autocomplete="no"
      @keydown.enter.prevent="context.model = selection.label"
      @keydown.down.prevent="increment"
      @keydown.up.prevent="decrement"
      @blur="context.blurHandler"
    >
    <ul
      v-if="filteredOptions.length"
      class="formulate-input-dropdown"
    >
      <li
        v-for="(option, index) in filteredOptions"
        :key="option.value"
        v-text="option.label"
        :data-is-selected="selection && selection.value === option.value"
        @mouseenter="selectedIndex = index"
        @click="context.model = selection.label"
      />
    </ul>
  </div>
</template>

<script>
export default {
  props: {
    context: {
      type: Object,
      required: true
    },
  },
  data () {
    return {
      selectedIndex: 0
    }
  },
  watch: {
    model () {
      this.selectedIndex = 0
    }
  },
  computed: {
    model () {
      return this.context.model
    },
    selection () {
      if (this.filteredOptions[this.selectedIndex]) {
        return this.filteredOptions[this.selectedIndex]
      }
      return false
    },
    filteredOptions () {
      if (Array.isArray(this.context.options) && this.context.model) {
        const isAlreadySelected = this.context.options.find(option => option.label === this.context.model)
        if (!isAlreadySelected) {
          return this.context.options
            .filter(option => option.label.toLowerCase().includes(this.context.model.toLowerCase()))
        }
      }
      return []
    }
  },
  methods: {
    increment () {
      const length = this.filteredOptions.length
      if (this.selectedIndex + 1 < length) {
        this.selectedIndex++
      } else {
        this.selectedIndex = 0
      }
    },
    decrement () {
      const length = this.filteredOptions.length
      if (this.selectedIndex - 1 >= 0) {
        this.selectedIndex--
      } else {
        this.selectedIndex = length - 1
      }
    }
  }
}
</script>

Note: in the above example we wrap our <template> with a div containing some .formulate-input-element classes, this is not required, but is a good practice to keep things consistent for theme authors.

# Custom props

Often, when creating custom inputs, you’ll need to pass custom props, such as a url or a debounce duration. Custom inputs can also declare they want to receive their own props directly. This is done by leveraging the slotProp (opens new window) mechanism under the component key during registration. For example to expose a url prop to a custom input named myinput:

// Vue Formulate config:
{
  library: {
    myinput: {
      classification: 'myclassification',
      component: 'MyFormulateInput',
      slotProps: {
        component: ['url']
      }
    }
  }
}

# Registering an input

Once your input component is written, you need to let Vue Formulate know there is a new type of input and it has a custom component (and/or classification). You do this by extending the library global option.

import Vue from 'vue'
import VueFormulate from '@braid/vue-formulate'
import MyFormulateAutocomplete from './MyFormulateAutocomplete'

// register your component with Vue
Vue.component('MyFormulateAutocomplete', MyFormulateAutocomplete)

Vue.use(VueFormulate, {
  library: {
    autocomplete: {
      classification: 'text',
      component: 'MyFormulateAutocomplete'
    }
  }
})

Presto! You’ve now extended Vue Formulate to include a custom input type.