Skip to content

Commit

Permalink
Adding animation events to motion value
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgperry committed Dec 15, 2022
1 parent f071d0d commit 44744e0
Show file tree
Hide file tree
Showing 15 changed files with 89 additions and 76 deletions.
4 changes: 2 additions & 2 deletions dev/examples/Drag-external-handlers.tsx
Expand Up @@ -44,7 +44,7 @@ export const App = () => {
)

useEffect(() => {
return transform.onChange(v => console.log(v))
return transform.on("change", (v) => console.log(v))
})

return (
Expand All @@ -54,7 +54,7 @@ export const App = () => {
_dragX={x}
_dragY={y}
dragConstraints={ref}
onMeasureDragConstraints={constraints => constraints}
onMeasureDragConstraints={(constraints) => constraints}
style={{
backgroundColor: color,
...child,
Expand Down
2 changes: 1 addition & 1 deletion dev/examples/ThreeSwitch.tsx
Expand Up @@ -114,7 +114,7 @@ export function useAnimatedText(target, textTransition) {
useEffect(() => {
ref.current.innerText = target.toFixed(2)

return value.onChange((v) => {
return value.on("change", (v) => {
ref.current.innerText = v.toFixed(2)
})
})
Expand Down
2 changes: 1 addition & 1 deletion dev/examples/useScroll.tsx
Expand Up @@ -47,7 +47,7 @@ export const Example = () => {
const yRange = useTransform(scrollYProgress, [0, 0.9], [0, 1])
const pathLength = useSpring(yRange, { stiffness: 400, damping: 90 })

useEffect(() => yRange.onChange((v) => setIsComplete(v >= 1)), [yRange])
useEffect(() => yRange.on("change", (v) => setIsComplete(v >= 1)), [yRange])

return (
<div
Expand Down
2 changes: 1 addition & 1 deletion dev/examples/useViewportScroll.tsx
Expand Up @@ -46,7 +46,7 @@ export const Example = () => {
const yRange = useTransform(scrollYProgress, [0, 0.9], [0, 1])
const pathLength = useSpring(yRange, { stiffness: 400, damping: 90 })

useEffect(() => yRange.onChange((v) => setIsComplete(v >= 1)), [yRange])
useEffect(() => yRange.on("change", (v) => setIsComplete(v >= 1)), [yRange])

return (
<>
Expand Down
2 changes: 1 addition & 1 deletion dev/tests/drag-to-reorder.tsx
Expand Up @@ -13,7 +13,7 @@ const Item = ({ item, axis }) => {

useEffect(() => {
let isActive = false
axisValue.onChange((latestY) => {
axisValue.on("change", (latestY) => {
const wasActive = isActive
if (latestY !== 0) {
isActive = true
Expand Down
Expand Up @@ -385,7 +385,7 @@ describe("animate prop as object", () => {
test("respects repeatDelay prop", async () => {
const promise = new Promise<number>((resolve) => {
const x = motionValue(0)
x.onChange(() => {
x.on("change", () => {
setTimeout(() => resolve(x.get()), 50)
})
const Component = () => (
Expand Down
4 changes: 2 additions & 2 deletions packages/framer-motion/src/motion/__tests__/variant.test.tsx
Expand Up @@ -511,7 +511,7 @@ describe("animate prop as variant", () => {

React.useEffect(
() =>
a.onChange((latest) => {
a.on("change", (latest) => {
if (latest >= 1 && b.get() === 0) resolve(true)
}),
[a, b]
Expand Down Expand Up @@ -564,7 +564,7 @@ describe("animate prop as variant", () => {

React.useEffect(
() =>
a.onChange((latest) => {
a.on("change", (latest) => {
if (latest >= 1 && b.get() === 0) resolve(true)
}),
[a, b]
Expand Down
8 changes: 6 additions & 2 deletions packages/framer-motion/src/render/VisualElement.ts
Expand Up @@ -412,7 +412,8 @@ export abstract class VisualElement<
private bindToMotionValue(key: string, value: MotionValue) {
const valueIsTransform = transformProps.has(key)

const removeOnChange = value.onChange(
const removeOnChange = value.on(
"change",
(latestValue: string | number) => {
this.latestValues[key] = latestValue
this.props.onUpdate &&
Expand All @@ -424,7 +425,10 @@ export abstract class VisualElement<
}
)

const removeOnRenderRequest = value.onRenderRequest(this.scheduleRender)
const removeOnRenderRequest = value.on(
"renderRequest",
this.scheduleRender
)

this.valueSubscriptions.set(key, () => {
removeOnChange()
Expand Down
Expand Up @@ -52,8 +52,8 @@ describe("useMotionValue", () => {
const onRenderRequest = jest.fn()
const Component = () => {
const x = useMotionValue(100)
x.onChange(onChange)
x.onRenderRequest(onRenderRequest)
x.on("change", onChange)
x.on("renderRequest", onRenderRequest)

x.set(500)

Expand Down
Expand Up @@ -13,7 +13,7 @@ describe("useSpring", () => {
const x = useSpring(0)

React.useEffect(() => {
x.onChange((v) => resolve(v))
x.on("change", (v) => resolve(v))
x.set(100)
})

Expand All @@ -37,7 +37,7 @@ describe("useSpring", () => {
const y = useSpring(x)

React.useEffect(() => {
y.onChange((v) => resolve(v))
y.on("change", (v) => resolve(v))
x.set(100)
})

Expand All @@ -64,7 +64,7 @@ describe("useSpring", () => {
} as any)

React.useEffect(() => {
return y.onChange((v) => {
return y.on("change", (v) => {
if (output.length >= 10) {
resolve(output)
} else {
Expand Down Expand Up @@ -105,6 +105,7 @@ describe("useSpring", () => {
rerender(<Component target={a} />)
rerender(<Component target={a} />)

expect(((a! as any).updateSubscribers! as any).getSize()).toBe(1)
// Cast to any here as `.events` is private API
expect((a as any).events.change.getSize()).toBe(1)
})
})
Expand Up @@ -46,13 +46,13 @@ describe("useVelocity", () => {

React.useEffect(() => {
const unsubscribe = pipe(
x.onChange((v) => {
x.on("change", (v) => {
output.push(Math.round(v))
}),
xVelocity.onChange((v) => {
xVelocity.on("change", (v) => {
outputVelocity.push(Math.round(v))
}),
xAcceleration.onChange((v) => {
xAcceleration.on("change", (v) => {
outputAcceleration.push(Math.round(v))
})
)
Expand Down
112 changes: 60 additions & 52 deletions packages/framer-motion/src/value/index.ts
Expand Up @@ -19,6 +19,15 @@ export type PassiveEffect<T> = (v: T, safeSetter: (v: T) => void) => void

export type StartAnimation = (complete: () => void) => () => void

export interface MotionValueEventCallbacks<V> {
animationStart: () => void
animationComplete: () => void
animationCancel: () => void
change: (latest: V) => void
renderRequest: () => void
velocityChange: (latest: number) => void
}

const isFloat = (value: any): value is string => {
return !isNaN(parseFloat(value))
}
Expand Down Expand Up @@ -73,29 +82,6 @@ export class MotionValue<V = any> {
*/
private lastUpdated: number = 0

/**
* Functions to notify when the `MotionValue` updates.
*
* @internal
*/
private updateSubscribers = new SubscriptionManager<Subscriber<V>>()

/**
* Functions to notify when the velocity updates.
*
* @internal
*/
public velocityUpdateSubscribers = new SubscriptionManager<
Subscriber<number>
>()

/**
* Functions to notify when the `MotionValue` updates and `render` is set to `true`.
*
* @internal
*/
private renderSubscribers = new SubscriptionManager<Subscriber<V>>()

/**
* Add a passive effect to this `MotionValue`.
*
Expand Down Expand Up @@ -159,8 +145,8 @@ export class MotionValue<V = any> {
* opacity.set(newOpacity)
* }
*
* const unsubscribeX = x.onChange(updateOpacity)
* const unsubscribeY = y.onChange(updateOpacity)
* const unsubscribeX = x.on("change", updateOpacity)
* const unsubscribeY = y.on("change", updateOpacity)
*
* return () => {
* unsubscribeX()
Expand All @@ -183,28 +169,34 @@ export class MotionValue<V = any> {
* @param subscriber - A function that receives the latest value.
* @returns A function that, when called, will cancel this subscription.
*
* @public
* @deprecated
*/
onChange(subscription: Subscriber<V>): () => void {
return this.updateSubscribers.add(subscription)
}

clearListeners() {
this.updateSubscribers.clear()
return this.on("change", subscription)
}

/**
* Adds a function that will be notified when the `MotionValue` requests a render.
*
* @param subscriber - A function that's provided the latest value.
* @returns A function that, when called, will cancel this subscription.
*
* @internal
* An object containing a SubscriptionManager for each active event.
*/
onRenderRequest(subscription: Subscriber<V>) {
// Render immediately
subscription(this.get())
return this.renderSubscribers.add(subscription)
private events: {
[key: string]: SubscriptionManager<any>
} = {}

on<EventName extends keyof MotionValueEventCallbacks<V>>(
eventName: EventName,
callback: MotionValueEventCallbacks<V>[EventName]
) {
if (!this.events[eventName]) {
this.events[eventName] = new SubscriptionManager()
}

return this.events[eventName].add(callback)
}

clearListeners() {
for (const eventManagers in this.events) {
this.events[eventManagers].clear()
}
}

/**
Expand Down Expand Up @@ -258,18 +250,18 @@ export class MotionValue<V = any> {
}

// Update update subscribers
if (this.prev !== this.current) {
this.updateSubscribers.notify(this.current)
if (this.prev !== this.current && this.events.change) {
this.events.change.notify(this.current)
}

// Update velocity subscribers
if (this.velocityUpdateSubscribers.getSize()) {
this.velocityUpdateSubscribers.notify(this.getVelocity())
if (this.events.velocityChange) {
this.events.velocityChange.notify(this.getVelocity())
}

// Update render subscribers
if (render) {
this.renderSubscribers.notify(this.current)
if (render && this.events.renderRequest) {
this.events.renderRequest.notify(this.current)
}
}

Expand Down Expand Up @@ -332,7 +324,10 @@ export class MotionValue<V = any> {
private velocityCheck = ({ timestamp }: FrameData) => {
if (timestamp !== this.lastUpdated) {
this.prev = this.current
this.velocityUpdateSubscribers.notify(this.getVelocity())

if (this.events.velocityChange) {
this.events.velocityChange.notify(this.getVelocity())
}
}
}

Expand All @@ -356,7 +351,16 @@ export class MotionValue<V = any> {
return new Promise<void>((resolve) => {
this.hasAnimated = true
this.stopAnimation = animation(resolve)
}).then(() => this.clearAnimation())

if (this.events.animationStart) {
this.events.animationStart.notify()
}
}).then(() => {
if (this.events.animationComplete) {
this.events.animationComplete.notify()
}
this.clearAnimation()
})
}

/**
Expand All @@ -365,7 +369,12 @@ export class MotionValue<V = any> {
* @public
*/
stop() {
if (this.stopAnimation) this.stopAnimation()
if (this.stopAnimation) {
this.stopAnimation()
if (this.events.animationCancel) {
this.events.animationCancel.notify()
}
}
this.clearAnimation()
}

Expand All @@ -392,8 +401,7 @@ export class MotionValue<V = any> {
* @public
*/
destroy() {
this.updateSubscribers.clear()
this.renderSubscribers.clear()
this.clearListeners()
this.stop()
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/framer-motion/src/value/use-motion-value.ts
Expand Up @@ -31,7 +31,7 @@ export function useMotionValue<T>(initial: T): MotionValue<T> {
const { isStatic } = useContext(MotionConfigContext)
if (isStatic) {
const [, setLatest] = useState(initial)
useEffect(() => value.onChange(setLatest), [])
useEffect(() => value.on("change", setLatest), [])
}

return value
Expand Down
4 changes: 2 additions & 2 deletions packages/framer-motion/src/value/use-on-change.ts
Expand Up @@ -9,7 +9,7 @@ export function useOnChange<T>(
useIsomorphicLayoutEffect(() => {
if (isMotionValue(value)) {
callback(value.get())
return value.onChange(callback)
return value.on("change", callback)
}
}, [value, callback])
}
Expand All @@ -20,7 +20,7 @@ export function useMultiOnChange(
cleanup: () => void
) {
useIsomorphicLayoutEffect(() => {
const subscriptions = values.map((value) => value.onChange(handler))
const subscriptions = values.map((value) => value.on("change", handler))

return () => {
subscriptions.forEach((unsubscribe) => unsubscribe())
Expand Down
2 changes: 1 addition & 1 deletion packages/framer-motion/src/value/use-velocity.ts
Expand Up @@ -16,7 +16,7 @@ export function useVelocity(value: MotionValue<number>): MotionValue<number> {
const velocity = useMotionValue(value.getVelocity())

useEffect(() => {
return value.velocityUpdateSubscribers.add((newVelocity) => {
return value.on("velocityChange", (newVelocity) => {
velocity.set(newVelocity)
})
}, [value])
Expand Down

1 comment on commit 44744e0

@samselikoff
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattgperry Just found this commit because I noticed the deprecated message in VSCode on onChange. Traced it to this commit, looks like .on("change") is the new preferred way for events. Love this change btw! Is that correct? If so maybe we could add a link to the deprecated message so folks can find this? I could help with that btw.

Please sign in to comment.