From 94ef3645bb32da0aac8306242c64ee06c48cd1ac Mon Sep 17 00:00:00 2001
From: ZhouY11 <79828139+ZhouY11@users.noreply.github.com>
Date: Sat, 22 Jun 2024 19:27:17 +0800
Subject: [PATCH] fix(component): preserve variant style on rerender (#203)

Co-authored-by: Bobbie Goede <bobbiegoede@gmail.com>
---
 src/utils/component.ts   | 13 +++++++++++++
 tests/components.spec.ts | 38 ++++++++++++++++++++++++++++++++++----
 2 files changed, 47 insertions(+), 4 deletions(-)

diff --git a/src/utils/component.ts b/src/utils/component.ts
index 90268855..5a67496b 100644
--- a/src/utils/component.ts
+++ b/src/utils/component.ts
@@ -17,6 +17,7 @@ import type {
   Variant,
 } from '../types/variants'
 import { useMotion } from '../useMotion'
+import { variantToStyle } from './transform'
 
 /**
  * Type guard, checks if passed string is an existing preset
@@ -228,6 +229,18 @@ export function setupMotionComponent(
       )
     }
 
+    /**
+     * Vue reapplies all styles every render, include style properties and calculated initially styles get reapplied every render.
+     * To prevent this, reapply the current motion state styles in vnode updated lifecycle
+     */
+    node.props.onVnodeUpdated = ({ el }) => {
+      const styles = variantToStyle(instances[index].state as Variant)
+
+      for (const [key, val] of Object.entries(styles)) {
+        (el as any).style[key] = val
+      }
+    }
+
     return node
   }
 
diff --git a/tests/components.spec.ts b/tests/components.spec.ts
index bd856353..70095238 100644
--- a/tests/components.spec.ts
+++ b/tests/components.spec.ts
@@ -1,7 +1,7 @@
 import { config, mount } from '@vue/test-utils'
 import { describe, expect, it } from 'vitest'
-import { h, nextTick } from 'vue'
-import { MotionPlugin } from '../src'
+import { h, nextTick, ref } from 'vue'
+import { MotionComponent, MotionPlugin } from '../src'
 import MotionGroup from '../src/components/MotionGroup'
 import { intersect } from './utils/intersectionObserver'
 import { getTestComponent, useCompletionFn, waitForMockCalls } from './utils'
@@ -10,8 +10,8 @@ import { getTestComponent, useCompletionFn, waitForMockCalls } from './utils'
 config.global.plugins.push(MotionPlugin)
 
 describe.each([
-  { t: 'directive', name: '`v-motion` directive' },
-  { t: 'component', name: '`<Motion>` component' },
+  { t: 'directive', name: '`v-motion` directive (shared tests)' },
+  { t: 'component', name: '`<Motion>` component (shared tests)' },
 ])(`$name`, async ({ t }) => {
   const TestComponent = getTestComponent(t)
 
@@ -136,6 +136,36 @@ describe.each([
   })
 })
 
+describe('`<Motion>` component', async () => {
+  it('#202 - preserve variant style on rerender', async () => {
+    const counter = ref(0)
+
+    const wrapper = mount(
+      { render: () => h(MotionComponent, null, () => counter.value) },
+      {
+        props: {
+          initial: { scale: 1 },
+          enter: { scale: 2 },
+          duration: 10,
+        },
+      },
+    )
+
+    const el = wrapper.element as HTMLDivElement
+    await nextTick()
+
+    // Renders enter
+    expect(el.style.transform).toEqual('scale(2) translateZ(0px)')
+
+    // Trigger rerender by updating slot variable
+    counter.value++
+    await nextTick()
+
+    // Variant style is preserved after rerender/update
+    expect(el.style.transform).toEqual('scale(2) translateZ(0px)')
+  })
+})
+
 describe('`<MotionGroup>` component', async () => {
   it('child node can overwrite helpers', async () => {
     const wrapper = mount({