formkit / auto-animate

A zero-config, drop-in animation utility that adds smooth transitions to your web app. You can use it with React, Vue, or any other JavaScript application.
https://auto-animate.formkit.com
MIT License
11.96k stars 210 forks source link

feat(vue): allow useAutoAnimate with Vue component ref #186

Open marshallswain opened 6 months ago

marshallswain commented 6 months ago

This adds support for using useAutoAnimate with a component as the parent.

When you add ref="parent" to a component, the element is found at parent.value.$el. This update checks for component elements or a plain HTML element ref.

Without this change, you have to create a proxy ref to extract the element in the onMounted hook, like this:

<script setup lang="ts">
const [autoAnimateParent, enable] = useAutoAnimate()

// create a "proxy" dragParent ref which will hold a ref to the component.
const dragParent = ref()

// assign the component's $el to the autoAnimateParent once the DOM mounts
onMounted(() => (autoAnimateParent.value = dragParent.value?.$el))
</script>

<template>
  <VueDraggable
    ref="dragParent"
    v-model="items"
    :animation="300"
  >
    <!-- ... -->
  </VueDraggable>
</template>

This PR doesn't address the types. I'm not sure the best way to handle that. I did pull the code into a local repo to try it out and took this screenshot of the type issue.

Screenshot 2023-12-19 at 7 39 52 PM

If you want to instruct me how to proceed on the types, I'll update the code. Or feel free to edit this PR directly.

vercel[bot] commented 6 months ago

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
auto-animate ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 20, 2023 3:47am
marshallswain commented 6 months ago

I just noticed this other PR. https://github.com/formkit/auto-animate/pull/159

I think their objective is the same, based on this comment: https://github.com/formkit/auto-animate/pull/159#issuecomment-1704555164.

marshallswain commented 6 months ago

Sorry about being lazy with the types. I'm not a TS ninja by any means, but I took a crack at it. Maybe you'll see a more optimal way to handle the ElementOrComponentRef type:

import { onMounted, ref, watchEffect } from 'vue'
import type { Directive, Plugin, Ref } from 'vue'
import type {
  AnimationController,
  AutoAnimateOptions,
  AutoAnimationPlugin,
} from '@formkit/auto-animate'
import autoAnimate, { vAutoAnimate as autoAnimateDirective,
} from '@formkit/auto-animate'

export const vAutoAnimate: Directive<
  HTMLElement,
  Partial<AutoAnimateOptions>
> = autoAnimateDirective

export const autoAnimatePlugin: Plugin = {
  install(app) {
    app.directive('auto-animate', vAutoAnimate)
  },
}

export type ElementOrComponentRef<T extends Element> = T & { $el?: Element }

/**
 * AutoAnimate hook for adding dead-simple transitions and animations to Vue.
 * @param options - Auto animate options or a plugin
 * @returns A template ref. Use the `ref` attribute of your parent element
 * to store the element in this template ref.
 */
export function useAutoAnimate<T extends Element>(
  options?: Partial<AutoAnimateOptions> | AutoAnimationPlugin,
): [Ref<ElementOrComponentRef<T>>, (enabled: boolean) => void] {
  const element = ref<ElementOrComponentRef<T>>()
  let controller: AnimationController | undefined
  function setEnabled(enabled: boolean) {
    if (controller)
      enabled ? controller.enable() : controller.disable()
  }
  onMounted(() => {
    watchEffect(() => {
      const el = element.value?.$el || element.value
      if (el instanceof HTMLElement)
        controller = autoAnimate(el, options || {})
    })
  })

  return [element as Ref<T>, setEnabled]
}