Remove NavExperimental from UIExplorer

Summary: Simplify the UIExplorer setup, and remove dependency on NavigationExperimental, which is being phased out in favor of React Navigation.

Reviewed By: mkonicek

Differential Revision: D4627131

fbshipit-source-id: 6294623a885074a73c831b0d817770fbe8a90221
This commit is contained in:
Eric Vicenti 2017-02-28 15:39:24 -08:00 committed by Facebook Github Bot
parent db8cec4331
commit 761d528153
7 changed files with 164 additions and 340 deletions

View File

@ -23,22 +23,35 @@
*/
'use strict';
export type UIExplorerListWithFilterAction = {
type: 'UIExplorerListWithFilterAction',
filter: ?string;
export type UIExplorerBackAction = {
type: 'UIExplorerBackAction',
};
export type UIExplorerListAction = {
type: 'UIExplorerListAction',
};
export type UIExplorerExampleAction = {
type: 'UIExplorerExampleAction',
openExample: string;
openExample: string,
};
export type UIExplorerAction = UIExplorerListWithFilterAction | UIExplorerExampleAction;
export type UIExplorerAction = (
UIExplorerBackAction |
UIExplorerListAction |
UIExplorerExampleAction
);
function ExampleListWithFilter(filter: ?string): UIExplorerListWithFilterAction {
function Back(): UIExplorerBackAction {
return {
type: 'UIExplorerListWithFilterAction',
filter,
type: 'UIExplorerBackAction',
};
}
function ExampleList(): UIExplorerListAction {
return {
type: 'UIExplorerListAction',
};
}
@ -50,7 +63,8 @@ function ExampleAction(openExample: string): UIExplorerExampleAction {
}
const UIExplorerActions = {
ExampleListWithFilter,
Back,
ExampleList,
ExampleAction,
};

View File

@ -33,18 +33,18 @@ const React = require('react');
const StatusBar = require('StatusBar');
const StyleSheet = require('StyleSheet');
const ToolbarAndroid = require('ToolbarAndroid');
const UIExplorerActions = require('./UIExplorerActions');
const UIExplorerExampleContainer = require('./UIExplorerExampleContainer');
const UIExplorerExampleList = require('./UIExplorerExampleList');
const UIExplorerList = require('./UIExplorerList');
const UIExplorerNavigationReducer = require('./UIExplorerNavigationReducer');
const UIExplorerStateTitleMap = require('./UIExplorerStateTitleMap');
const UIManager = require('UIManager');
const URIActionMap = require('./URIActionMap');
const View = require('View');
const nativeImageSource = require('nativeImageSource');
import type {UIExplorerNavigationState} from './UIExplorerNavigationReducer';
import type { UIExplorerNavigationState } from './UIExplorerNavigationReducer';
UIManager.setLayoutAnimationEnabledExperimental(true);
@ -54,27 +54,31 @@ type Props = {
exampleFromAppetizeParams: string,
};
type State = UIExplorerNavigationState & {
externalExample: ?string,
};
const APP_STATE_KEY = 'UIExplorerAppState.v2';
const HEADER_LOGO_ICON = nativeImageSource({
android: 'launcher_icon',
width: 132,
height: 144
});
const HEADER_NAV_ICON = nativeImageSource({
android: 'ic_menu_black_24dp',
width: 48,
height: 48
});
class UIExplorerApp extends React.Component {
_handleAction: Function;
_renderDrawerContent: Function;
state: State;
constructor(props: Props) {
super(props);
this._handleAction = this._handleAction.bind(this);
this._renderDrawerContent = this._renderDrawerContent.bind(this);
}
props: Props;
state: UIExplorerNavigationState;
componentWillMount() {
BackAndroid.addEventListener('hardwareBackPress', this._handleBackButtonPress.bind(this));
BackAndroid.addEventListener('hardwareBackPress', this._handleBackButtonPress);
}
componentDidMount() {
Linking.getInitialURL().then((url) => {
AsyncStorage.getItem('UIExplorerAppState', (err, storedString) => {
AsyncStorage.getItem(APP_STATE_KEY, (err, storedString) => {
const exampleAction = URIActionMap(this.props.exampleFromAppetizeParams);
const urlAction = URIActionMap(url);
const launchAction = exampleAction || urlAction;
@ -116,7 +120,7 @@ class UIExplorerApp extends React.Component {
);
}
_renderDrawerContent() {
_renderDrawerContent = () => {
return (
<View style={styles.drawerContentWrapper}>
<UIExplorerExampleList
@ -127,47 +131,33 @@ class UIExplorerApp extends React.Component {
/>
</View>
);
}
};
_renderApp() {
const {
externalExample,
stack,
openExample,
} = this.state;
if (externalExample) {
const Component = UIExplorerList.Modules[externalExample];
return (
<Component
onExampleExit={() => {
this._handleAction({ type: 'BackAction' });
}}
ref={(example) => { this._exampleRef = example; }}
/>
);
}
const title = UIExplorerStateTitleMap(stack.routes[stack.index]);
const index = stack.routes.length <= 1 ? 1 : stack.index;
if (stack && stack.routes[index]) {
const {key} = stack.routes[index];
const ExampleModule = UIExplorerList.Modules[key];
if (ExampleModule) {
if (openExample) {
const ExampleModule = UIExplorerList.Modules[openExample];
if (ExampleModule.external) {
return (
<ExampleModule
onExampleExit={() => {
this._handleAction(UIExplorerActions.Back());
}}
ref={(example) => { this._exampleRef = example; }}
/>
);
} else if (ExampleModule) {
return (
<View style={styles.container}>
<ToolbarAndroid
logo={nativeImageSource({
android: 'launcher_icon',
width: 132,
height: 144
})}
navIcon={nativeImageSource({
android: 'ic_menu_black_24dp',
width: 48,
height: 48
})}
logo={HEADER_LOGO_ICON}
navIcon={HEADER_NAV_ICON}
onIconClicked={() => this.drawer.openDrawer()}
style={styles.toolbar}
title={title}
title={ExampleModule.title}
/>
<UIExplorerExampleContainer
module={ExampleModule}
@ -177,46 +167,38 @@ class UIExplorerApp extends React.Component {
);
}
}
return (
<View style={styles.container}>
<ToolbarAndroid
logo={nativeImageSource({
android: 'launcher_icon',
width: 132,
height: 144
})}
navIcon={nativeImageSource({
android: 'ic_menu_black_24dp',
width: 48,
height: 48
})}
logo={HEADER_LOGO_ICON}
navIcon={HEADER_NAV_ICON}
onIconClicked={() => this.drawer.openDrawer()}
style={styles.toolbar}
title={title}
title="UIExplorer"
/>
<UIExplorerExampleList
onNavigate={this._handleAction}
list={UIExplorerList}
{...stack.routes[0]}
/>
</View>
);
}
_handleAction(action: Object): boolean {
_handleAction = (action: Object): boolean => {
this.drawer && this.drawer.closeDrawer();
const newState = UIExplorerNavigationReducer(this.state, action);
if (this.state !== newState) {
this.setState(
newState,
() => AsyncStorage.setItem('UIExplorerAppState', JSON.stringify(this.state))
() => AsyncStorage.setItem(APP_STATE_KEY, JSON.stringify(this.state))
);
return true;
}
return false;
}
};
_handleBackButtonPress() {
_handleBackButtonPress = () => {
if (this._overrideBackPressForDrawerLayout) {
// This hack is necessary because drawer layout provides an imperative API
// with open and close methods. This code would be cleaner if the drawer
@ -231,8 +213,8 @@ class UIExplorerApp extends React.Component {
) {
return true;
}
return this._handleAction({ type: 'BackAction' });
}
return this._handleAction(UIExplorerActions.Back());
};
}
const styles = StyleSheet.create({

View File

@ -27,62 +27,46 @@ const AsyncStorage = require('AsyncStorage');
const Linking = require('Linking');
const React = require('react');
const ReactNative = require('react-native');
const UIExplorerActions = require('./UIExplorerActions');
const UIExplorerExampleContainer = require('./UIExplorerExampleContainer');
const UIExplorerExampleList = require('./UIExplorerExampleList');
const UIExplorerList = require('./UIExplorerList.ios');
const UIExplorerNavigationReducer = require('./UIExplorerNavigationReducer');
const UIExplorerStateTitleMap = require('./UIExplorerStateTitleMap');
const URIActionMap = require('./URIActionMap');
const {
Button,
AppRegistry,
NavigationExperimental,
SnapshotViewIOS,
StyleSheet,
Text,
View,
} = ReactNative;
const {
CardStack: NavigationCardStack,
Header: NavigationHeader,
} = NavigationExperimental;
import type { NavigationSceneRendererProps } from 'NavigationTypeDefinition';
import type { UIExplorerNavigationState } from './UIExplorerNavigationReducer';
import type { UIExplorerExample } from './UIExplorerList.ios';
import type { UIExplorerAction } from './UIExplorerActions';
import type { UIExplorerNavigationState } from './UIExplorerNavigationReducer';
type Props = {
exampleFromAppetizeParams: string,
};
type State = UIExplorerNavigationState & {
externalExample?: string,
};
const APP_STATE_KEY = 'UIExplorerAppState.v2';
const APP_STATE_KEY = 'UIExplorerAppState.v1';
const Header = ({ onBack, title}) => (
<View style={styles.header}>
<View style={styles.headerCenter}>
<Text style={styles.title}>{title}</Text>
</View>
{onBack && <View style={styles.headerLeft}>
<Button title="Back" onPress={onBack} />
</View>}
</View>
);
class UIExplorerApp extends React.Component {
_handleBack: Function;
_handleAction: Function;
_renderCard: Function;
_renderHeader: Function;
_renderScene: Function;
_renderTitleComponent: Function;
state: State;
constructor(props: Props) {
super(props);
}
componentWillMount() {
this._handleAction = this._handleAction.bind(this);
this._handleBack = this._handleAction.bind(this, {type: 'back'});
this._renderHeader = this._renderHeader.bind(this);
this._renderScene = this._renderScene.bind(this);
this._renderTitleComponent = this._renderTitleComponent.bind(this);
}
props: Props;
state: UIExplorerNavigationState;
componentDidMount() {
Linking.getInitialURL().then((url) => {
@ -92,7 +76,7 @@ class UIExplorerApp extends React.Component {
const launchAction = exampleAction || urlAction;
if (err || !storedString) {
const initialAction = launchAction || {type: 'InitialAction'};
this.setState(UIExplorerNavigationReducer(null, initialAction));
this.setState(UIExplorerNavigationReducer(undefined, initialAction));
return;
}
const storedState = JSON.parse(storedString);
@ -109,7 +93,11 @@ class UIExplorerApp extends React.Component {
});
}
_handleAction(action: Object) {
_handleBack = () => {
this._handleAction(UIExplorerActions.Back());
}
_handleAction = (action: ?UIExplorerAction) => {
if (!action) {
return;
}
@ -126,73 +114,58 @@ class UIExplorerApp extends React.Component {
if (!this.state) {
return null;
}
if (this.state.externalExample) {
const Component = UIExplorerList.Modules[this.state.externalExample];
return (
<Component
onExampleExit={() => {
this._handleAction({ type: 'BackAction' });
}}
/>
);
if (this.state.openExample) {
const Component = UIExplorerList.Modules[this.state.openExample];
if (Component.external) {
return (
<Component
onExampleExit={this._handleBack}
/>
);
} else {
return (
<View style={styles.exampleContainer}>
<Header onBack={this._handleBack} title={Component.title} />
<UIExplorerExampleContainer module={Component} />
</View>
);
}
}
return (
<NavigationCardStack
navigationState={this.state.stack}
style={styles.container}
renderHeader={this._renderHeader}
renderScene={this._renderScene}
onNavigateBack={this._handleBack}
/>
);
}
_renderHeader(props: NavigationSceneRendererProps): React.Element<any> {
return (
<NavigationHeader
{...props}
onNavigateBack={this._handleBack}
renderTitleComponent={this._renderTitleComponent}
/>
);
}
_renderTitleComponent(props: NavigationSceneRendererProps): React.Element<any> {
return (
<NavigationHeader.Title>
{UIExplorerStateTitleMap(props.scene.route)}
</NavigationHeader.Title>
);
}
_renderScene(props: NavigationSceneRendererProps): ?React.Element<any> {
const state = props.scene.route;
if (state.key === 'AppList') {
return (
<View style={styles.exampleContainer}>
<Header title="UIExplorer" />
<UIExplorerExampleList
onNavigate={this._handleAction}
list={UIExplorerList}
style={styles.exampleContainer}
{...state}
/>
);
}
const Example = UIExplorerList.Modules[state.key];
if (Example) {
return (
<View style={styles.exampleContainer}>
<UIExplorerExampleContainer module={Example} />
</View>
);
}
return null;
</View>
);
}
}
const styles = StyleSheet.create({
container: {
header: {
height: 60,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#96969A',
backgroundColor: '#F5F5F6',
flexDirection: 'row',
paddingTop: 20,
},
headerLeft: {
},
headerCenter: {
flex: 1,
position: 'absolute',
top: 27,
left: 0,
right: 0,
},
title: {
fontSize: 19,
fontWeight: '600',
textAlign: 'center',
},
exampleContainer: {
flex: 1,

View File

@ -154,9 +154,7 @@ class UIExplorerExampleList extends React.Component {
}}}
onNavigate={this.props.onNavigate}
onPress={() => {
this.props.onNavigate(
UIExplorerActions.ExampleListWithFilter('')
);
this.props.onNavigate(UIExplorerActions.ExampleList());
}}
/>
);

View File

@ -23,151 +23,47 @@
*/
'use strict';
const ReactNative = require('react-native');
// $FlowFixMe : This is a platform-forked component, and flow seems to only run on iOS?
const UIExplorerList = require('./UIExplorerList');
const {
NavigationExperimental,
} = ReactNative;
const {
StateUtils: NavigationStateUtils,
} = NavigationExperimental;
import type {NavigationState} from 'NavigationTypeDefinition';
export type UIExplorerNavigationState = {
externalExample: ?string;
stack: NavigationState;
openExample: ?string,
};
const defaultGetReducerForState = (initialState) => (state) => state || initialState;
function UIExplorerNavigationReducer(
state: ?UIExplorerNavigationState,
action: any
): UIExplorerNavigationState {
function getNavigationState(state: any): ?NavigationState {
if (
(state instanceof Object) &&
(state.routes instanceof Array) &&
(state.routes[0] !== undefined) &&
(typeof state.index === 'number') &&
(state.routes[state.index] !== undefined)
// Default value is to see example list
!state ||
// Handle the explicit list action
action.type === 'UIExplorerListAction' ||
// Handle requests to go back to the list when an example is open
(state.openExample && action.type === 'UIExplorerBackAction')
) {
return state;
}
return null;
}
function StackReducer({initialState, getReducerForState, getPushedReducerForAction}: any): Function {
const getReducerForStateWithDefault = getReducerForState || defaultGetReducerForState;
return function (lastState: ?NavigationState, action: any): NavigationState {
if (!lastState) {
return initialState;
}
const lastParentState = getNavigationState(lastState);
if (!lastParentState) {
return lastState;
}
const activeSubState = lastParentState.routes[lastParentState.index];
const activeSubReducer = getReducerForStateWithDefault(activeSubState);
const nextActiveState = activeSubReducer(activeSubState, action);
if (nextActiveState !== activeSubState) {
const nextChildren = [...lastParentState.routes];
nextChildren[lastParentState.index] = nextActiveState;
return {
...lastParentState,
routes: nextChildren,
};
}
const subReducerToPush = getPushedReducerForAction(action, lastParentState);
if (subReducerToPush) {
return NavigationStateUtils.push(
lastParentState,
subReducerToPush(null, action)
);
}
switch (action.type) {
case 'back':
case 'BackAction':
if (lastParentState.index === 0 || lastParentState.routes.length === 1) {
return lastParentState;
}
return NavigationStateUtils.pop(lastParentState);
}
return lastParentState;
};
}
const UIExplorerStackReducer = StackReducer({
getPushedReducerForAction: (action, lastState) => {
if (action.type === 'UIExplorerExampleAction' && UIExplorerList.Modules[action.openExample]) {
if (lastState.routes.find(route => route.key === action.openExample)) {
// The example is already open, we should avoid pushing examples twice
return null;
}
return (state) => state || {key: action.openExample};
}
return null;
},
getReducerForState: (initialState) => (state) => state || initialState,
initialState: {
key: 'UIExplorerMainStack',
index: 0,
routes: [
{key: 'AppList'},
],
},
});
function UIExplorerNavigationReducer(lastState: ?UIExplorerNavigationState, action: any): UIExplorerNavigationState {
if (!lastState) {
return {
externalExample: null,
stack: UIExplorerStackReducer(null, action),
};
}
if (action.type === 'UIExplorerListWithFilterAction') {
return {
externalExample: null,
stack: {
key: 'UIExplorerMainStack',
index: 0,
routes: [
{
key: 'AppList',
filter: action.filter,
},
],
},
};
}
if (action.type === 'BackAction' && lastState.externalExample) {
return {
...lastState,
externalExample: null,
// A null openExample will cause the views to display the UIExplorer example list
openExample: null,
};
}
if (action.type === 'UIExplorerExampleAction') {
// Make sure we see the module before returning the new state
const ExampleModule = UIExplorerList.Modules[action.openExample];
if (ExampleModule && ExampleModule.external) {
if (ExampleModule) {
return {
...lastState,
externalExample: action.openExample,
openExample: action.openExample,
};
}
}
const newStack = UIExplorerStackReducer(lastState.stack, action);
if (newStack !== lastState.stack) {
return {
externalExample: null,
stack: newStack,
};
}
return lastState;
return state;
}
module.exports = UIExplorerNavigationReducer;

View File

@ -1,41 +0,0 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @flow
* @providesModule UIExplorerStateTitleMap
*/
'use strict';
// $FlowFixMe : This is a platform-forked component, and flow seems to only run on iOS?
const UIExplorerList = require('./UIExplorerList');
import type {NavigationRoute} from 'NavigationTypeDefinition';
function StateTitleMap(route: NavigationRoute): string {
if (UIExplorerList.Modules[route.key]) {
return UIExplorerList.Modules[route.key].title;
}
if (route.key === 'AppList') {
return 'UIExplorer';
}
return 'Unknown';
}
module.exports = StateTitleMap;

View File

@ -32,7 +32,9 @@ const {
Alert,
} = ReactNative;
function PathActionMap(path: string): ?Object {
import type { UIExplorerAction } from './UIExplorerActions';
function PathActionMap(path: string): ?UIExplorerAction {
// Warning! Hacky parsing for example code. Use a library for this!
const exampleParts = path.split('/example/');
const exampleKey = exampleParts[1];
@ -46,11 +48,11 @@ function PathActionMap(path: string): ?Object {
return null;
}
function URIActionMap(uri: ?string): ?Object {
// Warning! Hacky parsing for example code. Use a library for this!
function URIActionMap(uri: ?string): ?UIExplorerAction {
if (!uri) {
return null;
}
// Warning! Hacky parsing for example code. Use a library for this!
const parts = uri.split('rnuiexplorer:/');
if (!parts[1]) {
return null;