Add a way to pass reactive arguments to custom directives
What problem does this feature solve?
I created a minimal reproduction of the issue. I know that this minimal reproduction can be solved with both composables and component just fine, but please believe me that for my use case a directive is the most pleasant developer experience.
Arguments in custom directives can be dynamic, but unfortunately they seem to get unref
d in the process?
https://codesandbox.io/s/reactive-argument-in-custom-directives-6bpg7t?file=/src/directive.js Here I would like the background color on hover to react to the state change when toggling.
What does the proposed API look like?
I am clueless to how the API would need to change, since we already do pass a reactive property. Maybe when we pass a computed property it can keep its reactivity?
refs are unwrapped in templates, that's not going to change in the current major version at least. You can only work around that. i.e. by using a plain wrapper object (which is not being unwrapped).
Also, custom directives are not set up to deal with reactive effects from watch()
etc. calls to watch() will not be unmounted automatically as you did not call in in setup
- you called it in a custom directive's hook. Even if they were, they would only be cleaned up when the component unmounts, which means that if the element is unmounted by v-if, the watcher stays active. not what you want, most of the time.
So right now you have watchers that stay in memory forever.
A more typical implementation of such a custom directive would look something like this:
const elsColors = new WeakMap()
const elsHandlers = new WeakMap()
export default {
mounted(el, binding) {
elsColors.set(el, binding.value)
const handler = (event) => {
console.log('handler', event.type)
el.style.backgroundColor = event.type === 'mouseenter'
? elsColors.get(el)
: ''
}
elsHandlers.set(el, handler)
el.addEventListener('mouseenter', handler)
el.addEventListener('mouseleave', handler)
},
updated(el, binding) {
elsColors.set(el, binding.value)
// I can't find a way to tell `mounted()` to use a different backgroundColor from here
// Also: it would be tedious to manually keep state in sync using updated-hook
},
beforeUnmoun(el) {
const handler = elsHandlers.get(el)
el.removeEventListener('mouseenter', handler)
el.removeEventListener('mouseleave', handler)
}
};
For most instances, you could likely even drop the whole handling of the unmounting as the event listeners should stop working when the element is being unmounted anyway, and disappears with GC. However, Vue could re-use that element for other purposes (i.e. in a v-if /v-else scenario), so cleaning up like this is safer.
Very interesting; I've tried something similar, since it felt wrong to use reactive properties in directives, but did not know about WeakMap
–
I'm wondering what the prefix els
is for?
Should you clean up inside elsColors
and elsHandlers
aswell in beforeUnmount()
?
PS: in case somebody copies your code: beforeUnmoun
is missing the t
el(ement)s
You dont have to clean up the maps because they are WeakMaps. When the element is being garbage collected, the maps entry is as well.