diff --git a/Libraries/Components/UnimplementedViews/UnimplementedView.js b/Libraries/Components/UnimplementedViews/UnimplementedView.js new file mode 100644 index 000000000..6aebef38b --- /dev/null +++ b/Libraries/Components/UnimplementedViews/UnimplementedView.js @@ -0,0 +1,37 @@ +/** + * Common implementation for a simple stubbed view. Simply applies the view's styles to the inner + * View component and renders its children. + * + * @providesModule UnimplementedView + */ + +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var UnimplementedView = React.createClass({ + setNativeProps: function() { + // Do nothing. + // This method is required in order to use this view as a Touchable* child. + // See ensureComponentIsNative.js for more info + }, + render: function() { + return ( + + {this.props.children} + + ); + }, +}); + +var styles = StyleSheet.create({ + unimplementedView: { + borderWidth: 1, + borderColor: 'red', + alignSelf: 'flex-start', + } +}); + +module.exports = UnimplementedView; diff --git a/Libraries/ReactIOS/requireNativeComponent.js b/Libraries/ReactIOS/requireNativeComponent.js new file mode 100644 index 000000000..0231e7f5f --- /dev/null +++ b/Libraries/ReactIOS/requireNativeComponent.js @@ -0,0 +1,62 @@ +/** + * 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 requireNativeComponent + * @flow + */ +'use strict'; + +var RCTUIManager = require('NativeModules').UIManager; +var UnimplementedView = require('UnimplementedView'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var deepDiffer = require('deepDiffer'); +var insetsDiffer = require('insetsDiffer'); +var pointsDiffer = require('pointsDiffer'); +var matricesDiffer = require('matricesDiffer'); +var sizesDiffer = require('sizesDiffer'); + +/** + * 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. + * + * 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 { + var viewConfig = RCTUIManager.viewConfigs && RCTUIManager.viewConfigs[viewName]; + if (!viewConfig) { + return UnimplementedView; + } + var nativeProps = { + ...RCTUIManager.viewConfigs.RCTView.nativeProps, + ...viewConfig.nativeProps, + }; + viewConfig.validAttributes = {}; + for (var key in nativeProps) { + // TODO: deep diff by default in diffRawProperties instead of setting it here + var differ = TypeToDifferMap[nativeProps[key].type] || deepDiffer; + viewConfig.validAttributes[key] = {diff: differ}; + } + return createReactIOSNativeComponentClass(viewConfig); +} + +var TypeToDifferMap = { + // iOS Types + CATransform3D: matricesDiffer, + CGPoint: pointsDiffer, + CGSize: sizesDiffer, + UIEdgeInsets: insetsDiffer, + // Android Types + // (not yet implemented) +}; + +module.exports = requireNativeComponent; diff --git a/Libraries/Utilities/differ/sizesDiffer.js b/Libraries/Utilities/differ/sizesDiffer.js new file mode 100644 index 000000000..3bdc72acb --- /dev/null +++ b/Libraries/Utilities/differ/sizesDiffer.js @@ -0,0 +1,19 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule sizesDiffer + */ +'use strict'; + +var dummySize = {w: undefined, h: undefined}; + +var sizesDiffer = function(one, two) { + one = one || dummySize; + two = two || dummySize; + return one !== two && ( + one.w !== two.w || + one.h !== two.h + ); +}; + +module.exports = sizesDiffer; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 1aa978f46..01bff7eae 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -59,6 +59,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { // Plugins DeviceEventEmitter: require('RCTDeviceEventEmitter'), NativeModules: require('NativeModules'), + requireNativeComponent: require('requireNativeComponent'), addons: { LinkedStateMixin: require('LinkedStateMixin'), diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 86c58d0dd..266a1bb67 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -193,6 +193,7 @@ static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimatio NSMutableDictionary *_defaultShadowViews; // RCT thread only NSMutableDictionary *_defaultViews; // Main thread only NSDictionary *_viewManagers; + NSDictionary *_viewConfigs; NSUInteger _rootTag; } @@ -219,6 +220,28 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName) return name; } +static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewName) +{ + NSMutableDictionary *nativeProps = [[NSMutableDictionary alloc] init]; + static const char *prefix = "getPropConfig"; + static const NSUInteger prefixLength = sizeof("getPropConfig") - 1; + unsigned int methodCount = 0; + Method *methods = class_copyMethodList(objc_getMetaClass(class_getName(managerClass)), &methodCount); + for (unsigned int i = 0; i < methodCount; i++) { + Method method = methods[i]; + SEL getInfo = method_getName(method); + const char *selName = sel_getName(getInfo); + if (strlen(selName) > prefixLength && strncmp(selName, prefix, prefixLength) == 0) { + NSDictionary *info = ((NSDictionary *(*)(id, SEL))method_getImplementation(method))(managerClass, getInfo); + nativeProps[info[@"name"]] = info; + } + } + return @{ + @"uiViewClassName": viewName, + @"nativeProps": nativeProps + }; +} + /** * This private constructor should only be called when creating * isolated UIImanager instances for testing. Normal initialization @@ -292,13 +315,17 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName) // Get view managers from bridge NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *viewConfigs = [[NSMutableDictionary alloc] init]; [_bridge.modules enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, RCTViewManager *manager, BOOL *stop) { if ([manager isKindOfClass:[RCTViewManager class]]) { - viewManagers[RCTViewNameForModuleName(moduleName)] = manager; + NSString *viewName = RCTViewNameForModuleName(moduleName); + viewManagers[viewName] = manager; + viewConfigs[viewName] = RCTViewConfigForModule([manager class], viewName); } }]; _viewManagers = [viewManagers copy]; + _viewConfigs = [viewConfigs copy]; } - (void)registerRootView:(UIView *)rootView; @@ -1374,7 +1401,7 @@ RCT_EXPORT_METHOD(clearJSResponder) allJSConstants[name] = [constantsNamespace copy]; } }]; - + allJSConstants[@"viewConfigs"] = _viewConfigs; return allJSConstants; } diff --git a/React/Views/RCTViewManager.h b/React/Views/RCTViewManager.h index 92336fa41..74c7bbd8c 100644 --- a/React/Views/RCTViewManager.h +++ b/React/Views/RCTViewManager.h @@ -109,6 +109,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * within the view or shadowView. */ #define RCT_REMAP_VIEW_PROPERTY(name, keyPath, type) \ +RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ - (void)set_##name:(id)json forView:(id)view withDefaultView:(id)defaultView { \ if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \ (!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \ @@ -117,6 +118,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v } #define RCT_REMAP_SHADOW_PROPERTY(name, keyPath, type) \ +RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ - (void)set_##name:(id)json forShadowView:(id)view withDefaultView:(id)defaultView { \ if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \ (!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \ @@ -130,9 +132,11 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * refer to "json", "view" and "defaultView" to implement the required logic. */ #define RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass) \ +RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ - (void)set_##name:(id)json forView:(viewClass *)view withDefaultView:(viewClass *)defaultView #define RCT_CUSTOM_SHADOW_PROPERTY(name, type, viewClass) \ +RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ - (void)set_##name:(id)json forShadowView:(viewClass *)view withDefaultView:(viewClass *)defaultView /** @@ -160,4 +164,17 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v [self set_##newName:json forView:view withDefaultView:defaultView]; \ } +/** + * PROP_CONFIG macros should only be paired with property setters. + */ +#define RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \ ++ (NSDictionary *)getPropConfigView_##name { \ + return @{@"name": @#name, @"type": @#type}; \ +} + +#define RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \ ++ (NSDictionary *)getPropConfigShadow_##name { \ + return @{@"name": @#name, @"type": @#type}; \ +} + @end