mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-05-17 04:32:26 +00:00
Add APIs
- AppRegistry - AppState - AsyncStorage - Dimensions - NativeMethods - NetInfo - PixelRatio - Platform - UIManager
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# AppRegistry
|
||||
|
||||
`AppRegistry` is the control point for registering, running, prerendering, and
|
||||
unmounting all apps. App root components should register themselves with
|
||||
`AppRegistry.registerComponent`. Apps can be run by invoking
|
||||
`AppRegistry.runApplication`, and prerendered by invoking
|
||||
`AppRegistry.prerenderApplication` (see the [client and server rendering
|
||||
guide](../guides/rendering.md) for more details).
|
||||
|
||||
To "stop" an application when a view should be destroyed, call
|
||||
`AppRegistry.unmountApplicationComponentAtRootTag` with the tag that was passed
|
||||
into `runApplication`. These should always be used as a pair.
|
||||
|
||||
## Methods
|
||||
|
||||
(web) static **prerenderApplication**(appKey:string, appParameters: object)
|
||||
|
||||
Renders the given application to an HTML string. Use this for server-side
|
||||
rendering. Return object is of type `{ html: string; style: string; }`, where
|
||||
`html` the prerendered HTML, and `style` is the prerendered style sheet.
|
||||
|
||||
static **registerConfig**(config: Array<AppConfig>)
|
||||
|
||||
Registry multiple applications. `AppConfig` is of type `{ appKey: string;
|
||||
component: ComponentProvider; run?: Function }`.
|
||||
|
||||
static **registerComponent**(appKey: string, getComponentFunc: ComponentProvider)
|
||||
|
||||
Register a component provider under the given `appKey`.
|
||||
|
||||
static **registerRunnable**(appKey: string, run: Function)
|
||||
|
||||
Register a custom render function for an application. The function will receive
|
||||
the `appParameters` passed to `runApplication`.
|
||||
|
||||
static **getAppKeys**()
|
||||
|
||||
Returns all registered app keys.
|
||||
|
||||
static **runApplication**(appKey: string, appParameters?: object)
|
||||
|
||||
Runs the application that was registered under `appKey`. The `appParameters`
|
||||
must include the `rootTag` into which the application is rendered, and
|
||||
optionally any `initialProps`.
|
||||
|
||||
static **unmountApplicationComponentAtRootTag**(rootTag: HTMLElement)
|
||||
|
||||
To "stop" an application when a view should be destroyed, call
|
||||
`AppRegistry.unmountApplicationComponentAtRootTag` with the tag that was passed
|
||||
into `runApplication`
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
AppRegistry.registerComponent('MyApp', () => AppComponent)
|
||||
AppRegistry.runApplication('MyApp', {
|
||||
initialProps: {},
|
||||
rootTag: document.getElementById('react-root')
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
## AppState
|
||||
|
||||
`AppState` can tell you if the app is in the foreground or background, and
|
||||
notify you when the state changes.
|
||||
|
||||
States
|
||||
|
||||
* `active` - The app is running in the foreground
|
||||
* `background` - The app is running in the background (i.e., the user has not focused the app's tab).
|
||||
|
||||
## Properties
|
||||
|
||||
static **currentState**
|
||||
|
||||
Returns the current state of the app: `active` or `background`.
|
||||
|
||||
## Methods
|
||||
|
||||
static **addEventListener**(type: string, handler: Function)
|
||||
|
||||
Add a handler to `AppState` changes by listening to the `change` event type and
|
||||
providing the `handler`. The handler is called with the app state value.
|
||||
|
||||
static **removeEventListener**(type: string, handler: Function)
|
||||
|
||||
Remove a handler by passing the change event `type` and the `handler`.
|
||||
|
||||
## Examples
|
||||
|
||||
To see the current state, you can check `AppStateIOS.currentState`, which will
|
||||
be kept up-to-date. This example will only ever appear to say "Current state
|
||||
is: active" because the app is only visible to the user when in the `active`
|
||||
state, and the null state will happen only momentarily.
|
||||
|
||||
```js
|
||||
class Example extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { currentAppState: AppState.currentState }
|
||||
this._handleAppStateChange = this._handleAppStateChange.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AppState.addEventListener('change', this._handleAppStateChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this._handleAppStateChange);
|
||||
}
|
||||
|
||||
_handleAppStateChange(currentAppState) {
|
||||
this.setState({ currentAppState });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Text>Current state is: {this.state.currentAppState}</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
# AsyncStorage
|
||||
|
||||
`AsyncStorage` is a simple, asynchronous, persistent, key-value storage system
|
||||
that is global to the domain. It's a facade over, and should be used instead of
|
||||
`window.localStorage` to provide an asynchronous API and multi functions. Each
|
||||
method returns a `Promise` object.
|
||||
|
||||
It is recommended that you use an abstraction on top of `AsyncStorage` instead
|
||||
of `AsyncStorage` directly for anything more than light usage since it operates
|
||||
globally.
|
||||
|
||||
The batched functions are useful for executing a lot of operations at once,
|
||||
allowing for optimizations to provide the convenience of a single promise after
|
||||
all operations are complete.
|
||||
|
||||
## Methods
|
||||
|
||||
static **clear**()
|
||||
|
||||
Erases all AsyncStorage. You probably don't want to call this - use
|
||||
`removeItem` or `multiRemove` to clear only your own keys instead. Returns a
|
||||
Promise object.
|
||||
|
||||
static **getAllKeys**()
|
||||
|
||||
Gets all known keys. Returns a Promise object.
|
||||
|
||||
static **getItem**(key: string)
|
||||
|
||||
Fetches the value of the given key. Returns a Promise object.
|
||||
|
||||
static **mergeItem**(key: string, value: string)
|
||||
|
||||
Merges existing value with input value, assuming they are stringified JSON.
|
||||
Returns a Promise object.
|
||||
|
||||
static **multiGet**(keys: Array<string>)
|
||||
|
||||
`multiGet` results in an array of key-value pair arrays that matches the input
|
||||
format of `multiSet`. Returns a Promise object.
|
||||
|
||||
```js
|
||||
multiGet(['k1', 'k2']) -> [['k1', 'val1'], ['k2', 'val2']]
|
||||
```
|
||||
|
||||
static **multiMerge**(keyValuePairs: Array<Array<string>>)
|
||||
|
||||
multiMerge takes an array of key-value array pairs that match the output of
|
||||
`multiGet`. It merges existing values with input values, assuming they are
|
||||
stringified JSON. Returns a Promise object.
|
||||
|
||||
static **multiRemove**(keys: Array<string>)
|
||||
|
||||
Delete all the keys in the keys array. Returns a Promise object.
|
||||
|
||||
static **multiSet**(keyValuePairs: Array<Array<string>>)
|
||||
|
||||
`multiSet` takes an array of key-value array pairs that match the output of
|
||||
`multiGet`. Returns a Promise object.
|
||||
|
||||
```js
|
||||
multiSet([['k1', 'val1'], ['k2', 'val2']]);
|
||||
```
|
||||
|
||||
static **removeItem**(key: string)
|
||||
|
||||
Removes the value of the given key. Returns a Promise object.
|
||||
|
||||
static **setItem**(key: string, value: string)
|
||||
|
||||
Sets the value of the given key. Returns a Promise object.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Dimensions
|
||||
|
||||
Note: dimensions may change (e.g due to device rotation) so any rendering logic
|
||||
or styles that depend on these constants should try to call this function on
|
||||
every render, rather than caching the value.
|
||||
|
||||
## Methods
|
||||
|
||||
static **get**(dimension: string)
|
||||
|
||||
Get a dimension (e.g., `"window"` or `"screen"`).
|
||||
|
||||
Example: `const { height, width } = Dimensions.get('window')`
|
||||
@@ -0,0 +1,42 @@
|
||||
# NativeMethods
|
||||
|
||||
React Native for Web provides several methods to directly access the underlying
|
||||
DOM node. This can be useful in cases when you want to focus a view or measure
|
||||
its on-screen dimensions, for example.
|
||||
|
||||
The methods described are available on most of the default components provided
|
||||
by React Native for Web. Note, however, that they are *not* available on the
|
||||
composite components that you define in your own app. For more information, see
|
||||
[Direct Manipulation](../guides/direct-manipulation.md).
|
||||
|
||||
## Methods
|
||||
|
||||
**blur**()
|
||||
|
||||
Removes focus from an input or view. This is the opposite of `focus()`.
|
||||
|
||||
**focus**()
|
||||
|
||||
Requests focus for the given input or view. The exact behavior triggered will
|
||||
depend the type of view.
|
||||
|
||||
**measure**(callback: (x, y, width, height, pageX, pageY) => void)
|
||||
|
||||
For a given view, `measure` determines the offset relative to the parent view,
|
||||
width, height, and the offset relative to the viewport. Returns the values via
|
||||
an async callback.
|
||||
|
||||
Note that these measurements are not available until after the rendering has
|
||||
been completed.
|
||||
|
||||
**measureLayout**(relativeToNativeNode: DOMNode, onSuccess: (x, y, width, height) => void)
|
||||
|
||||
Like `measure`, but measures the view relative to another view, specified as
|
||||
`relativeToNativeNode`. This means that the returned `x`, `y` are relative to
|
||||
the origin `x`, `y` of the ancestor view.
|
||||
|
||||
**setNativeProps**(nativeProps: Object)
|
||||
|
||||
This function sends props straight to the underlying DOM node. See the [direct
|
||||
manipulation](../guides/direct-manipulation.md) guide for cases where
|
||||
`setNativeProps` should be used.
|
||||
@@ -0,0 +1,77 @@
|
||||
# NetInfo
|
||||
|
||||
`NetInfo` asynchronously determines the online/offline status of the
|
||||
application.
|
||||
|
||||
Connection types:
|
||||
|
||||
* `bluetooth` - The user agent is using a Bluetooth connection.
|
||||
* `cellular` - The user agent is using a cellular connection (e.g., EDGE, HSPA, LTE, etc.).
|
||||
* `ethernet` - The user agent is using an Ethernet connection.
|
||||
* `mixed` - The user agent is using multiple connection types.
|
||||
* `none` - The user agent will not contact the network (offline).
|
||||
* `other` - The user agent is using a connection type that is not one of enumerated connection types.
|
||||
* `unknown` - The user agent has established a network connection, but is unable to determine what is the underlying connection technology.
|
||||
* `wifi` - The user agent is using a Wi-Fi connection.
|
||||
* `wimax` - The user agent is using a WiMAX connection.
|
||||
|
||||
## Methods
|
||||
|
||||
Note that support for retrieving the connection type depends upon browswer
|
||||
support (and is limited to mobile browsers). It will default to `unknown` when
|
||||
support is missing.
|
||||
|
||||
static **addEventListener**(eventName: ChangeEventName, handler: Function)
|
||||
|
||||
static **fetch**(): Promise
|
||||
|
||||
static **removeEventListener**(eventName: ChangeEventName, handler: Function)
|
||||
|
||||
## Properties
|
||||
|
||||
**isConnected**
|
||||
|
||||
Available on all user agents. Asynchronously fetch a boolean to determine
|
||||
internet connectivity.
|
||||
|
||||
**isConnected.addEventListener**(eventName: ChangeEventName, handler: Function)
|
||||
|
||||
**isConnected.fetch**(): Promise
|
||||
|
||||
**isConnected.removeEventListener**(eventName: ChangeEventName, handler: Function)
|
||||
|
||||
## Examples
|
||||
|
||||
Fetching the connection type:
|
||||
|
||||
```
|
||||
NetInfo.fetch().then((connectionType) => {
|
||||
console.log('Connection type:', connectionType);
|
||||
});
|
||||
```
|
||||
|
||||
Subscribing to changes in the connection type:
|
||||
|
||||
```js
|
||||
const handleConnectivityTypeChange = (connectionType) => {
|
||||
console.log('Current connection type:', connectionType);
|
||||
}
|
||||
NetInfo.addEventListener('change', handleConnectivityTypeChange);
|
||||
```
|
||||
|
||||
Fetching the connection status:
|
||||
|
||||
```js
|
||||
NetInfo.isConnected.fetch().then((isConnected) => {
|
||||
console.log('Connection status:', (isConnected ? 'online' : 'offline'));
|
||||
});
|
||||
```
|
||||
|
||||
Subscribing to changes in the connection status:
|
||||
|
||||
```js
|
||||
const handleConnectivityStatusChange = (isConnected) => {
|
||||
console.log('Current connection status:', (isConnected ? 'online' : 'offline'));
|
||||
}
|
||||
NetInfo.isConnected.addEventListener('change', handleConnectivityStatusChange);
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
# PixelRatio
|
||||
|
||||
`PixelRatio` gives access to the device pixel density.
|
||||
|
||||
## Methods
|
||||
|
||||
static **get**()
|
||||
|
||||
Returns the device pixel density. Some examples:
|
||||
|
||||
* PixelRatio.get() === 1
|
||||
* mdpi Android devices (160 dpi)
|
||||
* PixelRatio.get() === 1.5
|
||||
* hdpi Android devices (240 dpi)
|
||||
* PixelRatio.get() === 2
|
||||
* iPhone 4, 4S
|
||||
* iPhone 5, 5c, 5s
|
||||
* iPhone 6
|
||||
* xhdpi Android devices (320 dpi)
|
||||
* PixelRatio.get() === 3
|
||||
* iPhone 6 plus
|
||||
* xxhdpi Android devices (480 dpi)
|
||||
* PixelRatio.get() === 3.5
|
||||
* Nexus 6
|
||||
|
||||
static **getPixelSizeForLayoutSize**(layoutSize: number)
|
||||
|
||||
Converts a layout size (dp) to pixel size (px). Guaranteed to return an integer
|
||||
number.
|
||||
|
||||
static **roundToNearestPixel**(layoutSize: number)
|
||||
|
||||
Rounds a layout size (dp) to the nearest layout size that corresponds to an
|
||||
integer number of pixels. For example, on a device with a PixelRatio of 3,
|
||||
`PixelRatio.roundToNearestPixel(8.4)` = `8.33`, which corresponds to exactly
|
||||
`(8.33 * 3)` = `25` pixels.
|
||||
|
||||
## Examples
|
||||
|
||||
Fetching a correctly sized image. You should get a higher resolution image if
|
||||
you are on a high pixel density device. A good rule of thumb is to multiply the
|
||||
size of the image you display by the pixel ratio.
|
||||
|
||||
```js
|
||||
const image = getImage({
|
||||
width: PixelRatio.getPixelSizeForLayoutSize(200),
|
||||
height: PixelRatio.getPixelSizeForLayoutSize(100),
|
||||
});
|
||||
|
||||
<Image source={image} style={{ width: 200, height: 100 }} />
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
# Platform
|
||||
|
||||
Detect what is the platform in which the app is running. This piece of
|
||||
functionality can be useful when only small parts of a component are platform
|
||||
specific.
|
||||
|
||||
## Properties
|
||||
|
||||
**OS**: string
|
||||
|
||||
`Platform.OS` will be `web` when running in a Web browser.
|
||||
|
||||
**userAgent**: string
|
||||
|
||||
On Web, the `Platform` module can be also be used to detect the browser
|
||||
`userAgent`.
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
const styles = StyleSheet.create({
|
||||
height: (Platform.OS === 'web') ? 200 : 100,
|
||||
});
|
||||
|
||||
if (Platform.userAgent.includes('Android')) {
|
||||
console.log('Running on Android!');
|
||||
}
|
||||
```
|
||||
@@ -5,7 +5,11 @@ CSS without requiring a compile-time step. Some styles cannot be resolved
|
||||
outside of the render loop and are applied as inline styles. Read more about to
|
||||
[how style your application](docs/guides/style).
|
||||
|
||||
Create a new StyleSheet:
|
||||
## Methods
|
||||
|
||||
**create**(obj: {[key: string]: any})
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
const styles = StyleSheet.create({
|
||||
@@ -36,7 +40,3 @@ Use styles:
|
||||
/>
|
||||
</View>
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
**create**(obj: {[key: string]: any})
|
||||
|
||||
+8
-3
@@ -12,12 +12,13 @@
|
||||
"examples": "webpack-dev-server --config config/webpack.config.example.js --inline --hot --colors --quiet",
|
||||
"lint": "eslint config examples src",
|
||||
"prepublish": "npm run build && npm run build:umd",
|
||||
"test": "npm run lint && npm run test:unit",
|
||||
"test:unit": "karma start config/karma.config.js",
|
||||
"test": "karma start config/karma.config.js",
|
||||
"test:watch": "npm run test:unit -- --no-single-run"
|
||||
},
|
||||
"dependencies": {
|
||||
"exenv": "^1.2.0",
|
||||
"inline-style-prefixer": "^0.5.3",
|
||||
"invariant": "^2.2.0",
|
||||
"lodash.debounce": "^3.1.1",
|
||||
"react-tappable": "^0.7.1",
|
||||
"react-textarea-autosize": "^3.1.0"
|
||||
@@ -53,8 +54,12 @@
|
||||
"webpack": "^1.12.9",
|
||||
"webpack-dev-server": "^1.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^0.14.3",
|
||||
"react-dom": "^0.14.3"
|
||||
},
|
||||
"author": "Nicolas Gallagher",
|
||||
"license": "MIT",
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/necolas/react-native-web.git"
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import Portal from '../../components/Portal'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import View from '../../components/View'
|
||||
|
||||
export default class ReactNativeApp extends Component {
|
||||
static propTypes = {
|
||||
initialProps: PropTypes.object,
|
||||
rootComponent: PropTypes.any.isRequired,
|
||||
rootTag: PropTypes.any
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this._handleModalVisibilityChange = this._handleModalVisibilityChange.bind(this)
|
||||
}
|
||||
|
||||
_handleModalVisibilityChange(modalVisible) {
|
||||
ReactDOM.findDOMNode(this._root).setAttribute('aria-hidden', `${modalVisible}`)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { initialProps, rootComponent: RootComponent, rootTag } = this.props
|
||||
|
||||
return (
|
||||
<View style={styles.appContainer}>
|
||||
<RootComponent {...initialProps} ref={(c) => { this._root = c }} rootTag={rootTag} />
|
||||
<Portal onModalVisibilityChanged={this._handleModalVisibilityChange} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
/**
|
||||
* Ensure that the application covers the whole screen. This prevents the
|
||||
* Portal content from being clipped.
|
||||
*/
|
||||
appContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import { Component } from 'react'
|
||||
import invariant from 'invariant'
|
||||
import ReactDOM from 'react-dom'
|
||||
import renderApplication, { prerenderApplication } from './renderApplication'
|
||||
|
||||
const runnables = {}
|
||||
|
||||
type ComponentProvider = () => Component<any, any, any>
|
||||
|
||||
type AppConfig = {
|
||||
appKey: string;
|
||||
component?: ComponentProvider;
|
||||
run?: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* `AppRegistry` is the JS entry point to running all React Native apps.
|
||||
*/
|
||||
export default class AppRegistry {
|
||||
static getAppKeys(): Array<string> {
|
||||
return Object.keys(runnables)
|
||||
}
|
||||
|
||||
static prerenderApplication(appKey: string, appParameters?: Object): string {
|
||||
invariant(
|
||||
runnables[appKey] && runnables[appKey].prerender,
|
||||
`Application ${appKey} has not been registered. ` +
|
||||
`This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.`
|
||||
)
|
||||
|
||||
return runnables[appKey].prerender(appParameters)
|
||||
}
|
||||
|
||||
static registerComponent(appKey: string, getComponentFunc: ComponentProvider): string {
|
||||
runnables[appKey] = {
|
||||
run: ({ initialProps, rootTag }) => renderApplication(getComponentFunc(), initialProps, rootTag),
|
||||
prerender: ({ initialProps } = {}) => prerenderApplication(getComponentFunc(), initialProps)
|
||||
}
|
||||
return appKey
|
||||
}
|
||||
|
||||
static registerConfig(config: Array<AppConfig>) {
|
||||
config.forEach(({ appKey, component, run }) => {
|
||||
if (run) {
|
||||
AppRegistry.registerRunnable(appKey, run)
|
||||
} else {
|
||||
invariant(component, 'No component provider passed in')
|
||||
AppRegistry.registerComponent(appKey, component)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: fix style sheet creation when using this method
|
||||
static registerRunnable(appKey: string, run: Function): string {
|
||||
runnables[appKey] = { run }
|
||||
return appKey
|
||||
}
|
||||
|
||||
static runApplication(appKey: string, appParameters?: Object): void {
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const params = { ...appParameters }
|
||||
params.rootTag = `#${params.rootTag.id}`
|
||||
|
||||
console.log(
|
||||
`Running application "${appKey}" with appParams: ${JSON.stringify(params)}. ` +
|
||||
`development-level warnings are ${isDevelopment ? 'ON' : 'OFF'}, ` +
|
||||
`performance optimizations are ${isDevelopment ? 'OFF' : 'ON'}`
|
||||
)
|
||||
|
||||
invariant(
|
||||
runnables[appKey] && runnables[appKey].run,
|
||||
`Application "${appKey}" has not been registered. ` +
|
||||
`This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.`
|
||||
)
|
||||
|
||||
runnables[appKey].run(appParameters)
|
||||
}
|
||||
|
||||
static unmountApplicationComponentAtRootTag(rootTag) {
|
||||
ReactDOM.unmountComponentAtNode(rootTag)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'invariant'
|
||||
import React, { Component } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
import ReactNativeApp from './ReactNativeApp'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
|
||||
const STYLESHEET_ID = 'react-stylesheet'
|
||||
const renderStyleSheetToString = () => `<style id="${STYLESHEET_ID}">${StyleSheet._renderToString()}</style>`
|
||||
|
||||
export default function renderApplication(RootComponent: Component, initialProps: Object, rootTag: any) {
|
||||
invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag)
|
||||
|
||||
// insert style sheet if needed
|
||||
const styleElement = document.getElementById(STYLESHEET_ID)
|
||||
if (!styleElement) { rootTag.insertAdjacentHTML('beforebegin', renderStyleSheetToString()) }
|
||||
|
||||
const component = (
|
||||
<ReactNativeApp
|
||||
initialProps={initialProps}
|
||||
rootComponent={RootComponent}
|
||||
rootTag={rootTag}
|
||||
/>
|
||||
)
|
||||
ReactDOM.render(component, rootTag)
|
||||
}
|
||||
|
||||
export function prerenderApplication(RootComponent: Component, initialProps: Object): string {
|
||||
const component = (
|
||||
<ReactNativeApp
|
||||
initialProps={initialProps}
|
||||
rootComponent={RootComponent}
|
||||
/>
|
||||
)
|
||||
const html = ReactDOMServer.renderToString(component)
|
||||
const style = renderStyleSheetToString()
|
||||
return { html, style }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
suite('apis/AppState', () => {
|
||||
test.skip('NO TEST COVERAGE', () => {})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import invariant from 'invariant'
|
||||
|
||||
const listeners = {}
|
||||
const eventTypes = [ 'change' ]
|
||||
|
||||
export default class AppState {
|
||||
static get currentState() {
|
||||
switch (document.visibilityState) {
|
||||
case 'hidden':
|
||||
case 'prerender':
|
||||
case 'unloaded':
|
||||
return 'background'
|
||||
default:
|
||||
return 'active'
|
||||
}
|
||||
}
|
||||
|
||||
static addEventListener(type: string, handler: Function) {
|
||||
listeners[handler] = () => handler(AppState.currentState)
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
document.addEventListener('visibilitychange', listeners[handler], false)
|
||||
}
|
||||
|
||||
static removeEventListener(type: string, handler: Function) {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to remove listener for unknown event: "%s"', type)
|
||||
document.removeEventListener('visibilitychange', listeners[handler], false)
|
||||
delete listeners[handler]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
suite('apis/AsyncStorage', () => {
|
||||
test.skip('NO TEST COVERAGE', () => {})
|
||||
})
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
const mergeLocalStorageItem = (key, value) => {
|
||||
const oldValue = window.localStorage.getItem(key)
|
||||
const oldObject = JSON.parse(oldValue)
|
||||
const newObject = JSON.parse(value)
|
||||
const nextValue = JSON.stringify({ ...oldObject, ...newObject })
|
||||
window.localStorage.setItem(key, nextValue)
|
||||
}
|
||||
|
||||
export default class AsyncStorage {
|
||||
/**
|
||||
* Erases *all* AsyncStorage for the domain.
|
||||
*/
|
||||
static clear() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
window.localStorage.clear()
|
||||
resolve(null)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets *all* keys known to the app, for all callers, libraries, etc.
|
||||
*/
|
||||
static getAllKeys() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const numberOfKeys = window.localStorage.length
|
||||
const keys = []
|
||||
for (let i = 0; i < numberOfKeys; i += 1) {
|
||||
const key = window.localStorage.key(i)
|
||||
keys.push(key)
|
||||
}
|
||||
resolve(keys)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches `key` value.
|
||||
*/
|
||||
static getItem(key: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const value = window.localStorage.getItem(key)
|
||||
resolve(value)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges existing value with input value, assuming they are stringified JSON.
|
||||
*/
|
||||
static mergeItem(key: string, value: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
mergeLocalStorageItem(key, value)
|
||||
resolve(null)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* multiGet resolves to an array of key-value pair arrays that matches the
|
||||
* input format of multiSet.
|
||||
*
|
||||
* multiGet(['k1', 'k2']) -> [['k1', 'val1'], ['k2', 'val2']]
|
||||
*/
|
||||
static multiGet(keys: Array<string>) {
|
||||
const promises = keys.map((key) => AsyncStorage.getItem(key))
|
||||
|
||||
return Promise.all(promises).then(
|
||||
(result) => Promise.resolve(result.map((value, i) => [ keys[i], value ])),
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of key-value array pairs and merges them with existing
|
||||
* values, assuming they are stringified JSON.
|
||||
*
|
||||
* multiMerge([['k1', 'val1'], ['k2', 'val2']])
|
||||
*/
|
||||
static multiMerge(keyValuePairs: Array<Array<string>>) {
|
||||
const promises = keyValuePairs.map((item) => AsyncStorage.mergeItem(item[0], item[1]))
|
||||
|
||||
return Promise.all(promises).then(
|
||||
() => Promise.resolve(null),
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the keys in the `keys` array.
|
||||
*/
|
||||
static multiRemove(keys: Array<string>) {
|
||||
const promises = keys.map((key) => AsyncStorage.removeItem(key))
|
||||
|
||||
return Promise.all(promises).then(
|
||||
() => Promise.resolve(null),
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of key-value array pairs.
|
||||
* multiSet([['k1', 'val1'], ['k2', 'val2']])
|
||||
*/
|
||||
static multiSet(keyValuePairs: Array<Array<string>>) {
|
||||
const promises = keyValuePairs.map((item) => AsyncStorage.setItem(item[0], item[1]))
|
||||
|
||||
return Promise.all(promises).then(
|
||||
() => Promise.resolve(null),
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a `key`
|
||||
*/
|
||||
static removeItem(key: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
window.localStorage.removeItem(key)
|
||||
resolve(null)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets `value` for `key`.
|
||||
*/
|
||||
static setItem(key: string, value: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
window.localStorage.setItem(key, value)
|
||||
resolve(null)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
suite('apis/Dimensions', () => {
|
||||
test.skip('NO TEST COVERAGE', () => {})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'invariant'
|
||||
|
||||
const dimensions = {
|
||||
screen: {
|
||||
fontScale: 1,
|
||||
get height() { return window.screen.height },
|
||||
scale: window.devicePixelRatio || 1,
|
||||
get width() { return window.screen.width }
|
||||
},
|
||||
window: {
|
||||
fontScale: 1,
|
||||
get height() { return document.documentElement.clientHeight },
|
||||
scale: window.devicePixelRatio || 1,
|
||||
get width() { return document.documentElement.clientWidth }
|
||||
}
|
||||
}
|
||||
|
||||
export default class Dimensions {
|
||||
static get(dimension: string): Object {
|
||||
invariant(dimensions[dimension], 'No dimension set for key ' + dimension)
|
||||
return dimensions[dimension]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
suite('apis/NetInfo', () => {
|
||||
test.skip('NO TEST COVERAGE', () => {})
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'invariant'
|
||||
|
||||
const connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection
|
||||
const eventTypes = [ 'change' ]
|
||||
|
||||
/**
|
||||
* Navigator online: https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine
|
||||
* Network Connection API: https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation
|
||||
*/
|
||||
const NetInfo = {
|
||||
addEventListener(type: string, handler: Function): { remove: () => void } {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
if (!connection) {
|
||||
console.error('Network Connection API is not supported. Not listening for connection type changes.')
|
||||
return {
|
||||
remove: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
connection.addEventListener(type, handler)
|
||||
return {
|
||||
remove: () => NetInfo.removeEventListener(type, handler)
|
||||
}
|
||||
},
|
||||
|
||||
removeEventListener(type: string, handler: Function): void {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
if (!connection) { return }
|
||||
connection.removeEventListener(type, handler)
|
||||
},
|
||||
|
||||
fetch(): Promise {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
resolve(connection.type)
|
||||
} catch (err) {
|
||||
resolve('unknown')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
isConnected: {
|
||||
addEventListener(type: string, handler: Function): { remove: () => void } {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
window.addEventListener('online', handler.bind(true), false)
|
||||
window.addEventListener('offline', handler.bind(false), false)
|
||||
|
||||
return {
|
||||
remove: () => NetInfo.isConnected.removeEventListener(type, handler)
|
||||
}
|
||||
},
|
||||
|
||||
removeEventListener(type: string, handler: Function): void {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
window.removeEventListener('online', handler.bind(true), false)
|
||||
window.removeEventListener('offline', handler.bind(false), false)
|
||||
},
|
||||
|
||||
fetch(): Promise {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
resolve(window.navigator.onLine)
|
||||
} catch (err) {
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
suite('apis/PixelRatio', () => {
|
||||
test.skip('NO TEST COVERAGE', () => {})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import Dimensions from '../Dimensions'
|
||||
|
||||
/**
|
||||
* PixelRatio gives access to the device pixel density.
|
||||
*/
|
||||
export default class PixelRatio {
|
||||
/**
|
||||
* Returns the device pixel density.
|
||||
*/
|
||||
static get(): number {
|
||||
return Dimensions.get('window').scale
|
||||
}
|
||||
|
||||
/**
|
||||
* No equivalent for Web
|
||||
*/
|
||||
static getFontScale(): number {
|
||||
return Dimensions.get('window').fontScale || PixelRatio.get()
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a layout size (dp) to pixel size (px).
|
||||
* Guaranteed to return an integer number.
|
||||
*/
|
||||
static getPixelSizeForLayoutSize(layoutSize: number): number {
|
||||
return Math.round(layoutSize * PixelRatio.get())
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds a layout size (dp) to the nearest layout size that corresponds to
|
||||
* an integer number of pixels. For example, on a device with a PixelRatio
|
||||
* of 3, `PixelRatio.roundToNearestPixel(8.4) = 8.33`, which corresponds to
|
||||
* exactly (8.33 * 3) = 25 pixels.
|
||||
*/
|
||||
static roundToNearestPixel(layoutSize: number): number {
|
||||
const ratio = PixelRatio.get()
|
||||
return Math.round(layoutSize * ratio) / ratio
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { canUseDOM } from 'exenv'
|
||||
|
||||
const Platform = {
|
||||
OS: 'web',
|
||||
userAgent: canUseDOM ? window.navigator.userAgent : ''
|
||||
}
|
||||
|
||||
export default Platform
|
||||
@@ -0,0 +1,100 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import UIManager from '..'
|
||||
|
||||
const createNode = (style = {}) => {
|
||||
const root = document.createElement('div')
|
||||
Object.keys(style).forEach((prop) => {
|
||||
root.style[prop] = style[prop]
|
||||
})
|
||||
return root
|
||||
}
|
||||
|
||||
let defaultBodyMargin
|
||||
|
||||
suite('apis/UIManager', () => {
|
||||
setup(() => {
|
||||
// remove default body margin so we can predict the measured offsets
|
||||
defaultBodyMargin = document.body.style.margin
|
||||
document.body.style.margin = 0
|
||||
})
|
||||
|
||||
teardown(() => {
|
||||
document.body.style.margin = defaultBodyMargin
|
||||
})
|
||||
|
||||
suite('measure', () => {
|
||||
test('provides correct layout to callback', () => {
|
||||
const node = createNode({ height: '5000px', left: '100px', position: 'relative', top: '100px', width: '5000px' })
|
||||
document.body.appendChild(node)
|
||||
|
||||
UIManager.measure(node, (x, y, width, height, pageX, pageY) => {
|
||||
assert.equal(x, 100)
|
||||
assert.equal(y, 100)
|
||||
assert.equal(width, 5000)
|
||||
assert.equal(height, 5000)
|
||||
assert.equal(pageX, 100)
|
||||
assert.equal(pageY, 100)
|
||||
})
|
||||
|
||||
// test values account for scroll position
|
||||
window.scrollTo(200, 200)
|
||||
UIManager.measure(node, (x, y, width, height, pageX, pageY) => {
|
||||
assert.equal(x, 100)
|
||||
assert.equal(y, 100)
|
||||
assert.equal(width, 5000)
|
||||
assert.equal(height, 5000)
|
||||
assert.equal(pageX, -100)
|
||||
assert.equal(pageY, -100)
|
||||
})
|
||||
|
||||
document.body.removeChild(node)
|
||||
})
|
||||
})
|
||||
|
||||
suite('measureLayout', () => {
|
||||
test('provides correct layout to onSuccess callback', () => {
|
||||
const node = createNode({ height: '10px', width: '10px' })
|
||||
const middle = createNode({ padding: '20px' })
|
||||
const context = createNode({ padding: '20px' })
|
||||
middle.appendChild(node)
|
||||
context.appendChild(middle)
|
||||
document.body.appendChild(context)
|
||||
|
||||
UIManager.measureLayout(node, context, () => {}, (x, y, width, height) => {
|
||||
assert.equal(x, 40)
|
||||
assert.equal(y, 40)
|
||||
assert.equal(width, 10)
|
||||
assert.equal(height, 10)
|
||||
})
|
||||
|
||||
document.body.removeChild(context)
|
||||
})
|
||||
})
|
||||
|
||||
suite('updateView', () => {
|
||||
test('adds new className to existing className', () => {
|
||||
const node = createNode()
|
||||
node.className = 'existing'
|
||||
const props = { className: 'extra' }
|
||||
UIManager.updateView(node, props)
|
||||
assert.equal(node.getAttribute('class'), 'existing extra')
|
||||
})
|
||||
|
||||
test('adds new style to existing style', () => {
|
||||
const node = createNode({ color: 'red' })
|
||||
const props = { style: { opacity: 0 } }
|
||||
UIManager.updateView(node, props)
|
||||
assert.equal(node.getAttribute('style'), 'color: red; opacity: 0;')
|
||||
})
|
||||
|
||||
test('sets attribute values', () => {
|
||||
const node = createNode()
|
||||
const props = { 'aria-level': '4', 'data-of-type': 'string' }
|
||||
UIManager.updateView(node, props)
|
||||
assert.equal(node.getAttribute('aria-level'), '4')
|
||||
assert.equal(node.getAttribute('data-of-type'), 'string')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations'
|
||||
|
||||
const measureAll = (node, callback, relativeToNativeNode) => {
|
||||
const { height, left, top, width } = node.getBoundingClientRect()
|
||||
const relativeNode = relativeToNativeNode || node.parentNode
|
||||
const relativeRect = relativeNode.getBoundingClientRect()
|
||||
const x = left - relativeRect.left
|
||||
const y = top - relativeRect.top
|
||||
callback(x, y, width, height, left, top)
|
||||
}
|
||||
|
||||
const UIManager = {
|
||||
measure(node, callback) {
|
||||
measureAll(node, callback)
|
||||
},
|
||||
|
||||
measureLayout(node, relativeToNativeNode, onFail, onSuccess) {
|
||||
measureAll(node, (x, y, width, height) => onSuccess(x, y, width, height), relativeToNativeNode)
|
||||
},
|
||||
|
||||
updateView(node, props) {
|
||||
for (const prop in props) {
|
||||
const value = props[prop]
|
||||
if (prop === 'style') {
|
||||
CSSPropertyOperations.setValueForStyles(node, value)
|
||||
} else if (prop === 'className') {
|
||||
node.classList.add(value)
|
||||
} else {
|
||||
node.setAttribute(prop, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UIManager
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import { Component } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import UIManager from '../../apis/UIManager'
|
||||
|
||||
type MeasureOnSuccessCallback = (
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
pageX: number,
|
||||
pageY: number
|
||||
) => void
|
||||
|
||||
type MeasureLayoutOnSuccessCallback = (
|
||||
left: number,
|
||||
top: number,
|
||||
width: number,
|
||||
height: number
|
||||
) => void
|
||||
|
||||
const NativeMethodsMixin = {
|
||||
/**
|
||||
* Removes focus from an input or view. This is the opposite of `focus()`.
|
||||
*/
|
||||
blur() {
|
||||
ReactDOM.findDOMNode(this).blur()
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests focus for the given input or view.
|
||||
* The exact behavior triggered will depend the type of view.
|
||||
*/
|
||||
focus() {
|
||||
ReactDOM.findDOMNode(this).focus()
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the position and dimensions of the view
|
||||
*/
|
||||
measure(callback: MeasureOnSuccessCallback) {
|
||||
UIManager.measure(
|
||||
ReactDOM.findDOMNode(this),
|
||||
mountSafeCallback(this, callback)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Measures the view relative to another view (usually an ancestor)
|
||||
*/
|
||||
measureLayout(
|
||||
relativeToNativeNode: number,
|
||||
onSuccess: MeasureLayoutOnSuccessCallback,
|
||||
onFail: () => void /* currently unused */
|
||||
) {
|
||||
UIManager.measureLayout(
|
||||
ReactDOM.findDOMNode(this),
|
||||
relativeToNativeNode,
|
||||
mountSafeCallback(this, onFail),
|
||||
mountSafeCallback(this, onSuccess)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* This function sends props straight to the underlying DOM node.
|
||||
*/
|
||||
setNativeProps(nativeProps: Object) {
|
||||
UIManager.updateView(
|
||||
ReactDOM.findDOMNode(this),
|
||||
nativeProps
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In the future, we should cleanup callbacks by cancelling them instead of
|
||||
* using this.
|
||||
*/
|
||||
const mountSafeCallback = (context: Component, callback: ?Function) => () => {
|
||||
if (!callback || (context.isMounted && !context.isMounted())) {
|
||||
return
|
||||
}
|
||||
return callback.apply(context, arguments)
|
||||
}
|
||||
|
||||
export default NativeMethodsMixin
|
||||
|
||||
export const nativeMethodsDecorator = (Component) => {
|
||||
Object.keys(NativeMethodsMixin).forEach((method) => {
|
||||
Component.prototype[method] = NativeMethodsMixin[method]
|
||||
})
|
||||
return Component
|
||||
}
|
||||
Reference in New Issue
Block a user