core icon indicating copy to clipboard operation
core copied to clipboard

Activated hook for directives

Open syuilo opened this issue 4 years ago • 11 comments

What problem does this feature solve?

component's keep-alive activated and deactivated hooks should also be available in directives.

For example, suppose you have a directive that adds a class based on the size of an element. When the component to which the directive is added is in a keep-alive, the directive's mounted hook will be called, even if it is not actually mounted in the DOM, and the size of the element will be zero. If we can detect that a component with a directive bound to it is now active, we can get the correct size.

Thank you!

What does the proposed API look like?

app.directive('my-directive', {
  activated() { ... },
  deactivated() { ... },
})

syuilo avatar Oct 10 '20 15:10 syuilo

This would be very helpful my v-shared-element library as well. I hope this gets implemented!

justintaddei avatar Oct 11 '20 15:10 justintaddei

My use case is v-keep-scroll directive. In Chrome elements loose their scroll positions when detouched from DOM, so the directive fixes it. It saves scroll positions in attributes and restores it when component is activated. Can't port it to Vue 3. It used non-public 'hook:activated' event in Vue 2.

shameleo avatar Nov 04 '20 08:11 shameleo

Note: at least for Browser environments, you should be able to use Node.isConnected (at least for some use cases) to check wether the element is in the DOM (=component is active) or not (=component is deactivatdd by keep-alive)

LinusBorg avatar Nov 04 '20 13:11 LinusBorg

Yep, but I need event, not just property.

shameleo avatar Nov 05 '20 05:11 shameleo

Same thing here. I'm using the activated hook to tell when I need to record the position of the elements and animate from their previous state. Simply haven't a property would not work for my case.

justintaddei avatar Nov 05 '20 06:11 justintaddei

I rethinked my issue and created abstract component instead of directive. Maybe it will help somebody

Code is here
  // MIT License

  import { withDirectives, onActivated } from 'vue';

  const onScroll = ({ target }) => {
      target.dataset.scrollPos = target.scrollLeft + '-' + target.scrollTop;
  };
  const onScrollOptions = { passive: true };
  
  export default {
      setup(props, { slots }) {
          let els = [];

          const saveScroll = {
              created(el) {
                  el.addEventListener('scroll', onScroll, onScrollOptions);
                  els.push(el);
              },
              unmounted(el) {
                  el.removeEventListener('scroll', onScroll, onScrollOptions);
                  els = els.filter(e => e !== el);
              }
          };

          onActivated(() => {
              els.forEach(el => {
                  const { scrollPos } = el.dataset;

                  if (scrollPos) {
                      const pos = scrollPos.split('-');
                      el.scrollLeft = pos[0];
                      el.scrollTop = pos[1];
                  }
              });
          });

          return () => slots.default().map(vnode => withDirectives(vnode, [[ saveScroll ]]));
      }
  }

It is even more appropriate as before (using Vue 2) I used activated hook of context, and context is not a parent in general case, but simply component which template applies directive. Consider this:

Component.vue

<template>
    ...
    <keep-alive>
        <SubComponent v-if="isHidden">
            <div v-my-directive></div>
        </SubComponent>
    </keep-alive>
    ...
</template>

vnode.context.$on('hook:activated') in directive (using Vue 2) subscribes on activated event of Component, instead of SubComponent. So if we really (?) need activated hook in directives, which component should we keep in mind?

shameleo avatar Feb 18 '21 12:02 shameleo

Hi, I was using the vnode.context.$on('hook:activated') too in vue 2 in a custom directive. I am migrating to vue 3 and this is not working anymore because there is no $on, $off, $once methods anymore. (but the event is still emitted!) The abstract component solution is not fitting for me, I'd like a way to react on an event when the element is activated or deactivated. Is there something in vue instead that I can use in directives ? on maybe a native listener that will fire on Node.isConnected value change ?

benavern avatar Aug 10 '21 08:08 benavern

+100 for this

Miguelklappes avatar Aug 24 '21 21:08 Miguelklappes

yes please, add this

arabyalhomsi avatar Jul 25 '22 21:07 arabyalhomsi

I was able to do this

function activated() {
    console.log("activated")
}

function deactivated() {
    console.log("deactivated")
}

const isActive = ref(false)
watch(isActive, () => {
    isActive.value ? activated() : deactivated()
})

export default {
    beforeUpdate(el, binding, vnode) {
        isActive.value = vnode.el.isConnected
    }
}

funkyvisions avatar Dec 02 '22 18:12 funkyvisions

Something like this can work too (works well when you have multiple instances, unlike the one above)

    beforeUpdate(el) {

        if (el.isConnected && !el.isActive) {
            el.isActive = true
            activated(el)
        }

        if (!el.isConnected && el.isActive) {
            el.isActive = false
            deactivated(el)
        }
    }

funkyvisions avatar Dec 02 '22 21:12 funkyvisions

Something like this can work too (works well when you have multiple instances, unlike the one above)

    beforeUpdate(el) {

        if (el.isConnected && !el.isActive) {
            el.isActive = true
            activated(el)
        }

        if (!el.isConnected && el.isActive) {
            el.isActive = false
            deactivated(el)
        }
    }

The issue with this solution is that beforeUpdate doesn't fire in all cases when the component is deactivated. so the isActive property doesn't reflect the actual state and can trigger autofocus when it's not desired.

Our team went with the composable route instead. Our issue was with the autofocus directive not able to fire when the component gets activated

import { onMounted, onActivated, Ref } from 'vue'

export function focusElement(targetInput: HTMLInputElement | null) {
	if (!targetInput) {
		return
	}

	targetInput.focus()
}

export function useAutofocus(inputRef?: Ref<HTMLInputElement | null>) {
	onMounted(() => {
		if (inputRef) {
			focusElement(inputRef.value)
		}
	})

	onActivated(() => {
		if (inputRef) {
			focusElement(inputRef.value)
		}
	})
}
setup(props) {
	const inputRef = ref<HTMLInputElement | null>(null)

	useAutofocus(inputRef)

	return {
		inputRef,
	}
},

samueleiche avatar May 08 '23 09:05 samueleiche

I reworked this today using a component from vueuse

    mounted(el) {
        const visible = useElementVisibility(el)
        watch(visible, () => {
            if (visible.value) {
                activated(el)
            } else {
                deactivated(el)
            }
        })
    }

funkyvisions avatar Feb 20 '24 15:02 funkyvisions