- AppRegistry
- AppState
- AsyncStorage
- Dimensions
- NativeMethods
- NetInfo
- PixelRatio
- Platform
- UIManager
This commit is contained in:
Nicolas Gallagher
2016-02-16 23:52:57 -08:00
parent 60409bea18
commit 1c7fb4cb45
29 changed files with 1210 additions and 8 deletions
+60
View File
@@ -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')
})
```
+61
View File
@@ -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>
)
}
}
```
+71
View File
@@ -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.
+13
View File
@@ -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')`
+42
View File
@@ -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.
+77
View File
@@ -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);
```
+51
View File
@@ -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 }} />
```
+28
View File
@@ -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 -5
View File
@@ -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
View File
@@ -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"
+47
View File
@@ -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
}
})
+90
View File
@@ -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)
}
}
+46
View File
@@ -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', () => {})
})
+29
View File
@@ -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', () => {})
})
+159
View File
@@ -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', () => {})
})
+31
View File
@@ -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]
}
}
+5
View File
@@ -0,0 +1,5 @@
/* eslint-env mocha */
suite('apis/NetInfo', () => {
test.skip('NO TEST COVERAGE', () => {})
})
+77
View File
@@ -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', () => {})
})
+47
View File
@@ -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
}
}
+8
View File
@@ -0,0 +1,8 @@
import { canUseDOM } from 'exenv'
const Platform = {
OS: 'web',
userAgent: canUseDOM ? window.navigator.userAgent : ''
}
export default Platform
+100
View File
@@ -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')
})
})
})
+35
View File
@@ -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
+100
View File
@@ -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
}