Story begins as usual - project is just starting, the design is "almost" done and the requirements are all over the place. Not wanting to deal with with major refactorings later down the road, team decides to follow atomic design pattern as much as possible.
Life is good. All changes are isolated in small chunks, but suddenly, a wild animation for an already developed component appears! styled-components to the rescue!
Animation component
As an example, let's create a simple animation for rotating an item. It is just a simple wrapper, which uses chainable .attrs
to pass dynamic props and set animation
properties. Note: it should only use css
and values, that can be used in transitions. So no px
to %
transitions.
For passing props, you could also use tagged template literal, but it would create a new classname for each different variant of the transition.
import styled from "styled-components";
const Rotate = styled("div").attrs(
({ state, duration = "300ms", start = 0, end = 180 }) => ({
style: {
transition: duration,
transform: `rotate(${state ? start : end}deg)`
}
})
)``;
export default Rotate;
Usage
To use it, just import the animation, wrap the component you want to animate and provide some sort of a state handler. In this case it is just a simple component to change the state, when clicking a button. In practice, it could be almost anything from a button click, to a form validation status.
<StateSwitcher>
{({ state }) => (
<Rotate state={state} duration="1s" end={360}>
<Element>Rotate</Element>
</Rotate>
)}
</StateSwitcher>
Combining multiple animations
Rinse and repeat. The setup is almost identical.
import styled from "styled-components";
const Opacity = styled("div").attrs(
({ state, duration = "300ms", start = 0, end = 1 }) => ({
style: {
transition: duration,
opacity: state ? end : start
}
})
)``;
export default Opacity;
Now use it to wrap and voila.
<StateSwitcher>
{({ state }) => (
<Opacity state={state}>
<Rotate state={state}>
<Element>Rotate + Opacity</Element>
</Rotate>
</Opacity>
)}
</StateSwitcher>
Testing
Testing this setup is dead simple with @testing-library/react. Just change the state and check what the resulting style changes.
import React from "react";
import { render } from "@testing-library/react";
import Rotate from "./Rotate";
describe("Rotate", () => {
it("renders Rotate and changes state ", async () => {
const component = state => (
<Rotate state={state} start={0} end={123} data-testid="rotate-transition">
<div>COMPONENT</div>
</Rotate>
);
const { rerender, getByTestId } = render(component(true));
const RenderedComponent = getByTestId("rotate-transition");
let style = window.getComputedStyle(RenderedComponent);
expect(style.transform).toBe("rotate(0deg)");
rerender(component(false));
style = window.getComputedStyle(RenderedComponent);
expect(style.transform).toBe("rotate(123deg)");
});
});
Results
You could have many different variants (move, rotate, color ...) and extend these much more - handle animation finish callbacks, setTimeouts and etc.
This setup might not be the suitable in all cases, but in my case, it ticks all the right marks:
- Easy to use and share;
- Easy to extend;
- Easy to test;