From 915151c5d75aea054cf0a16335abd5c84ddd19cf Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Thu, 16 Apr 2015 18:17:19 -0700 Subject: [PATCH] [ReactNative] verifyPropTypes against native exports --- Examples/UIExplorer/ImageExample.js | 18 ++-- Libraries/Components/ScrollView/ScrollView.js | 19 +++- Libraries/Components/SliderIOS/SliderIOS.js | 26 +++-- Libraries/Image/Image.ios.js | 96 ++++++++++++++----- Libraries/Image/RCTNetworkImageViewManager.m | 2 +- Libraries/Image/RCTStaticImageManager.m | 2 +- Libraries/ReactIOS/ReactIOSStyleAttributes.js | 17 ++-- Libraries/ReactIOS/requireNativeComponent.js | 20 +++- Libraries/ReactIOS/verifyPropTypes.js | 40 ++++++++ React/Views/RCTViewManager.m | 1 - 10 files changed, 177 insertions(+), 64 deletions(-) create mode 100644 Libraries/ReactIOS/verifyPropTypes.js diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index b115e0f0be..b4c54f997d 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -226,10 +226,8 @@ exports.examples = [ Contain @@ -238,10 +236,8 @@ exports.examples = [ Cover @@ -250,10 +246,8 @@ exports.examples = [ Stretch diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index fc7fc7223c..0308688b57 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -17,7 +17,6 @@ var PointPropType = require('PointPropType'); var RCTScrollView = require('NativeModules').UIManager.RCTScrollView; var RCTScrollViewConsts = RCTScrollView.Constants; var React = require('React'); -var ReactIOSTagHandles = require('ReactIOSTagHandles'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var RCTUIManager = require('NativeModules').UIManager; var ScrollResponder = require('ScrollResponder'); @@ -32,6 +31,7 @@ var flattenStyle = require('flattenStyle'); var insetsDiffer = require('insetsDiffer'); var invariant = require('invariant'); var pointsDiffer = require('pointsDiffer'); +var requireNativeComponent = require('requireNativeComponent'); var PropTypes = React.PropTypes; @@ -73,6 +73,12 @@ var ScrollView = React.createClass({ * the `alwaysBounce*` props are true. The default value is true. */ bounces: PropTypes.bool, + /** + * When true, gestures can drive zoom past min/max and the zoom will animate + * to the min/max value at gesture end, otherwise the zoom will not exceed + * the limits. + */ + bouncesZoom: PropTypes.bool, /** * When true, the scroll view bounces horizontally when it reaches the end * even if the content is smaller than the scroll view itself. The default @@ -120,6 +126,16 @@ var ScrollView = React.createClass({ * instead of vertically in a column. The default value is false. */ horizontal: PropTypes.bool, + /** + * When true, the ScrollView will try to lock to only vertical or horizontal + * scrolling while dragging. The default value is false. + */ + directionalLockEnabled: PropTypes.bool, + /** + * When false, once tracking starts, won't try to drag if the touch moves. + * The default value is true. + */ + canCancelContentTouches: PropTypes.bool, /** * Determines whether the keyboard gets dismissed in response to a drag. * - 'none' (the default), drags do not dismiss the keyboard. @@ -359,6 +375,7 @@ if (Platform.OS === 'android') { validAttributes: validAttributes, uiViewClassName: 'RCTScrollView', }); + var RCTScrollView = requireNativeComponent('RCTScrollView', ScrollView); } module.exports = ScrollView; diff --git a/Libraries/Components/SliderIOS/SliderIOS.js b/Libraries/Components/SliderIOS/SliderIOS.js index ae2475d336..81815ba34c 100644 --- a/Libraries/Components/SliderIOS/SliderIOS.js +++ b/Libraries/Components/SliderIOS/SliderIOS.js @@ -12,6 +12,7 @@ 'use strict'; var NativeMethodsMixin = require('NativeMethodsMixin'); +var Platform = require('Platform'); var PropTypes = require('ReactPropTypes'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); @@ -21,6 +22,7 @@ var View = require('View'); var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); var merge = require('merge'); +var requireNativeComponent = require('requireNativeComponent'); type Event = Object; @@ -96,16 +98,20 @@ var styles = StyleSheet.create({ }, }); -var validAttributes = { - ...ReactIOSViewAttributes.UIView, - value: true, - minimumValue: true, - maximumValue: true, -}; +if (Platform.OS === 'ios') { + var RCTSlider = requireNativeComponent('RCTSlider', SliderIOS); +} else { + var validAttributes = { + ...ReactIOSViewAttributes.UIView, + value: true, + minimumValue: true, + maximumValue: true, + }; -var RCTSlider = createReactIOSNativeComponentClass({ - validAttributes: validAttributes, - uiViewClassName: 'RCTSlider', -}); + var RCTSlider = createReactIOSNativeComponentClass({ + validAttributes: validAttributes, + uiViewClassName: 'RCTSlider', + }); +} module.exports = SliderIOS; diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 5f3dfdd986..fed358e133 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -14,6 +14,7 @@ var EdgeInsetsPropType = require('EdgeInsetsPropType'); var NativeMethodsMixin = require('NativeMethodsMixin'); var NativeModules = require('NativeModules'); +var Platform = require('Platform'); var PropTypes = require('ReactPropTypes'); var ImageResizeMode = require('ImageResizeMode'); var ImageStylePropTypes = require('ImageStylePropTypes'); @@ -27,7 +28,9 @@ var flattenStyle = require('flattenStyle'); var insetsDiffer = require('insetsDiffer'); var invariant = require('invariant'); var merge = require('merge'); +var requireNativeComponent = require('requireNativeComponent'); var warning = require('warning'); +var verifyPropTypes = require('verifyPropTypes'); /** * A react component for displaying different types of images, @@ -64,6 +67,13 @@ var Image = React.createClass({ source: PropTypes.shape({ uri: PropTypes.string, }), + /** + * A static image to display while downloading the final image off the + * network. + */ + defaultSource: PropTypes.shape({ + uri: PropTypes.string, + }), /** * Whether this element should be revealed as an accessible element. */ @@ -80,6 +90,11 @@ var Image = React.createClass({ * [Apple documentation](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIImage_Class/index.html#//apple_ref/occ/instm/UIImage/resizableImageWithCapInsets) */ capInsets: EdgeInsetsPropType, + /** + * Determines how to resize the image when the frame doesn't match the raw + * image dimensions. + */ + resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch']), style: StyleSheetPropType(ImageStylePropTypes), /** * A unique identifier for this element to be used in UI Automation @@ -104,6 +119,12 @@ var Image = React.createClass({ }, render: function() { + for (var prop in nativeOnlyProps) { + if (this.props[prop] !== undefined) { + console.warn('Prop `' + prop + ' = ' + this.props[prop] + '` should ' + + 'not be set directly on Image.'); + } + } var style = flattenStyle([styles.base, this.props.style]); invariant(style, "style must be initialized"); var source = this.props.source; @@ -119,28 +140,36 @@ var Image = React.createClass({ if (this.props.style && this.props.style.tintColor) { warning(RawImage === RCTStaticImage, 'tintColor style only supported on static images.'); } - + var resizeMode = this.props.resizeMode || style.resizeMode; var contentModes = NativeModules.UIManager.UIView.ContentMode; - var resizeMode; - if (style.resizeMode === ImageResizeMode.stretch) { - resizeMode = contentModes.ScaleToFill; - } else if (style.resizeMode === ImageResizeMode.contain) { - resizeMode = contentModes.ScaleAspectFit; + var contentMode; + if (resizeMode === ImageResizeMode.stretch) { + contentMode = contentModes.ScaleToFill; + } else if (resizeMode === ImageResizeMode.contain) { + contentMode = contentModes.ScaleAspectFit; } else { // ImageResizeMode.cover or undefined - resizeMode = contentModes.ScaleAspectFill; + contentMode = contentModes.ScaleAspectFill; } var nativeProps = merge(this.props, { style, - resizeMode, + contentMode, tintColor: style.tintColor, }); + if (Platform.OS === 'android') { + // TODO: update android native code to not need this + nativeProps.resizeMode = contentMode; + delete nativeProps.contentMode; + } if (isStored) { nativeProps.imageTag = source.uri; } else { nativeProps.src = source.uri; } + if (this.props.defaultSource) { + nativeProps.defaultImageSrc = this.props.defaultSource.uri; + } return ; } }); @@ -151,24 +180,39 @@ var styles = StyleSheet.create({ }, }); -var CommonImageViewAttributes = merge(ReactIOSViewAttributes.UIView, { - accessible: true, - accessibilityLabel: true, - capInsets: {diff: insetsDiffer}, // UIEdgeInsets=UIEdgeInsetsZero - imageTag: true, - resizeMode: true, +if (Platform.OS === 'android') { + var CommonImageViewAttributes = merge(ReactIOSViewAttributes.UIView, { + accessible: true, + accessibilityLabel: true, + capInsets: {diff: insetsDiffer}, // UIEdgeInsets=UIEdgeInsetsZero + imageTag: true, + resizeMode: true, + src: true, + testID: PropTypes.string, + }); + + var RCTStaticImage = createReactIOSNativeComponentClass({ + validAttributes: merge(CommonImageViewAttributes, { tintColor: true }), + uiViewClassName: 'RCTStaticImage', + }); + + var RCTNetworkImage = createReactIOSNativeComponentClass({ + validAttributes: merge(CommonImageViewAttributes, { defaultImageSrc: true }), + uiViewClassName: 'RCTNetworkImageView', + }); +} else { + var RCTStaticImage = requireNativeComponent('RCTStaticImage', null); + var RCTNetworkImage = requireNativeComponent('RCTNetworkImageView', null); +} +var nativeOnlyProps = { src: true, - testID: PropTypes.string, -}); - -var RCTStaticImage = createReactIOSNativeComponentClass({ - validAttributes: merge(CommonImageViewAttributes, { tintColor: true }), - uiViewClassName: 'RCTStaticImage', -}); - -var RCTNetworkImage = createReactIOSNativeComponentClass({ - validAttributes: merge(CommonImageViewAttributes, { defaultImageSrc: true }), - uiViewClassName: 'RCTNetworkImageView', -}); + defaultImageSrc: true, + imageTag: true, + contentMode: true, +}; +if (__DEV__) { + verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps); + verifyPropTypes(Image, RCTNetworkImage.viewConfig, nativeOnlyProps); +} module.exports = Image; diff --git a/Libraries/Image/RCTNetworkImageViewManager.m b/Libraries/Image/RCTNetworkImageViewManager.m index 005b726cf4..2ecf699714 100644 --- a/Libraries/Image/RCTNetworkImageViewManager.m +++ b/Libraries/Image/RCTNetworkImageViewManager.m @@ -29,6 +29,6 @@ RCT_EXPORT_MODULE() RCT_REMAP_VIEW_PROPERTY(defaultImageSrc, defaultImage, UIImage) RCT_REMAP_VIEW_PROPERTY(src, imageURL, NSURL) -RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) +RCT_EXPORT_VIEW_PROPERTY(contentMode, UIViewContentMode) @end diff --git a/Libraries/Image/RCTStaticImageManager.m b/Libraries/Image/RCTStaticImageManager.m index 2d80117e4d..87a50d8fe8 100644 --- a/Libraries/Image/RCTStaticImageManager.m +++ b/Libraries/Image/RCTStaticImageManager.m @@ -26,7 +26,7 @@ RCT_EXPORT_MODULE() } RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets) -RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) +RCT_EXPORT_VIEW_PROPERTY(contentMode, UIViewContentMode) RCT_CUSTOM_VIEW_PROPERTY(src, NSURL, RCTStaticImage) { if (json) { diff --git a/Libraries/ReactIOS/ReactIOSStyleAttributes.js b/Libraries/ReactIOS/ReactIOSStyleAttributes.js index b332bec216..5f83523e28 100644 --- a/Libraries/ReactIOS/ReactIOSStyleAttributes.js +++ b/Libraries/ReactIOS/ReactIOSStyleAttributes.js @@ -10,22 +10,23 @@ * @flow */ -"use strict"; +'use strict'; +var ImageStylePropTypes = require('ImageStylePropTypes'); var TextStylePropTypes = require('TextStylePropTypes'); var ViewStylePropTypes = require('ViewStylePropTypes'); -var deepDiffer = require('deepDiffer'); var keyMirror = require('keyMirror'); var matricesDiffer = require('matricesDiffer'); -var merge = require('merge'); +var sizesDiffer = require('sizesDiffer'); -var ReactIOSStyleAttributes = merge( - keyMirror(ViewStylePropTypes), - keyMirror(TextStylePropTypes) -); +var ReactIOSStyleAttributes = { + ...keyMirror(ViewStylePropTypes), + ...keyMirror(TextStylePropTypes), + ...keyMirror(ImageStylePropTypes), +}; ReactIOSStyleAttributes.transformMatrix = { diff: matricesDiffer }; -ReactIOSStyleAttributes.shadowOffset = { diff: deepDiffer }; +ReactIOSStyleAttributes.shadowOffset = { diff: sizesDiffer }; module.exports = ReactIOSStyleAttributes; diff --git a/Libraries/ReactIOS/requireNativeComponent.js b/Libraries/ReactIOS/requireNativeComponent.js index 0231e7f5f8..55ad8a6b98 100644 --- a/Libraries/ReactIOS/requireNativeComponent.js +++ b/Libraries/ReactIOS/requireNativeComponent.js @@ -20,18 +20,27 @@ var insetsDiffer = require('insetsDiffer'); var pointsDiffer = require('pointsDiffer'); var matricesDiffer = require('matricesDiffer'); var sizesDiffer = require('sizesDiffer'); +var verifyPropTypes = require('verifyPropTypes'); /** * Used to create React components that directly wrap native component * implementations. Config information is extracted from data exported from the - * RCTUIManager module. It is still strongly preferred that you wrap the native - * component in a hand-written component with full propTypes definitions and - * other documentation. + * RCTUIManager module. You should also wrap the native component in a + * hand-written component with full propTypes definitions and other + * documentation - pass the hand-written component in as `wrapperComponent` to + * verify all the native props are documented via `propTypes`. + * + * If some native props shouldn't be exposed in the wrapper interface, you can + * pass null for `wrapperComponent` and call `verifyPropTypes` directly + * with `nativePropsToIgnore`; * * Common types are lined up with the appropriate prop differs with * `TypeToDifferMap`. Non-scalar types not in the map default to `deepDiffer`. */ -function requireNativeComponent(viewName: string): Function { +function requireNativeComponent( + viewName: string, + wrapperComponent: ?Function +): Function { var viewConfig = RCTUIManager.viewConfigs && RCTUIManager.viewConfigs[viewName]; if (!viewConfig) { return UnimplementedView; @@ -46,6 +55,9 @@ function requireNativeComponent(viewName: string): Function { var differ = TypeToDifferMap[nativeProps[key].type] || deepDiffer; viewConfig.validAttributes[key] = {diff: differ}; } + if (__DEV__) { + wrapperComponent && verifyPropTypes(wrapperComponent, viewConfig); + } return createReactIOSNativeComponentClass(viewConfig); } diff --git a/Libraries/ReactIOS/verifyPropTypes.js b/Libraries/ReactIOS/verifyPropTypes.js new file mode 100644 index 0000000000..032e572ece --- /dev/null +++ b/Libraries/ReactIOS/verifyPropTypes.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule verifyPropTypes + * @flow + */ +'use strict'; + +var ReactIOSStyleAttributes = require('ReactIOSStyleAttributes'); +var View = require('View'); + +function verifyPropTypes( + component: Function, + viewConfig: Object, + nativePropsToIgnore?: Object +) { + if (!viewConfig) { + return; // This happens for UnimplementedView. + } + var nativeProps = viewConfig.nativeProps; + for (var prop in viewConfig.nativeProps) { + if (!component.propTypes[prop] && + !View.propTypes[prop] && + !ReactIOSStyleAttributes[prop] && + (!nativePropsToIgnore || !nativePropsToIgnore[prop])) { + throw new Error( + '`' + component.displayName + '` has no propType for native prop `' + + viewConfig.uiViewClassName + '.' + prop + '` of native type `' + + nativeProps[prop].type + '`' + ); + } + } +} + +module.exports = verifyPropTypes; diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 9758a09443..3c84853745 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -61,7 +61,6 @@ RCT_EXPORT_MODULE() #pragma mark - View properties RCT_EXPORT_VIEW_PROPERTY(accessibilityLabel, NSString) -RCT_EXPORT_VIEW_PROPERTY(hidden, BOOL) RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) RCT_REMAP_VIEW_PROPERTY(accessible, isAccessibilityElement, BOOL) RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString)