Skip to content
This repository was archived by the owner on Apr 9, 2019. It is now read-only.

Commit 01ee1da

Browse files
committed
Merge pull request #2 from programmarchy/pop-to-root
Pop to root view
2 parents 6f6a5e6 + bdc8008 commit 01ee1da

File tree

4 files changed

+221
-0
lines changed

4 files changed

+221
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,20 @@ Addtional options - see [pushView()](#push-options)
156156

157157
***
158158

159+
### `popToRootView([options])`
160+
161+
Pop the all the views off the stack except the first (root) view
162+
163+
**Arguments**
164+
165+
##### `options` `{object}`
166+
167+
Addtional options - see [pushView()](#push-options)
168+
169+
##### `options.transiton` `{number|function}` `default=Transition.type.PUSH_RIGHT`
170+
171+
***
172+
159173
### `setViews(views, [options])`
160174

161175
Replaces the views currently managed by the navigationController

examples/src/view.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ class View extends React.Component {
4848
transition: Transition.type.COVER_UP
4949
});
5050
}
51+
onPopToRoot() {
52+
this.props.navigationController.popToRootView({
53+
transition: this.props.modal ? Transition.type.REVEAL_DOWN : Transition.type.PUSH_RIGHT
54+
});
55+
}
5156
render() {
5257
return (
5358
<div
@@ -65,6 +70,7 @@ class View extends React.Component {
6570
<button onClick={this.onModal.bind(this)}>
6671
Show Modal
6772
</button>
73+
{this.renderPopToRootButton()}
6874
</section>
6975
</div>
7076
);
@@ -80,6 +86,11 @@ class View extends React.Component {
8086
? <div />
8187
: <button onClick={this.onNext.bind(this)}>Next</button>;
8288
}
89+
renderPopToRootButton() {
90+
return this.props.index === 1
91+
? <div />
92+
: <button onClick={this.onPopToRoot.bind(this)}>Pop To Root</button>;
93+
}
8394
}
8495

8596
View.defaultProps ={

spec/navigation-controller.spec.jsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const transformPrefix = getVendorPrefix('transform');
2020
const View = require('../examples/src/view');
2121
class ViewA extends View { }
2222
class ViewB extends View { }
23+
class ViewC extends View { }
2324

2425
describe('NavigationController', () => {
2526
const views = [
@@ -594,6 +595,145 @@ describe('NavigationController', () => {
594595
});
595596
});
596597
});
598+
describe('#__popToRootView', () => {
599+
beforeEach(done => {
600+
controller = renderIntoDocument(
601+
<NavigationController views={[<ViewA />,<ViewB />,<ViewC />]} />
602+
);
603+
requestAnimationFrame(() => {
604+
done();
605+
});
606+
});
607+
it('throws an error if an only one view is in the stack', () => {
608+
controller.state.views = [<ViewA />];
609+
expect(() => {
610+
controller.__popToRootView()
611+
}).to.throw(/stack/);
612+
});
613+
it('returns early if the controller is already transitioning', () => {
614+
const spy = sinon.spy(controller, 'setState');
615+
controller.__isTransitioning = true;
616+
controller.__popToRootView();
617+
expect(spy.called).not.to.be.true;
618+
});
619+
it('shows the view wrappers', () => {
620+
const spy = sinon.spy(controller, '__displayViews');
621+
controller.__popToRootView();
622+
expect(spy.calledWith('block')).to.be.true;
623+
});
624+
it('removes all but the root view from state.views', (done) => {
625+
controller.__popToRootView({
626+
onComplete() {
627+
expect(controller.state.views).to.have.length(1);
628+
expect(controller.state.views[0].type).to.equal(ViewA);
629+
done();
630+
},
631+
transition: Transition.type.NONE
632+
});
633+
});
634+
it('sets state.transition', (done) => {
635+
controller.__popToRootView({
636+
transition: Transition.type.NONE,
637+
onComplete() {
638+
done();
639+
}
640+
});
641+
requestAnimationFrame(() => {
642+
expect(controller.state.transition).to.equal(Transition.type.NONE);
643+
});
644+
});
645+
it('sets state.mountedViews', (done) => {
646+
const [prev,next] = controller.__viewIndexes;
647+
controller.__popToRootView({
648+
transition: Transition.type.PUSH_RIGHT,
649+
onComplete() {
650+
done();
651+
}
652+
});
653+
requestAnimationFrame(() => {
654+
expect(controller.state.mountedViews[prev].type).to.equal(ViewC);
655+
expect(controller.state.mountedViews[next].type).to.equal(ViewA);
656+
});
657+
});
658+
it('transitions the views', (done) => {
659+
const spy = sinon.spy(controller, '__transitionViews');
660+
controller.__popToRootView({ transition: Transition.type.NONE });
661+
requestAnimationFrame(() => {
662+
expect(spy.calledOnce).to.be.true;
663+
done();
664+
});
665+
});
666+
it('sets __isTransitioning=true', () => {
667+
controller.__popToRootView({ transition: Transition.type.NONE });
668+
expect(controller.__isTransitioning).to.be.true;
669+
});
670+
it('calls the onComplete callback', (done) => {
671+
controller.__popToRootView({
672+
onComplete() {
673+
expect(true).to.be.true;
674+
done();
675+
}
676+
});
677+
});
678+
it('does not rehydrate the state', (done) => {
679+
controller = renderIntoDocument(
680+
<NavigationController views={[<ViewA />]} preserveState={false} />
681+
);
682+
requestAnimationFrame(() => {
683+
var rootView = controller.refs[`view-${controller.__viewIndexes[0]}`];
684+
rootView.setState({
685+
foo: 'bar'
686+
});
687+
controller.pushView(<ViewB />, {
688+
transition: Transition.type.NONE,
689+
onComplete() {
690+
controller.pushView(<ViewC />, {
691+
transition: Transition.type.NONE,
692+
onComplete() {
693+
controller.popToRootView({
694+
transition: Transition.type.NONE,
695+
onComplete() {
696+
rootView = controller.refs[`view-${controller.__viewIndexes[1]}`];
697+
expect(rootView.state)
698+
.not.to.have.property('foo');
699+
done();
700+
}
701+
});
702+
}
703+
})
704+
}
705+
});
706+
});
707+
});
708+
it('rehydrates the state', (done) => {
709+
controller = renderIntoDocument(
710+
<NavigationController views={[<ViewA />]} preserveState={true} />
711+
);
712+
requestAnimationFrame(() => {
713+
controller.refs[`view-${controller.__viewIndexes[0]}`].setState({
714+
foo: 'bar'
715+
});
716+
controller.pushView(<ViewB />, {
717+
transition: Transition.type.NONE,
718+
onComplete() {
719+
controller.pushView(<ViewC />, {
720+
transition: Transition.type.NONE,
721+
onComplete() {
722+
controller.popToRootView({
723+
transition: Transition.type.NONE,
724+
onComplete() {
725+
expect(controller.refs[`view-${controller.__viewIndexes[1]}`].state)
726+
.to.have.property('foo');
727+
done();
728+
}
729+
});
730+
}
731+
})
732+
}
733+
});
734+
});
735+
});
736+
});
597737
describe('#__setViews', () => {
598738
beforeEach(done => {
599739
requestAnimationFrame(() => {

src/navigation-controller.jsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ const optionTypes = {
4545
]),
4646
onComplete: React.PropTypes.func
4747
},
48+
popToRootView: {
49+
transition: React.PropTypes.oneOfType([
50+
React.PropTypes.func,
51+
React.PropTypes.number
52+
]),
53+
onComplete: React.PropTypes.func
54+
},
4855
setViews: {
4956
views: React.PropTypes.arrayOf(
5057
React.PropTypes.element
@@ -448,6 +455,51 @@ class NavigationController extends React.Component {
448455
this.__pushView(last(views), options);
449456
}
450457

458+
__popToRootView(options) {
459+
options = typeof options === 'object' ? options : {};
460+
const defaults = {
461+
transition: Transition.type.PUSH_RIGHT
462+
};
463+
options = assign({}, defaults, options);
464+
checkOptions('popToRootView', options);
465+
if (this.state.views.length === 1) {
466+
throw new Error('popToRootView() can only be called with two or more views in the stack')
467+
};
468+
if (this.__isTransitioning) return;
469+
const {transition} = options;
470+
const [prev,next] = this.__viewIndexes;
471+
const rootView = this.state.views[0];
472+
const topView = last(this.state.views);
473+
const mountedViews = [];
474+
mountedViews[prev] = topView;
475+
mountedViews[next] = rootView;
476+
// Display only the root view
477+
const views = [rootView];
478+
// Show the wrappers
479+
this.__displayViews('block');
480+
// Pop from the top view, all the way to the root view
481+
this.setState({
482+
transition,
483+
views,
484+
mountedViews
485+
}, () => {
486+
// The view that will be shown
487+
const rootView = this.refs[`view-1`];
488+
if (rootView && this.state.preserveState) {
489+
const state = this.__viewStates[0];
490+
// Rehydrate the state
491+
if (state) {
492+
rootView.setState(state);
493+
}
494+
}
495+
// Clear view states
496+
this.__viewStates.length = 0;
497+
// Transition
498+
this.__transitionViews(options);
499+
});
500+
this.__isTransitioning = true;
501+
}
502+
451503
pushView() {
452504
this.__pushView.apply(this, arguments);
453505
}
@@ -456,6 +508,10 @@ class NavigationController extends React.Component {
456508
this.__popView.apply(this, arguments);
457509
}
458510

511+
popToRootView() {
512+
this.__popToRootView.apply(this, arguments);
513+
}
514+
459515
setViews() {
460516
this.__setViews.apply(this, arguments);
461517
}

0 commit comments

Comments
 (0)