Skip to content

Commit

Permalink
add mountOnEnter
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Feb 15, 2017
1 parent cd5d1ce commit 22ee7f6
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 84 deletions.
67 changes: 32 additions & 35 deletions src/Transition.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ class Transition extends React.Component {
constructor(props, context) {
super(props, context);

const { transitionAppear, unmountOnExit, mountOnEnter } = props;

let initialStatus;
if (props.in) {
// Start enter transition in componentDidMount.
initialStatus = props.transitionAppear ? EXITED : ENTERED;
initialStatus = transitionAppear ? EXITED : ENTERED;
} else {
initialStatus = props.unmountOnExit ? UNMOUNTED : EXITED;
initialStatus = unmountOnExit || mountOnEnter ? UNMOUNTED : EXITED;
}
this.state = {status: initialStatus};

Expand All @@ -44,51 +46,41 @@ class Transition extends React.Component {
}

componentWillReceiveProps(nextProps) {
if (nextProps.in && this.props.unmountOnExit) {
if (this.state.status === UNMOUNTED) {
// Start enter transition in componentDidUpdate.
this.setState({status: EXITED});
}
}
else {
this._needsUpdate = true
this._propsUpdated = true

if (nextProps.in && this.state.status === UNMOUNTED) {
// Start enter transition in componentDidUpdate.
this.setState({ status: EXITED });
}
}

componentDidUpdate() {
const status = this.state.status;

if (this.props.unmountOnExit && status === EXITED) {
// EXITED is always a transitional state to either ENTERING or UNMOUNTED
// when using unmountOnExit.
if (this.props.in) {
this.performEnter(this.props);
} else {
this.setState({status: UNMOUNTED});
}
// EXITED is always a transitional state to either ENTERING or UNMOUNTED
// when using unmountOnExit.
const needsTransitionalUpdate = (
this.props.unmountOnExit &&
status === EXITED
)

if (!this._propsUpdated && !needsTransitionalUpdate) {
return
}

// guard ensures we are only responding to prop changes
if (this._needsUpdate) {
this._needsUpdate = false;

if (this.props.in) {
if (status === EXITING) {
this.performEnter(this.props);
}
else if (status === EXITED) {
this.performEnter(this.props);
}
// Otherwise we're already entering or entered.
} else {
if (status === ENTERING || status === ENTERED) {
this.performExit(this.props);
}
// Otherwise we're already exited or exiting.
this._propsUpdated = false;

if (this.props.in) {
if (status === EXITING || status === EXITED) {
this.performEnter(this.props);
}
}
else if (needsTransitionalUpdate) {
this.setState({ status: UNMOUNTED });
}
else if (status === ENTERING || status === ENTERED) {
this.performExit(this.props);
}
}

componentWillUnmount() {
Expand Down Expand Up @@ -216,6 +208,11 @@ Transition.propTypes = {
*/
in: React.PropTypes.bool,

/**
* Wait until the first "enter" transition to mount the component (add it to the DOM)
*/
mountOnEnter: React.PropTypes.bool,

/**
* Unmount the component (remove it from the DOM) when it is not shown
*/
Expand Down
180 changes: 131 additions & 49 deletions test/TransitionSpec.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-addons-test-utils';
import { render } from './helpers';
import tsp from 'teaspoon';
import Transition, {UNMOUNTED, EXITED, ENTERING, ENTERED, EXITING} from
'../src/Transition';

describe('Transition', function () {
it('should not transition on mount', function(){
let instance = render(
<Transition in timeout={0} onEnter={()=> { throw new Error('should not Enter'); }}>
<div></div>
</Transition>
);

expect(instance.state.status).to.equal(ENTERED);
import { render } from './helpers';
import Transition, { UNMOUNTED, EXITED, ENTERING, ENTERED, EXITING }
from '../src/Transition';

describe('Transition', () => {
it('should not transition on mount', () => {
let instance = tsp(
<Transition
in
timeout={0}
onEnter={()=> { throw new Error('should not Enter'); }}
>
<div></div>
</Transition>
)
.render()

expect(instance.state('status')).to.equal(ENTERED);
});

it('should transition on mount with transitionAppear', done =>{
let instance = ReactTestUtils.renderIntoDocument(
<Transition in
transitionAppear
timeout={0}
onEnter={()=> done()}
>
<div></div>
</Transition>
);
it('should transition on mount with transitionAppear', done => {
let instance = tsp(
<Transition in
transitionAppear
timeout={0}
onEnter={()=> done()}
>
<div></div>
</Transition>
)
.render();

expect(instance.state.status).to.equal(EXITED);
expect(instance.state('status')).to.equal(EXITED);
});

it('should flush new props to the DOM before initiating a transition', function(done) {
Expand Down Expand Up @@ -56,28 +62,29 @@ describe('Transition', function () {
})
});

describe('entering', ()=> {
describe('entering', () => {
let instance;

beforeEach(function(){
instance = render(
beforeEach(() => {
instance = tsp(
<Transition
timeout={10}
enteredClassName='test-enter'
enteringClassName='test-entering'
>
<div/>
</Transition>
);
)
.render();
});

it('should fire callbacks', done => {
let onEnter = sinon.spy();
let onEntering = sinon.spy();

expect(instance.state.status).to.equal(EXITED);
expect(instance.state('status')).to.equal(EXITED);

instance = instance.renderWithProps({
instance.props({
in: true,

onEnter,
Expand All @@ -96,23 +103,23 @@ describe('Transition', function () {
it('should move to each transition state', done => {
let count = 0;

expect(instance.state.status).to.equal(EXITED);
expect(instance.state('status')).to.equal(EXITED);

instance = instance.renderWithProps({
instance.props({
in: true,

onEnter(){
count++;
expect(instance.state.status).to.equal(EXITED);
expect(instance.state('status')).to.equal(EXITED);
},

onEntering(){
count++;
expect(instance.state.status).to.equal(ENTERING);
expect(instance.state('status')).to.equal(ENTERING);
},

onEntered(){
expect(instance.state.status).to.equal(ENTERED);
expect(instance.state('status')).to.equal(ENTERED);
expect(count).to.equal(2);
done();
}
Expand All @@ -122,9 +129,9 @@ describe('Transition', function () {
it('should apply classes at each transition state', done => {
let count = 0;

expect(instance.state.status).to.equal(EXITED);
expect(instance.state('status')).to.equal(EXITED);

instance = instance.renderWithProps({
instance.props({
in: true,

onEnter(node){
Expand All @@ -149,8 +156,8 @@ describe('Transition', function () {
describe('exiting', ()=> {
let instance;

beforeEach(function(){
instance = render(
beforeEach(() => {
instance = tsp(
<Transition
in
timeout={10}
Expand All @@ -159,16 +166,17 @@ describe('Transition', function () {
>
<div/>
</Transition>
);
)
.render();
});

it('should fire callbacks', done => {
let onExit = sinon.spy();
let onExiting = sinon.spy();

expect(instance.state.status).to.equal(ENTERED);
expect(instance.state('status')).to.equal(ENTERED);

instance = instance.renderWithProps({
instance.props({
in: false,

onExit,
Expand All @@ -187,23 +195,23 @@ describe('Transition', function () {
it('should move to each transition state', done => {
let count = 0;

expect(instance.state.status).to.equal(ENTERED);
expect(instance.state('status')).to.equal(ENTERED);

instance = instance.renderWithProps({
instance.props({
in: false,

onExit(){
count++;
expect(instance.state.status).to.equal(ENTERED);
expect(instance.state('status')).to.equal(ENTERED);
},

onExiting(){
count++;
expect(instance.state.status).to.equal(EXITING);
expect(instance.state('status')).to.equal(EXITING);
},

onExited(){
expect(instance.state.status).to.equal(EXITED);
expect(instance.state('status')).to.equal(EXITED);
expect(count).to.equal(2);
done();
}
Expand All @@ -213,9 +221,9 @@ describe('Transition', function () {
it('should apply classes at each transition state', done => {
let count = 0;

expect(instance.state.status).to.equal(ENTERED);
expect(instance.state('status')).to.equal(ENTERED);

instance = instance.renderWithProps({
instance.props({
in: false,

onExit(node){
Expand All @@ -237,6 +245,80 @@ describe('Transition', function () {
});
});

describe('mountOnEnter', () => {
class MountTransition extends React.Component {
constructor(props) {
super(props);
this.state = {in: props.initialIn};
}

render() {
const { ...props } = this.props;
delete props.initialIn;

return (
<Transition
ref="transition"
mountOnEnter
in={this.state.in}
timeout={10}
{...props}
>
<div />
</Transition>
);
}

getStatus() {
return this.refs.transition.state.status;
}
}

it('should mount when entering', done => {
const instance = tsp(
<MountTransition
initialIn={false}
onEnter={() => {
expect(instance.unwrap().getStatus()).to.equal(EXITED);
expect(instance.dom()).to.exist;
done();
}}
/>
)
.render();

expect(instance.unwrap().getStatus()).to.equal(UNMOUNTED);

expect(instance.dom()).to.not.exist;

instance.props({ in: true });
});

it('should stay mounted after exiting', done => {
const instance = tsp(
<MountTransition
initialIn={false}
onEntered={() => {
expect(instance.unwrap().getStatus()).to.equal(ENTERED);
expect(instance.dom()).to.exist;

instance.state({ in: false });
}}
onExited={() => {
expect(instance.unwrap().getStatus()).to.equal(EXITED);
expect(instance.dom()).to.exist;

done();
}}
/>
)
.render();

expect(instance.dom()).to.not.exist;
instance.state({ in: true });
});
})

describe('unmountOnExit', () => {
class UnmountTransition extends React.Component {
constructor(props) {
Expand Down

0 comments on commit 22ee7f6

Please sign in to comment.