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:
// 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
:
<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:
// 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:
// 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.