Add global addon to elements

Learn how to apply an addon on a config level.

Simple Solution

The easiest way to add an addon globally to an element, eg. LocationElement we can create a plugin and override its addons prop:

js
// vueform.config.js

import { defineConfig } from '@vueform/vueform'
import { h } from 'vue'

const ClearLocationComponent = {
  inject: ['el$'],
  render() {
    let el$ = this.el$

    return h('div', {
      onClick() {
        el$.reset()
      },
    }, '×')
  }
}

const LocationAddonPlugin = {
  apply: 'LocationElement',
  props: {
    addons: {
      type: Object,
      required: false,
      default: () => ({
        after: ClearLocationComponent
      })
    }
  }
}

export default defineConfig({
  plugins: [
    LocationAddonPlugin,
  ]
})

The only caveat is that if we still need to use a before addon, we have to manually include our component as well for after:

vue
<template>
  <Vueform>
    <LocationElement name="location" :addons="{
      before: '<span class="icon-pin"></span>',
      after: ClearLocationComponent,
    }">
  </Vueform>
</template>

<script setup>
import ClearLocationComponent from './ClearLocationComponent.vue'
</script>

More Complex Solution

If we want the after addon to truely behave as a default addon for LocationElement, so being able to add before addon without having to reassign the after addon we can take a bit more complex approach.

First we check out how the addons are rendered in LocationElement: https://github.com/vueform/vueform/tree/1.9.0/themes/blank/templates/elements/LocationElement.vue#L9

We can see that ElementAddon is rendered if hasAddonAfter is truthy, so let's see how that looks like: https://github.com/vueform/vueform/tree/1.9.0/src/composables/elements/useAddons.js#L18

Ok, so we know, we can force-render the after addon probably by overriding hasAddonAfter in a plugin. Next we need to set a value for fieldSlots['addon-after'] in all cases: https://github.com/vueform/vueform/tree/1.9.0/themes/blank/templates/elements/LocationElement.vue#L10

We check out how fieldSlots are defined: https://github.com/vueform/vueform/tree/1.9.0/src/composables/elements/useSlots.js#L73

So if we extend it and manually add the addon-after value in our plugin, we should be good to go.

Let's create the first part of our plugin:

js
// vueform.config.js

import { h } from 'vue'
import { defineConfig } from '@vueform/vueform'

const ClearLocationComponent = {
  inject: ['el$'],
  render() {
    let el$ = this.el$

    return h('div', {
      onClick() {
        el$.reset()
      },
    }, '×')
  }
}

const LocationAddonPlugin = () => ([
  {
    apply: 'LocationElement',
    setup(props, context, component) {
      const {
        fieldSlots: fieldSlotsBase,
      } = component

      /**
       * Extended from here: 
       * https://github.com/vueform/vueform/blob/main/themes/blank/templates/elements/LocationElement.vue#L10
       */
      const fieldSlots = computed(() => {
        let fieldSlots = { ...fieldSlotsBase.value }

        // We manually add our component as an after addon
        fieldSlots['addon-after'] = ClearLocationComponent

        return fieldSlots
      })

      /**
       * Overrides this:
       * https://github.com/vueform/vueform/tree/1.9.0/src/composables/elements/useAddons.js#L18
       */
      const hasAddonAfter = ref(true)

      return {
        ...component,
        fieldSlots,
        hasAddonAfter,
      }
    }
  },
])

export default defineConfig({
  plugins: [
    LocationAddonPlugin,
  ]
})

This would work in itself if ElementAddon was not doing an extra check to decide how it should render the addon:
https://github.com/vueform/vueform/blob/1.9.0/themes/blank/templates/ElementAddon.vue#L3
https://github.com/vueform/vueform/blob/1.9.0/src/components/ElementAddon.js#L55

It seems like baseAddon needs to be overwritten as well because it will look for the addon in the LocationElement's addons prop, that is not set.

So let's add the second part of our plugin and extend ElementAddon to make sure our component is returned when needed:

js
// vueform.config.js

import localize from '@vueform/vueform/src/utils/localize'
import isVueComponent from '@vueform/vueform/src/utils/isVueComponent'
import { h, toRefs, computed, ref, inject } from 'vue'
import { defineConfig } from '@vueform/vueform'

const ClearLocationComponent = {
  inject: ['el$'],
  render() {
    let el$ = this.el$

    return h('div', {
      onClick() {
        el$.reset()
      },
    }, '×')
  }
}

const LocationAddonPlugin = () => ([
  {
    apply: 'LocationElement',
    setup(props, context, component) {
      const {
        fieldSlots: fieldSlotsBase,
      } = component

      /**
       * Extended from here: 
       * https://github.com/vueform/vueform/blob/main/themes/blank/templates/elements/LocationElement.vue#L10
       */
      const fieldSlots = computed(() => {
        let fieldSlots = { ...fieldSlotsBase.value }

        // We manually add our component as an after addon
        fieldSlots['addon-after'] = ClearLocationComponent

        return fieldSlots
      })

      /**
       * Overrides this:
       * https://github.com/vueform/vueform/tree/1.9.0/src/composables/elements/useAddons.js#L18
       */
      const hasAddonAfter = ref(true)

      return {
        ...component,
        fieldSlots,
        hasAddonAfter,
      }
    }
  },
  {
    apply: 'ElementAddon',
    setup(props, context, component) {
      const { type } = toRefs(props)

      const {
        form$,
        el$,
      } = component
      
      // Don't apply the plugin to non-location elements
      if (el$.value.type !== 'location') {
        return component
      }

      // =============== INJECT ===============

      const config$ = inject('config$')

      // ============== COMPUTED ==============

      /**
       * This is where we need to make sure we return our component for 'after'
       * https://github.com/vueform/vueform/tree/1.9.0/src/components/ElementAddon.js#L46
       */
      const baseAddon = computed(() => {
        // Manually provide the component for 'after' type
        if (type.value === 'after') {
          return ClearLocationComponent
        }

        return el$.value.addons[type.value]
      })

      /**
       * These are all needed to be copied here because they use baseAddon, that we overwritten:
       * https://github.com/vueform/vueform/tree/1.9.0/src/components/ElementAddon.js#L55-85
       */
      const addon = computed(() => {
        let addon = isAddonFunction.value ? baseAddon.value(el$.value) : baseAddon.value || /* istanbul ignore next: failsafe */ null

        if (!isAddonComponent.value) {
          addon = localize(addon, config$.value, form$.value)
        }

        return addon
      })

      const isAddonFunction = computed(() => {
        return typeof baseAddon.value === 'function' && (!baseAddon.value.prototype || !baseAddon.value.prototype.constructor || (baseAddon.value.prototype.constructor && baseAddon.value.prototype.constructor.name !== 'VueComponent'))
      })

      const isAddonComponent = computed(() => {
        return isVueComponent(baseAddon.value)
      })

      return {
        ...component,
        isAddonFunction,
        isAddonComponent,
        addon,
      }
    }
  }
])

export default defineConfig({
  plugins: [
    LocationAddonPlugin,
  ]
})

Note that we only wanted to override baseAddon but as other computed variables are based on that, we needed to include those (without modification) in our plugin as well.

You can see that writing plugins can be a bit cumbersome sometimes and requires discovering and understanding Vueform's source. Of course it's always better to have everything out-of-the-box, but when it's not possible, at least our hands are not tied and we can achieve what we want. Vueform was built with extensibility in mind and this example show how flexible it can be.

If you have a similar problem that goes beyond your understanding of Vueform's source code, feel free to open a Discussion on GitHub or post it on our Discord.

  
👋 Hire Vueform team for form customizations and developmentLearn more