Skip to content

Commit

Permalink
Support passing props to tabs=true routes (#1057)
Browse files Browse the repository at this point in the history
  • Loading branch information
benstepp authored and aksonov committed Sep 1, 2016
1 parent 2ff66a2 commit d03e70b
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 10 deletions.
1 change: 1 addition & 0 deletions src/Actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,5 @@ class Actions {
}
}

export { Actions as ActionsTest };
export default new Actions();
10 changes: 8 additions & 2 deletions src/Reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function inject(state, action, props, scenes) {
return state;
}
let ind;
let children;

switch (ActionMap[action.type]) {
case ActionConst.POP_TO: {
Expand Down Expand Up @@ -163,13 +164,18 @@ function inject(state, action, props, scenes) {
case ActionConst.JUMP:
assert(state.tabs, `Parent=${state.key} is not tab bar, jump action is not valid`);
ind = -1;
state.children.forEach((c, i) => { if (c.sceneKey === action.key) { ind = i; } });
children = getInitialState(props, scenes, ind, action);
children = Array.isArray(children) ? children : [children];
children.forEach((child, i) => {
if (child.sceneKey === action.key) ind = i;
});

assert(ind !== -1, `Cannot find route with key=${action.key} for parent=${state.key}`);

if (action.unmountScenes) {
resetHistoryStack(state.children[ind]);
}
return { ...state, index: ind };
return { ...state, index: ind, children };
case ActionConst.REPLACE:
if (state.children[state.index].sceneKey === action.key) {
return state;
Expand Down
7 changes: 7 additions & 0 deletions src/State.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*
*/
import { assert } from './Util';
import * as ActionConst from './ActionConst';

function getStateFromScenes(route, scenes, props) {
const getters = [];
Expand Down Expand Up @@ -69,6 +70,12 @@ export function getInitialState(
res.children = [getInitialState(scenes[route.children[index]], scenes, 0, props)];
res.index = 0;
}

// Copy props to the children of tab routes
if (route.type === ActionConst.JUMP) {
res.children = res.children.map(child => ({ ...props, ...child }));
}

res.key = `${position}_${res.key}`;
return res;
}
Expand Down
139 changes: 133 additions & 6 deletions test/Actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { expect } from 'chai';

import React from 'react';

import Actions from '../src/Actions';
import * as ActionConst from '../src/ActionConst';
import { ActionsTest } from '../src/Actions';
import Scene from '../src/Scene';

let id = 0;
const guid = () => id++;
const noop = () => {};

const scenesData = (
<Scene
Expand Down Expand Up @@ -39,13 +41,11 @@ const scenesData = (
</Scene>
</Scene>);

let scenes;

describe('Actions', () => {
before(() => {
scenes = Actions.create(scenesData);
});
it('should produce needed actions', () => {
const Actions = new ActionsTest();
const scenes = Actions.create(scenesData);

// check scenes
expect(scenes.conversations.component).to.equal('Conversations');

Expand All @@ -67,4 +67,131 @@ describe('Actions', () => {
expect(latestAction.param3).equal('Hello world3');
expect(latestAction.key).equal('messaging');
});

it('throws when not providing a root scene', () => {
const Actions = new ActionsTest();
const scene = void 0;
expect(() => Actions.create(scene)).to.throw(Error, 'root scene');
});

it('throws when using a reserved method', () => {
const scene = (
<Scene key="root" component={noop}>
<Scene key="create" component={noop} />
</Scene>
);

const Actions = new ActionsTest();
expect(() => Actions.create(scene)).to.throw(Error, 'create');
});

it('throws when using an action method', () => {
const scene = (
<Scene key="root" component={noop}>
<Scene key="push" component={noop} />
</Scene>
);

const Actions = new ActionsTest();
expect(() => Actions.create(scene)).to.throw(Error, 'push');
});

it('wraps child scenes if the parent is tabs', () => {
const scene = (
<Scene key="root" component={noop}>
<Scene key="main" tabs>
<Scene key="home" component={noop} />
<Scene key="map" component={noop} />
<Scene key="myAccount" component={noop} />
</Scene>
</Scene>
);
const Actions = new ActionsTest();
const scenes = Actions.create(scene);

const tabKeys = ['home', 'map', 'myAccount'];
tabKeys.forEach(key => {
expect(scenes[key].component).to.eq(void 0);
expect(scenes[key].type).to.eq(ActionConst.JUMP);

const wrappedKey = scenes[key].children[0];
expect(scenes[wrappedKey].component).to.not.eq(void 0);
expect(scenes[wrappedKey].type).to.eq(ActionConst.PUSH);
});
});

it('provides keys to children of a scene', () => {
const scene = (
<Scene key="root" component={noop}>
<Scene key="home" component={noop} />
<Scene key="map" component={noop} />
<Scene key="myAccount" component={noop} />
</Scene>
);

const Actions = new ActionsTest();
const scenes = Actions.create(scene);

const childrenKeys = ['home', 'map', 'myAccount'];
expect(scenes.root.children).to.include.all(...childrenKeys);
});

it('substates have their base set to their parent', () => {
const scene = (
<Scene key="root" component={noop}>
<Scene key="view" type={ActionConst.REFRESH} />
<Scene key="edit" type={ActionConst.REFRESH} edit />
<Scene key="save" type={ActionConst.REFRESH} save />
</Scene>
);

const Actions = new ActionsTest();
const scenes = Actions.create(scene);

const subStates = ['view', 'edit', 'save'];
subStates.forEach(key => {
expect(scenes[key].base).to.eq('root');
expect(scenes[key].parent).to.eq(scenes.root.parent);
});
});

it('substates do not need to specify REFRESH type', () => {
const scene = (
<Scene key="root" component={noop}>
<Scene key="view" />
<Scene key="edit" edit />
<Scene key="save" save />
</Scene>
);

const Actions = new ActionsTest();
const scenes = Actions.create(scene);

const subStates = ['view', 'edit', 'save'];
subStates.forEach(key => {
expect(scenes[key].type).to.eq(ActionConst.REFRESH);
});
});

it('allows mixing of substates with children', () => {
const scene = (
<Scene key="root" component={noop}>
<Scene key="view" />
<Scene key="edit" edit />
<Scene key="save" save />
<Scene key="messaging" component={noop}>
<Scene key="conversations" component={noop} />
</Scene>
</Scene>
);

const Actions = new ActionsTest();
const scenes = Actions.create(scene);

const subStates = ['view', 'edit', 'save'];
subStates.forEach(key => {
expect(scenes[key].type).to.eq(ActionConst.REFRESH);
});
expect(scenes.messaging.type).to.eq(ActionConst.PUSH);
});
});
152 changes: 150 additions & 2 deletions test/Reducer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import React from 'react';
import { expect } from 'chai';

import Scene from '../src/Scene';
import Actions from '../src/Actions';
import { ActionsTest } from '../src/Actions';
import * as ActionConst from '../src/ActionConst';

import createReducer from '../src/Reducer';
import getInitialState from '../src/State';

Expand Down Expand Up @@ -54,6 +53,7 @@ describe('createReducer', () => {
// create scenes and initialState
// For creating scenes we use external modules.
// TODO: Think about fully isolated test.
const Actions = new ActionsTest();
const scenes = Actions.create(scenesData);
const initialState = getInitialState(scenes); // TODO: write test for this.

Expand Down Expand Up @@ -109,3 +109,151 @@ describe('createReducer', () => {
expect(currentScene.param1).equal('Conversations new param');
});
});

describe('passing props from actions', () => {
it('passes props for normal scenes', () => {
const noop = () => {};
const scene = (
<Scene key="root" component={noop}>
<Scene key="hello" component={noop} initial />
<Scene key="world" component={noop} />
</Scene>
);

const Actions = new ActionsTest();
const scenes = Actions.create(scene);
const initialState = getInitialState(scenes);
const reducer = createReducer({ initialState, scenes });

let state = { ...initialState, scenes };
let current = getCurrent(state);
Actions.callback = action => {
state = reducer(state, action);
current = getCurrent(state);
};

Actions.hello({ customProp: 'Hello' });
expect(current.customProp).to.eq('Hello');
Actions.world({ customProp: 'World' });
expect(current.customProp).to.eq('World');
Actions.hello();
expect(current.customProp).to.eq(void 0);
});

it('passes props for tab scenes', () => {
const noop = () => {};
const scene = (
<Scene key="root" component={noop} tabs>
<Scene key="home" component={noop} />
<Scene key="map" component={noop} />
</Scene>
);

const Actions = new ActionsTest();
const scenes = Actions.create(scene);
const initialState = getInitialState(scenes);
const reducer = createReducer({ initialState, scenes });

let state = { ...initialState, scenes };
let current = getCurrent(state);
Actions.callback = action => {
state = reducer(state, action);
current = getCurrent(state);
};

Actions.home({ customProp: 'Home' });
expect(current.customProp).to.eq('Home');
Actions.map({ customProp: 'Map', anotherProp: 'Another' });
expect(current.customProp).to.eq('Map');
expect(current.anotherProp).to.eq('Another');

Actions.home();
expect(current.customProp).to.eq(void 0);
expect(current.anotherProp).to.eq(void 0);
});

it('passes props for nested tab scenes', () => {
const noop = () => {};
const scene = (
<Scene key="root" component={noop} tabs>
<Scene key="home" component={noop} />
<Scene key="map" component={noop} tabs>
<Scene key="nested" component={noop} />
<Scene key="nested2" component={noop} />
</Scene>
</Scene>
);

const Actions = new ActionsTest();
const scenes = Actions.create(scene);
const initialState = getInitialState(scenes);
const reducer = createReducer({ initialState, scenes });

let state = { ...initialState, scenes };
let current = getCurrent(state);
Actions.callback = action => {
state = reducer(state, action);
current = getCurrent(state);
};

Actions.home({ customProp: 'Home', anotherProp: 'Another' });
expect(current.customProp).to.eq('Home');
expect(current.anotherProp).to.eq('Another');

Actions.map();
expect(current.customProp).to.eq(void 0);
expect(current.anotherProp).to.eq(void 0);

Actions.nested2({ customProp: 'Custom' });
expect(current.customProp).to.eq('Custom');
expect(current.anotherProp).to.eq(void 0);
});

it('passes props for very nested tab scenes', () => {
const noop = () => {};
const scene = (
<Scene key="root" component={noop} tabs>
<Scene key="home" component={noop} />
<Scene key="map" component={noop} tabs>
<Scene key="nested" component={noop} />
<Scene key="nested2" component={noop} />
<Scene key="nestedTabs" component={noop} tabs>
<Scene key="nestedTab" component={noop} />
<Scene key="nestedTab2" component={noop} />
</Scene>
</Scene>
<Scene key="normal" component={noop} />
</Scene>
);

const Actions = new ActionsTest();
const scenes = Actions.create(scene);
const initialState = getInitialState(scenes);
const reducer = createReducer({ initialState, scenes });

let state = { ...initialState, scenes };
let current = getCurrent(state);
Actions.callback = action => {
state = reducer(state, action);
current = getCurrent(state);
};

Actions.home({ customProp: 'Home', anotherProp: 'Another' });
expect(current.customProp).to.eq('Home');
expect(current.anotherProp).to.eq('Another');

Actions.map();
expect(current.customProp).to.eq(void 0);
expect(current.anotherProp).to.eq(void 0);

Actions.nestedTabs({ customProp: 'Custom' });
expect(current.customProp).to.eq('Custom');
expect(current.anotherProp).to.eq(void 0);

Actions.nestedTab2();
expect(current.customProp).to.eq(void 0);

Actions.map({ customProp: 'Custom' });
expect(current.customProp).to.eq('Custom');
});
});

0 comments on commit d03e70b

Please sign in to comment.