Skip to content
This repository has been archived by the owner on Apr 14, 2020. It is now read-only.

Commit

Permalink
Merge pull request #116 from madou/animator-triggering-self
Browse files Browse the repository at this point in the history
feat: adds triggerSelfKey to animator component
  • Loading branch information
Madou authored May 23, 2019
2 parents f6f55b7 + 6ad648e commit 3dc3413
Show file tree
Hide file tree
Showing 25 changed files with 411 additions and 73 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# yubaba 🧙✨

/juːba:ba/ out of the box animated experiences for [React.js](https://reactjs.org/) 🧙✨
/juːbaːba/ out of the box animated experiences for [React.js](https://reactjs.org/) 🧙✨

[![npm](https://img.shields.io/npm/v/yubaba.svg)](https://www.npmjs.com/package/yubaba) [![npm bundle size (minified + gzip)](https://badgen.net/bundlephobia/minzip/yubaba)](https://bundlephobia.com/result?p=yubaba)

Expand Down Expand Up @@ -44,7 +44,7 @@ yarn add yubaba react@^16.4.x react-dom@^16.4.x emotion@^10.x.x
import Animator, { Move } from 'yubaba';

({ isLarge }) => (
<Animator name="my-first-baba" key={isLarge}>
<Animator name="my-first-baba" triggerSelfKey={isLarge}>
<Move>{anim => <div {...anim} className={isLarge ? 'large' : 'small'} />}</Move>
</Animator>
);
Expand Down
8 changes: 7 additions & 1 deletion doczrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const primaryText = 'rgba(255, 255, 255, 0.95)';
const background = `linear-gradient(135deg, ${altPrimary} 25%, ${primary} 100%)`;

module.exports = {
title: 'yubaba 🧙✨',
title: `yubaba ${pkg.description}`,
description: `yubaba ${pkg.description}`,
typescript: true,
dest: '/docs',
Expand Down Expand Up @@ -44,6 +44,7 @@ module.exports = {
display: none;
}
a[class^='MenuLink__LinkAnchor-'],
a[class^='MenuLink__createLink-'] {
font-weight: 400;
Expand All @@ -55,6 +56,11 @@ module.exports = {
a[class^='SmallLink__Link'] {
opacity: 0.65;
:hover,
:focus {
opacity: 0.9;
}
}
`,
h1: css`
Expand Down
2 changes: 1 addition & 1 deletion packages/yubaba/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"main": "dist/cjs/packages/yubaba/src/index.js",
"module": "dist/esm/packages/yubaba/src/index.js",
"sideEffects": false,
"description": "/juːba:ba/ out of the box animated experiences for React.js 🧙✨",
"description": "/juːbaːba/ out of the box animated experiences for React.js 🧙✨",
"keywords": [
"react",
"flip",
Expand Down
77 changes: 68 additions & 9 deletions packages/yubaba/src/Animator/__docs__/docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,83 @@ import Animator from '../index';

# Animator

This is the brains component.
When unmounting or flipping the `in` prop from `true` to `false`,
it will execute all the animations **top to bottom** if a matching Animator pair (either itself or another Animator element) is found within `50ms`.
You will use this component with most animations.
It does a few things:

> **Tip -** See [Getting started](/getting-started) for more information on how to use this component.
- take snapshots of the DOM
- pass data to the animations
- execute and orchestrate the animations

## Usage
You'll really only need to be concerned with _execute and orchestrate the animations_.
There are three ways you can execute animations,
listed below.

> **Tip -** Missing some context? Have a look at [Getting started](/getting-started) first.
## Animate unmounted to mounted

You'll find this is the goto way for triggering animations.
It will animate between an element that unmounts and an element that mounts over a state change.
[Moving to another element](/getting-started#moving-to-another-element) is a good example of this.

```js
import Animator, { Move } from 'yubaba';

({ isShown }) => [
!isShown && (
<Animator name="default">
<Move>{children}</Move>
</Animator>
),
isShown && (
<Animator name="default">
<Move>{children}</Move>
</Animator>
),
];
```

## Animate self

Using the `triggerSelfKey` prop to force an animation on itself over a state change.
[Moving to the same element](/getting-started#moving-to-the-same-element) is a good example of this.

```js
import Animator from 'yubaba';
import Animator, { Move } from 'yubaba';

const ListItem = ({ index }) => (
<Animator name={`item-${index}`}>
{({ ref, style, className }) => <div ref={ref} style={style} className={className} />}
({ children, itemId }) => (
<Animator triggerSelfKey={itemId} name="self-target">
<Move>{children}</Move>
</Animator>
);
```

> **Tip -** You can't use this with the `in` prop,
> if you try you'll get a dev error.
## Animate persisted to mounted

Animate between a react element that never unmounts and one that mounts/unmounts over a state change.
Using the `in` prop to mark a persisted component if it is considered in or not.
[Moving from a persisted element](/getting-started#moving-from-a-persisted-element) is a good example of this.

```js
import Animator, { Move } from 'yubaba';

({ isShown }) => [
<Animator name="default" in={!isShown}>
<Move>{children}</Move>
</Animator>,
isShown && (
<Animator name="default">
<Move>{children}</Move>
</Animator>
),
];
```

> **Tip -** You can also use the same method to animate over unmounted to persisted.
## Props

<Props of={Animator} />
18 changes: 18 additions & 0 deletions packages/yubaba/src/Animator/__snapshots__/test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Animator /> persisted animations should throw when changing into "in" after initial mount 1`] = `
"yubaba v0.0.0
You're switching between persisted and unpersisted, don't do this. Either always set the \\"in\\" prop as true or false, or keep as undefined."
`;

exports[`<Animator /> self targetted animations should throw when changing into "triggerSelfKey" after initial mount 1`] = `
"yubaba v0.0.0
You're switching between self triggering modes, don't do this. Either always set the \\"triggerSelfKey\\" prop, or keep as undefined."
`;

exports[`<Animator /> self targetted animations should throw when using both "in" and "triggerSelfKey" props after initial mount 1`] = `
"yubaba v0.0.0
Don't use \\"in\\" and \\"triggerSelfKey\\" together. If your element is persisted use \\"in\\". If your element is targeting itself for animations use \\"triggerSelfKey\\"."
`;

exports[`<Animator /> should pass dom data to child animation 1`] = `
Array [
Object {
Expand Down
110 changes: 84 additions & 26 deletions packages/yubaba/src/Animator/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface ChildProps {
className?: string;
}

export interface State {
export interface AnimatorState {
childProps: ChildProps;
animationsMarkup: React.ReactPortal[];
}
Expand All @@ -48,11 +48,18 @@ export interface AnimatorProps extends CollectorChildrenProps, InjectedProps {
name: string;

/**
* Used alternatively to the implicit animation triggering via unmounting or mounting of Animator components.
* Only use `in` if your component is expected to persist through the entire lifecyle of the app.
* When you transition to the "next page" make sure to set your "in" to false. When you transition
* back to the original page set the "in" prop back to true. This lets the Animator components know when to
* execute the animations.
* Will trigger animations over itself when this prop changes.
*
* You can't use the with the "in" prop.
*/
triggerSelfKey?: string;

/**
* Use if your element is expected to persist through an animation.
* When you transition to the next state set your "in" to false and vice versa.
* This lets the Animator components know when to execute the animations.
*
* You can't use this with the "triggerSelfKey".
*/
in?: boolean;

Expand All @@ -75,7 +82,7 @@ export interface AnimatorProps extends CollectorChildrenProps, InjectedProps {
container: HTMLElement | (() => HTMLElement);
}

export default class Animator extends React.PureComponent<AnimatorProps, State> {
export default class Animator extends React.PureComponent<AnimatorProps, AnimatorState> {
static displayName = 'Animator';

static defaultProps = {
Expand All @@ -84,7 +91,7 @@ export default class Animator extends React.PureComponent<AnimatorProps, State>
container: document.body,
};

state: State = {
state: AnimatorState = {
animationsMarkup: [],
childProps: {},
};
Expand Down Expand Up @@ -115,55 +122,95 @@ export default class Animator extends React.PureComponent<AnimatorProps, State>
if (componentIn === undefined || componentIn) {
// Ok nothing is there yet, show ourself and store DOM data for later.
// We'll be waiting for another Animator to mount.
this.showSelfAndNotifyManager();
this.notifyVisibilityManagerAnimationsAreFinished();
}
}

componentWillUpdate(prevProps: AnimatorProps) {
const { in: isIn } = this.props;
if (prevProps.in === false && isIn === true) {
// We're being removed from "in". Let's recalculate our DOM position.
getSnapshotBeforeUpdate(prevProps: AnimatorProps) {
if (prevProps.in === true && this.props.in === false) {
this.storeDOMData();
this.delayedClearStore();
this.abortAnimations();
}

if (prevProps.triggerSelfKey !== this.props.triggerSelfKey) {
this.storeDOMData();
this.delayedClearStore();
}

// we can return snapshot here to circumvent the entire storing of dom data.
// would remove the need for setting a name!
return null;
}

componentDidUpdate(prevProps: AnimatorProps) {
const { in: isIn, name } = this.props;
componentDidUpdate(prevProps: AnimatorProps, _: AnimatorState) {
const inPropSame = this.props.in === prevProps.in;
const triggerSelfKeyPropSame = this.props.triggerSelfKey === prevProps.triggerSelfKey;

if (isIn === prevProps.in) {
if (inPropSame && triggerSelfKeyPropSame) {
// Nothing has changed, return early.
return;
}

if (
process.env.NODE_ENV === 'development' &&
(isIn === undefined || prevProps.in === undefined)
) {
warn(
`You're switching between controlled and uncontrolled, don't do this. Either always set the "in" prop as true or false, or keep as undefined.`
if (process.env.NODE_ENV === 'development') {
precondition(
!(this.props.in !== undefined && this.props.triggerSelfKey !== undefined),
`Don't use "in" and "triggerSelfKey" together. If your element is persisted use "in". If your element is targeting itself for animations use "triggerSelfKey".`
);
}

if (process.env.NODE_ENV === 'development') {
precondition(
!((this.props.in === undefined || prevProps.in === undefined) && !inPropSame),
`You're switching between persisted and unpersisted, don't do this. Either always set the "in" prop as true or false, or keep as undefined.`
);
}

if (process.env.NODE_ENV === 'development') {
precondition(
!(
(this.props.triggerSelfKey === undefined || prevProps.triggerSelfKey === undefined) &&
!triggerSelfKeyPropSame
),
`You're switching between self triggering modes, don't do this. Either always set the "triggerSelfKey" prop, or keep as undefined.`
);
}

if (isIn) {
if (store.has(name)) {
if (this.props.in) {
if (store.has(this.props.name)) {
this.executeAnimations();
// return early dont tell manager yet dawg
return;
}
// No animation to trigger, tell manager we're all good regardless.
this.notifyVisibilityManagerAnimationsAreFinished();
return;
}

this.showSelfAndNotifyManager();
if (!triggerSelfKeyPropSame) {
// Defer execution to the next frame to capture correctly.
// Make sure to keep react state the same for any inflight animations to be captured correctly.
requestAnimationFrame(() => {
this.abortAnimations();
this.executeAnimations();
});
}
}

componentWillUnmount() {
if (this.props.triggerSelfKey) {
this.abortAnimations();
this.unmounting = true;
return;
}

this.storeDOMData();
this.delayedClearStore();
this.abortAnimations();
this.unmounting = true;
}

showSelfAndNotifyManager() {
notifyVisibilityManagerAnimationsAreFinished() {
const { context, name } = this.props;

// If a VisibilityManager is a parent up the tree context will be available.
Expand Down Expand Up @@ -233,6 +280,7 @@ If it's an image, try and have the image loaded before mounting, or set a static
const { name, container: getContainer, context } = this.props;
const container = typeof getContainer === 'function' ? getContainer() : getContainer;
const fromTarget = store.get(name);
let aborted = false;

if (fromTarget) {
const { collectorData, elementData } = fromTarget;
Expand Down Expand Up @@ -332,6 +380,10 @@ If it's an image, try and have the image loaded before mounting, or set a static
container.removeChild(elementToMountChildren);
}

if (targetData.payload.abort) {
targetData.payload.abort();
}

if (this.unmounting) {
return;
}
Expand Down Expand Up @@ -370,6 +422,8 @@ If it's an image, try and have the image loaded before mounting, or set a static
);

this.abortAnimations = () => {
aborted = true;

if (this.animating) {
this.animating = false;
blocks.forEach(block => block.forEach(anim => anim.cleanup()));
Expand Down Expand Up @@ -423,6 +477,10 @@ If it's an image, try and have the image loaded before mounting, or set a static
);
})
.then(() => {
if (aborted) {
return;
}

blocks.forEach(block => block.forEach(anim => anim.cleanup()));
})
.then(() => {
Expand Down
Loading

0 comments on commit 3dc3413

Please sign in to comment.