Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support passing props to tab routes #1057

Merged
merged 1 commit into from
Sep 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}`);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not modify existing state, but produce new one. Here i see you modify existing child within existing state var.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good looking out. It just seemed like the way to go with all of the shallow slices pointing back to previous state objects, and direct setting of state vars here and here.

This does give me a change to better cover edge cases though. I noticed when nesting tabs things stop behaving as expected. I'm thinking at least tests for double nested tab routes should cover it. I'll take another stab at it this weekend.


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');
});
});