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);