Skip to content

Instantly share code, notes, and snippets.

@wsmd
Last active October 29, 2017 20:00
Show Gist options
  • Select an option

  • Save wsmd/d022ebdd5d167745ab8bde3f696a4ee0 to your computer and use it in GitHub Desktop.

Select an option

Save wsmd/d022ebdd5d167745ab8bde3f696a4ee0 to your computer and use it in GitHub Desktop.
Counter Button in React
import React, { Component } from 'react';
import classNames from 'classnames';
import './CounterButton.css';
const WIDTH_DIFF_THRESHOLD = 2.5;
const PARSABLE_PROPERTIES = ['margin-left', 'animation-duration'];
const getComputedProperty = (node, property) => {
const value = window.getComputedStyle(node)[property];
if (PARSABLE_PROPERTIES.includes(property)) {
return parseFloat(value);
}
return value;
}
class CounterButton extends Component {
deferredUpdates = [];
state = {
count: this.props.count,
next: this.props.count + 1,
prev: this.props.count - 1,
animation: null,
width: null,
}
componentDidMount() {
this.ANIMATION_DURATION =
getComputedProperty(this.currentNode, 'animation-duration') * 1000;
this.setState({
width: (() => {
// @todo I should probbarly avoid a second call here
const margin = getComputedProperty(this.counterNode, 'margin-left');
const initial = this.counter.getBoundingClientRect().width;
const current = this.currentNode.getBoundingClientRect().width;
return initial + margin + current;
})(),
})
}
componentWillReceiveProps(nextProps) {
if (this.state.animation === null) {
this.animateNumber(nextProps.count);
} else {
this.deferredUpdates.push(nextProps.count);
}
}
animateNumber(nextCount) {
if (nextCount > this.state.count) {
this.setState({ animation: 'inc', next: nextCount }, () => {
this.setState({ width: this.getWidth(this.nextNode) });
this.updateCount(nextCount)
});
} else {
this.setState({ animation: 'dec', prev: nextCount }, () => {
this.setState({ width: this.getWidth(this.prevNode) });
this.updateCount(nextCount)
});
}
}
updateCount(number) {
setTimeout(() => {
this.setState({ count: number, animation: null }, this.handleDeferredItems);
}, this.ANIMATION_DURATION);
}
handleDeferredItems() {
const deferredLength = this.deferredUpdates.length;
if (deferredLength > 0) {
const lastDeferredNumber = this.deferredUpdates[deferredLength - 1];
if (lastDeferredNumber !== this.state.count) {
this.animateNumber(lastDeferredNumber);
}
this.deferredUpdates = [];
}
}
getWidth(nextNode) {
const totalWidth = this.counter.getBoundingClientRect().width;
const currentWidth = this.currentNode.getBoundingClientRect().width;
const nextWidth = nextNode.getBoundingClientRect().width;
if (Math.abs(currentWidth - nextWidth) < WIDTH_DIFF_THRESHOLD) {
return totalWidth;
};
const newWidth = totalWidth - currentWidth + nextWidth;
return newWidth;
}
get buttonClassName() {
return classNames('counter', {
incrementing: this.state.animation === 'inc',
decrementing: this.state.animation === 'dec',
})
}
render() {
return (
<button
className={this.buttonClassName}
ref={n => { this.counter = n; }}
style={{ width: this.state.width }}
>
{this.props.children}
<div
className="counter-counts"
ref={n => { this.counterNode = n; }}
>
<span
className="counter-count counter-count--next"
ref={n => { this.nextNode = n; }}
children={this.state.next}
/>
<span
className="counter-count counter-count--active"
ref={n => { this.currentNode = n; }}
children={this.state.count}
/>
<span
className="counter-count counter-count--prev"
ref={n => { this.prevNode = n; }}
children={this.state.prev}
/>
</div>
</button>
);
}
}
export default CounterButton;
import React, { Component } from 'react';
import CounterButton from './CounterButton';
import TodoList from './TodoList'; // simple to do list with onCheck and onUncheck props
// See live demo:
// https://react-counter-button.herokuapp.com/
class App extends Component {
state = {
counter: 0,
}
// some logic to handle state.counter
render() {
return (
<div className="App">
<CounterButton count={this.state.counter}>Archive</CounterButton>
</div>
);
}
}
export default App;
.counter {
color: white;
background: #0076FF;
box-shadow: 0 4px 8px -3px rgba(0, 118, 255, 0.5), 0 1px 1px rgba(0, 118, 255, 0.25);
padding: 8px 12px;
line-height: 16px;
display: inline-block;
border-radius: 4px;
transition: all 0.15s ease;
font-size: 14px;
box-sizing: border-box;
cursor: pointer;
text-align: left;
border: 0;
}
.counter-counts {
margin-left: 8px;
position: absolute;
display: inline-block;
text-align: center;
opacity: 0.75;
}
.counter-count {
transition: all .2s ease;
display: inline-block;
}
.counter-count--active {
transform: translateY(0px);
}
.counter-count--prev,
.counter-count--next {
position: absolute;
left: 0;
opacity: 0;
}
.counter-count--prev {
transform: translateY(25px);
}
.counter-count--next {
transform: translateY(-25px);
}
@keyframes incrementingNext {
to {
transform: translateY(0px);
opacity: 1;
}
}
@keyframes incrementingActive {
60% {
opacity: 0;
}
to {
transform: translateY(25px);
opacity: 0;
}
}
@keyframes decrementingPrev {
to {
transform: translateY(0px);
opacity: 1;
}
}
@keyframes decrementingActive {
60% {
opacity: 0;
}
to {
transform: translateY(-25px);
opacity: 0;
}
}
.counter-count {
animation-duration: 300ms;
animation-timing-function: ease;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
}
.counter.incrementing .counter-count--next {
animation-name: incrementingNext;
}
.counter.incrementing .counter-count--active {
animation-name: incrementingActive;
}
.counter.decrementing .counter-count--prev {
animation-name: decrementingPrev;
}
.counter.decrementing .counter-count--active {
animation-name: decrementingActive;
}
@wsmd
Copy link
Author

wsmd commented Oct 29, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment