Wrapper Vue2/3 directives to allow dynamic add/remove
A composable that converts a Vue2/3 directive in a directive that can be dynamically adder/removed.
A composable that converts a Vue2/3 directive in a directive that can be dynamically adder/removed.
When using directives in render functions in Vue3 components the beforeUnmount
and unmounted
lifecycle hook are not called when the directive is no longer applied.
This can lead to some unwanted leftovers that were cleaned in beforeUnmount
and unmounted
lifecycle hook.
The useRemovableDirective
composable receives a directive definition (Vue2 or Vue3) and returns a list with two directives:
Then, in your render function, instead of not applying your directive in withDirectives
when it is not needed you replace it with the removed
verion.
<script land="ts">
import {
defineComponent,
h,
ref,
withDirectives,
} from 'vue';
import useRemovableDirective from './useRemovableDirective.ts';
const logs = ref([]);
const [vColor, vColorRemoved] = useRemovableDirective({
// called before bound element's attributes
// or event listeners are applied
created(el /* , binding, vnode, prevVnode */) {
logs.value.push({ event: 'created', el: el.className, ts: (new Date()).toISOString().slice(11, 23) });
},
// called right before the element is inserted into the DOM.
beforeMount(el /* , binding, vnode, prevVnode */) {
logs.value.push({ event: 'beforeMount', el: el.className, ts: (new Date()).toISOString().slice(11, 23) });
},
// called when the bound element's parent component
// and all its children are mounted.
mounted(el, binding /* , vnode, prevVnode */) {
logs.value.push({ event: 'mounted', el: el.className, ts: (new Date()).toISOString().slice(11, 23) });
if (el.ctx === undefined) {
el.ctx = { color: el.style.color };
}
el.style.color = binding.value;
},
// called before the parent component is updated
beforeUpdate(el /* , binding, vnode, prevVnode */) {
logs.value.push({ event: 'beforeUpdate', el: el.className, ts: (new Date()).toISOString().slice(11, 23) });
},
// called after the parent component and
// all of its children have updated
updated(el, binding /* , vnode, prevVnode */) {
logs.value.push({ event: 'updated', el: el.className, ts: (new Date()).toISOString().slice(11, 23) });
if (el.ctx === undefined) {
el.ctx = { color: el.style.color };
}
el.style.color = binding.value;
},
// called before the parent component is unmounted
beforeUnmount(el /* , binding, vnode, prevVnode */) {
logs.value.push({ event: 'beforeUnmount', el: el.className, ts: (new Date()).toISOString().slice(11, 23) });
},
// called when the parent component is unmounted
unmounted(el /* , binding, vnode, prevVnode */) {
logs.value.push({ event: 'unmounted', el: el.className, ts: (new Date()).toISOString().slice(11, 23) });
if (el.ctx !== undefined) {
Object.assign(el.style, el.ctx);
}
},
});
const testBlock = defineComponent({
props: {
text: String,
active: Boolean,
},
setup(props) {
const active = ref(props.active === true);
return () => withDirectives(
h('div', { class: props.active === true ? 'start-applied' : 'start-not-applied' }, [
h('label', [
h('input', { type: 'checkbox', checked: active.value, onInput(ev) { active.value = ev.target.checked; } }),
`Apply directive (initially ${ props.active === true ? '' : 'not ' }applied)`,
]),
` / ${ props.text.length % 2 === 0 ? 'red' : 'green' } / ${ props.text }`,
]),
active.value === true
? [[vColor, props.text.length % 2 === 0 ? 'red' : 'green']]
: [[vColorRemoved]],
);
},
});
const eventColor = (event) => {
if (['created', 'beforeMount', 'mounted'].includes(event)) {
return 'blue';
}
return ['beforeUpdate', 'updated'].includes(event)
? 'grey'
: 'orangered';
};
const logsBlock = defineComponent({
setup() {
return () => h(
'ol',
logs.value.slice().reverse().map((row) => h(
'li',
{ style: { display: 'flex' } },
[
h('div', { style: { width: '20ch' } }, row.ts),
h('div', { style: { width: '20ch', color: row.el.indexOf('-not-') > -1 ? 'orangered' : 'green' } }, row.el),
h('div', { style: { color: eventColor(row.event) } }, row.event),
],
)),
);
},
});
export default defineComponent({
setup() {
const msg = ref('');
return () => h('div', { style: 'padding: 16px' }, [
h(testBlock, { text: msg.value, active: true }),
h(testBlock, { text: msg.value, active: false }),
h('input', { value: msg.value, onInput(ev) { msg.value = ev.target.value; } }),
h(logsBlock),
]);
},
});
</script>
function getSSRProps() { }
export default function useRemovableDirective(dirDef) {
const dirDefId = Symbol('id');
const dirDefAdded = typeof dirDef === 'function'
? { getSSRProps, mounted: dirDef, updated: dirDef }
: { getSSRProps, ...dirDef };
const dirDefRemoved = { getSSRProps };
const {
created,
beforeMount,
mounted,
beforeUpdate,
beforeUnmount,
unmounted,
} = dirDefAdded;
dirDefAdded.created = typeof created === 'function'
? (el, binding, vnode, prevVnode) => {
el[dirDefId] = true;
created(el, binding, vnode, prevVnode);
}
: (el) => {
el[dirDefId] = true;
};
if (
typeof created === 'function'
|| typeof beforeMount === 'function'
|| typeof mounted === 'function'
) {
const calls = [];
if (typeof created === 'function') { calls.push(created); }
if (typeof beforeMount === 'function') { calls.push(beforeMount); }
if (typeof mounted === 'function') { calls.push(mounted); }
dirDefAdded.beforeUpdate = typeof beforeUpdate === 'function'
? (el, binding, vnode, prevVnode) => {
if (el[dirDefId] !== true) {
el[dirDefId] = true;
calls.forEach((call) => { call(el, binding, vnode, prevVnode); });
}
beforeUpdate(el, binding, vnode, prevVnode);
}
: (el, binding, vnode, prevVnode) => {
if (el[dirDefId] !== true) {
el[dirDefId] = true;
calls.forEach((call) => { call(el, binding, vnode, prevVnode); });
}
};
}
if (typeof beforeUnmount === 'function') {
dirDefRemoved.beforeUpdate = (el, binding, vnode, prevVnode) => {
if (el[dirDefId] === true) {
beforeUnmount(el, binding, vnode, prevVnode);
}
};
}
dirDefRemoved.updated = typeof unmounted === 'function'
? (el, binding, vnode, prevVnode) => {
if (el[dirDefId] === true) {
unmounted(el, binding, vnode, prevVnode);
el[dirDefId] = false;
}
}
: (el) => {
if (el[dirDefId] === true) {
el[dirDefId] = false;
}
};
return [dirDefAdded, dirDefRemoved];
}