[Navigator]: Add a method keyOf to NavigationRouteStack.

Summary:
# Summary

Add a method `keyOf` to NavigationRouteStack.

The method `keyOf` returns a key that is associated with the route.
The a route is added to a stack, the stack creats an unique key for it and
will keep the key for the route until the route is rmeoved from the stack.

The stack also passes the keys to its derived stack (the new stack created by the
mutation API such as `push`, `pop`...etc).

The key for the route persists until the initial stack and its derived stack no longer
contains this route.

# Why Do We Need This?

Navigator has needs to use an unique key to manage the scenes rendered.
The problem is that `route` itself isn't a very reliable thing to be used as the key.

Consider this example:

```
// `scene_1` animates into the viewport.
navigator.push('scene_1');

setTimeout(() => {
 // `scene_1` animates off the viewport.
 navigator.pop();
}, 100);

setTimeout(() => {
 // Should we bring in a new scene or bring back the one that was previously popped?
 navigator.push('scene_1');
}, 200);

```

Because we currently use `route` itself as a key for the scene, we'd have to block a route
until its scene is completely off the components tree even the route itself is no longer
in the stack otherwise we'd see strange animation of jumping scenes.

# What's Next

We're hoping that we can build pure reactive view for NavigationRouteStack easily.
The naive implementation of  NavigationRouteStackView may look like this:

```
class NavigationRouteStackView {
  constructor() {
    this.state = {
      staleScenes: {},
    };
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.stack !== this.props.stack) {
      var stale;
      var staleScenes = {...this.state.staleScenes};
      this.props.stack.forEach((route, index, key) => {
        if (nextProps.stack.keyOf(route) !== key) {
          stale = true;
          staleScenes[key] = {route, index, key, stale};
        }
      });
      if (stale) {
        this.setState({
          staleScenes,
        });
      }
    }
  }

  render() {
    var scenes = [];

    this.props.stack.forEach((route, index, key) => {
      scenes.push({route, index, key});
    });

    Object.keys(this.state.staleScenes).forEach(key => {
      scenes.push(this.state.staleScenes[key]);
    });

    scenes.sort(stableSortByIndex);

    return <View>{scenes.map(renderScene)}</View>;
  }
}

```
This commit is contained in:
Hedger Wang
2015-07-29 16:42:46 -07:00
parent 37636fc59a
commit 4b16e4d550
2 changed files with 229 additions and 46 deletions

View File

@@ -24,15 +24,23 @@
*/
'use strict';
jest
.dontMock('NavigationRouteStack')
.dontMock('clamp')
.dontMock('invariant')
.dontMock('immutable');
.autoMockOff()
.mock('ErrorUtils');
var NavigationRouteStack = require('NavigationRouteStack');
describe('NavigationRouteStack:', () => {
// Different types of routes.
var ROUTES = [
'foo',
1,
true,
{foo: 'bar'},
['foo'],
];
// Basic
it('gets index', () => {
var stack = new NavigationRouteStack(1, ['a', 'b', 'c']);
@@ -76,6 +84,92 @@ describe('NavigationRouteStack:', () => {
expect(stack.indexOf('c')).toBe(-1);
});
// Key
it('gets key for route', () => {
var test = (route) => {
var stack = new NavigationRouteStack(0, ['a']);
var key = stack.push(route).keyOf(route);
expect(typeof key).toBe('string');
expect(!!key).toBe(true);
};
ROUTES.forEach(test);
});
it('gets a key of larger value for route', () => {
var lastKey = '';
var test = (route) => {
var stack = new NavigationRouteStack(0, ['a']);
var key = stack.push(route).keyOf(route);
expect(key > lastKey).toBe(true);
lastKey = key;
};
ROUTES.forEach(test);
});
it('gets an unique key for a different route', () => {
var stack = new NavigationRouteStack(0, ['a']);
var keys = {};
var test = (route) => {
stack = stack.push(route);
var key = stack.keyOf(route);
expect(keys[key]).toBe(undefined);
keys[key] = true;
};
ROUTES.forEach(test);
});
it('gets the same unique key for the same route', () => {
var test = (route) => {
var stack = new NavigationRouteStack(0, [route]);
expect(stack.keyOf(route)).toBe(stack.keyOf(route));
};
ROUTES.forEach(test);
});
it('gets the same unique key form the derived stack', () => {
var test = (route) => {
var stack = new NavigationRouteStack(0, [route]);
var derivedStack = stack.push('wow').pop().slice(0, 10).push('blah');
expect(derivedStack.keyOf(route)).toBe(stack.keyOf(route));
};
ROUTES.forEach(test);
});
it('gets a different key from a different stack', () => {
var test = (route) => {
var stack1 = new NavigationRouteStack(0, [route]);
var stack2 = new NavigationRouteStack(0, [route]);
expect(stack1.keyOf(route)).not.toBe(stack2.keyOf(route));
};
ROUTES.forEach(test);
});
it('gets no key for a route that does not contains this route', () => {
var stack = new NavigationRouteStack(0, ['a']);
expect(stack.keyOf('b')).toBe(null);
});
it('gets a new key for a route that was removed and added again', () => {
var test = (route) => {
var stack = new NavigationRouteStack(0, ['a']);
var key1 = stack.push(route).keyOf(route);
var key2 = stack.push(route).pop().push(route).keyOf(route);
expect(key1).not.toBe(key2);
};
ROUTES.forEach(test);
});
// Slice
it('slices', () => {
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c', 'd']);
var stack2 = stack1.slice(1, 3);
@@ -226,4 +320,25 @@ describe('NavigationRouteStack:', () => {
stack.replaceAtIndex(100, 'x');
}).toThrow();
});
// Iteration
it('iterates each item', () => {
var stack = new NavigationRouteStack(0, ['a', 'b']);
var logs = [];
var keys = {};
stack.forEach((route, index, key) => {
logs.push([
route,
index,
(typeof key === 'string' && key.length > 0 && !(key in keys)),
]);
keys[key] = true;
});
expect(logs).toEqual([
['a', 0, true],
['b', 1, true],
]);
});
});