diff --git a/packages/react-native-web/src/exports/Image/ImageSourcePropType.js b/packages/react-native-web/src/exports/Image/ImageSourcePropType.js
new file mode 100644
index 00000000..dfe9528c
--- /dev/null
+++ b/packages/react-native-web/src/exports/Image/ImageSourcePropType.js
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ * @format
+ */
+'use strict';
+
+import PropTypes from 'prop-types';
+
+const ImageURISourcePropType = PropTypes.shape({
+ /**
+ * `uri` is a string representing the resource identifier for the image, which
+ * could be an http address, a local file path, or the name of a static image
+ * resource (which should be wrapped in the `require('./path/to/image.png')`
+ * function).
+ */
+ uri: PropTypes.string,
+ /**
+ * `bundle` is the iOS asset bundle which the image is included in. This
+ * will default to [NSBundle mainBundle] if not set.
+ * @platform ios
+ */
+ bundle: PropTypes.string,
+ /**
+ * `method` is the HTTP Method to use. Defaults to GET if not specified.
+ */
+ method: PropTypes.string,
+ /**
+ * `headers` is an object representing the HTTP headers to send along with the
+ * request for a remote image.
+ */
+ headers: PropTypes.objectOf(PropTypes.string),
+ /**
+ * `body` is the HTTP body to send with the request. This must be a valid
+ * UTF-8 string, and will be sent exactly as specified, with no
+ * additional encoding (e.g. URL-escaping or base64) applied.
+ */
+ body: PropTypes.string,
+ /**
+ * `cache` determines how the requests handles potentially cached
+ * responses.
+ *
+ * - `default`: Use the native platforms default strategy. `useProtocolCachePolicy` on iOS.
+ *
+ * - `reload`: The data for the URL will be loaded from the originating source.
+ * No existing cache data should be used to satisfy a URL load request.
+ *
+ * - `force-cache`: The existing cached data will be used to satisfy the request,
+ * regardless of its age or expiration date. If there is no existing data in the cache
+ * corresponding the request, the data is loaded from the originating source.
+ *
+ * - `only-if-cached`: The existing cache data will be used to satisfy a request, regardless of
+ * its age or expiration date. If there is no existing data in the cache corresponding
+ * to a URL load request, no attempt is made to load the data from the originating source,
+ * and the load is considered to have failed.
+ *
+ * @platform ios
+ */
+ cache: PropTypes.oneOf(['default', 'reload', 'force-cache', 'only-if-cached']),
+ /**
+ * `width` and `height` can be specified if known at build time, in which case
+ * these will be used to set the default `` component dimensions.
+ */
+ width: PropTypes.number,
+ height: PropTypes.number,
+ /**
+ * `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if
+ * unspecified, meaning that one image pixel equates to one display point / DIP.
+ */
+ scale: PropTypes.number
+});
+
+const ImageSourcePropType = PropTypes.oneOfType([
+ ImageURISourcePropType,
+ // Opaque type returned by require('./image.jpg')
+ PropTypes.number,
+ PropTypes.string,
+ // Multiple sources
+ PropTypes.arrayOf(ImageURISourcePropType)
+]);
+
+export default ImageSourcePropType;
diff --git a/packages/react-native-web/src/exports/Image/__tests__/index-test.js b/packages/react-native-web/src/exports/Image/__tests__/index-test.js
index 6eecde55..9c83d94f 100644
--- a/packages/react-native-web/src/exports/Image/__tests__/index-test.js
+++ b/packages/react-native-web/src/exports/Image/__tests__/index-test.js
@@ -147,6 +147,13 @@ describe('components/Image', () => {
});
describe('prop "source"', () => {
+ test('does not throw', () => {
+ const sources = [null, '', {}, { uri: '' }, { uri: 'https://google.com' }];
+ sources.forEach(source => {
+ expect(() => shallow()).not.toThrow();
+ });
+ });
+
test('is not set immediately if the image has not already been loaded', () => {
const uri = 'https://google.com/favicon.ico';
const source = { uri };
diff --git a/packages/react-native-web/src/exports/Image/index.js b/packages/react-native-web/src/exports/Image/index.js
index 423488ea..ebc5ac74 100644
--- a/packages/react-native-web/src/exports/Image/index.js
+++ b/packages/react-native-web/src/exports/Image/index.js
@@ -13,6 +13,7 @@ import createElement from '../createElement';
import { getAssetByID } from '../../modules/AssetRegistry';
import ImageLoader from '../../modules/ImageLoader';
import ImageResizeMode from './ImageResizeMode';
+import ImageSourcePropType from './ImageSourcePropType';
import ImageStylePropTypes from './ImageStylePropTypes';
import ImageUriCache from './ImageUriCache';
import requestIdleCallback, { cancelIdleCallback } from '../../modules/requestIdleCallback';
@@ -20,7 +21,7 @@ import StyleSheet from '../StyleSheet';
import StyleSheetPropType from '../../modules/StyleSheetPropType';
import View from '../View';
import ViewPropTypes from '../ViewPropTypes';
-import { bool, func, number, oneOf, oneOfType, shape, string } from 'prop-types';
+import { bool, func, number, oneOf, shape } from 'prop-types';
import React, { Component } from 'react';
const emptyObject = {};
@@ -31,16 +32,6 @@ const STATUS_LOADING = 'LOADING';
const STATUS_PENDING = 'PENDING';
const STATUS_IDLE = 'IDLE';
-const ImageSourcePropType = oneOfType([
- number,
- shape({
- height: number,
- uri: string.isRequired,
- width: number
- }),
- string
-]);
-
const getImageState = (uri, shouldDisplaySource) => {
return shouldDisplaySource ? STATUS_LOADED : uri ? STATUS_PENDING : STATUS_IDLE;
};
@@ -56,26 +47,28 @@ const resolveAssetDimensions = source => {
};
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
-const resolveAssetSource = source => {
- let uri;
+const resolveAssetUri = source => {
+ let uri = '';
if (typeof source === 'number') {
// get the URI from the packager
const asset = getAssetByID(source);
const scale = asset.scales[0];
const scaleSuffix = scale !== 1 ? `@${scale}x` : '';
uri = asset ? `${asset.httpServerLocation}/${asset.name}${scaleSuffix}.${asset.type}` : '';
- } else if (source && source.uri) {
+ } else if (typeof source === 'string') {
+ uri = source;
+ } else if (source && typeof source.uri === 'string') {
uri = source.uri;
- } else {
- uri = source || '';
}
- const match = uri.match(svgDataUriPattern);
- // inline SVG markup may contain characters (e.g., #, ") that need to be escaped
- if (match) {
- const [, prefix, svg] = match;
- const encodedSvg = encodeURIComponent(svg);
- return `${prefix}${encodedSvg}`;
+ if (uri) {
+ const match = uri.match(svgDataUriPattern);
+ // inline SVG markup may contain characters (e.g., #, ") that need to be escaped
+ if (match) {
+ const [, prefix, svg] = match;
+ const encodedSvg = encodeURIComponent(svg);
+ return `${prefix}${encodedSvg}`;
+ }
}
return uri;
@@ -137,7 +130,7 @@ class Image extends Component<*, State> {
constructor(props, context) {
super(props, context);
// If an image has been loaded before, render it immediately
- const uri = resolveAssetSource(props.source);
+ const uri = resolveAssetUri(props.source);
const shouldDisplaySource = ImageUriCache.has(uri);
this.state = { shouldDisplaySource };
this._imageState = getImageState(uri, shouldDisplaySource);
@@ -161,8 +154,8 @@ class Image extends Component<*, State> {
}
componentWillReceiveProps(nextProps) {
- const uri = resolveAssetSource(this.props.source);
- const nextUri = resolveAssetSource(nextProps.source);
+ const uri = resolveAssetUri(this.props.source);
+ const nextUri = resolveAssetUri(nextProps.source);
if (uri !== nextUri) {
ImageUriCache.remove(uri);
const isPreviouslyLoaded = ImageUriCache.has(nextUri);
@@ -172,7 +165,8 @@ class Image extends Component<*, State> {
}
componentWillUnmount() {
- ImageUriCache.remove(resolveAssetSource(this.props.source));
+ const uri = resolveAssetUri(this.props.source);
+ ImageUriCache.remove(uri);
this._destroyImageLoader();
this._isMounted = false;
}
@@ -200,7 +194,7 @@ class Image extends Component<*, State> {
...other
} = this.props;
- const displayImage = resolveAssetSource(shouldDisplaySource ? source : defaultSource);
+ const displayImage = resolveAssetUri(shouldDisplaySource ? source : defaultSource);
const imageSizeStyle = resolveAssetDimensions(shouldDisplaySource ? source : defaultSource);
const backgroundImage = displayImage ? `url("${displayImage}")` : null;
const originalStyle = StyleSheet.flatten(this.props.style);
@@ -260,7 +254,7 @@ class Image extends Component<*, State> {
this._destroyImageLoader();
this._loadRequest = requestIdleCallback(
() => {
- const uri = resolveAssetSource(source);
+ const uri = resolveAssetUri(source);
this._imageRequestId = ImageLoader.load(uri, this._onLoad, this._onError);
this._onLoadStart();
},
@@ -286,7 +280,7 @@ class Image extends Component<*, State> {
if (onError) {
onError({
nativeEvent: {
- error: `Failed to load resource ${resolveAssetSource(source)} (404)`
+ error: `Failed to load resource ${resolveAssetUri(source)} (404)`
}
});
}
@@ -296,7 +290,7 @@ class Image extends Component<*, State> {
_onLoad = e => {
const { onLoad, source } = this.props;
const event = { nativeEvent: e };
- ImageUriCache.add(resolveAssetSource(source));
+ ImageUriCache.add(resolveAssetUri(source));
this._updateImageState(STATUS_LOADED);
if (onLoad) {
onLoad(event);