diff --git a/.flowconfig b/.flowconfig index c05e58f597e..b8a0973f6b7 100644 --- a/.flowconfig +++ b/.flowconfig @@ -36,4 +36,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError [version] -^0.37.0 +^0.38.0 diff --git a/package.json b/package.json index b549bbb3617..c526db4b36e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "eslint-plugin-react-internal": "file:eslint-rules", "fbjs": "^0.8.9", "fbjs-scripts": "^0.6.0", - "flow-bin": "^0.37.0", + "flow-bin": "^0.38.0", "glob": "^6.0.1", "grunt": "^0.4.5", "grunt-cli": "^0.1.13", diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index c5ff7cee6ac..82da1f162b3 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -1477,6 +1477,10 @@ src/renderers/native/__tests__/ReactNativeMount-test.js * should be able to create and update a native component * returns the correct instance and calls it in the callback +src/renderers/native/__tests__/createReactNativeComponentClass-test.js +* should register viewConfigs +* should not allow viewConfigs with duplicate uiViewClassNames to be registered + src/renderers/shared/__tests__/ReactDebugTool-test.js * should add and remove hooks * warns once when an error is thrown in hook diff --git a/src/renderers/native/NativeMethodsMixin.js b/src/renderers/native/NativeMethodsMixin.js index 810ec3167fa..7816c3dffdd 100644 --- a/src/renderers/native/NativeMethodsMixin.js +++ b/src/renderers/native/NativeMethodsMixin.js @@ -18,40 +18,19 @@ var UIManager = require('UIManager'); var invariant = require('invariant'); -type MeasureOnSuccessCallback = ( - x: number, - y: number, - width: number, - height: number, - pageX: number, - pageY: number -) => void - -type MeasureInWindowOnSuccessCallback = ( - x: number, - y: number, - width: number, - height: number, -) => void - -type MeasureLayoutOnSuccessCallback = ( - left: number, - top: number, - width: number, - height: number -) => void - -function warnForStyleProps(props, validAttributes) { - for (var key in validAttributes.style) { - if (!(validAttributes[key] || props[key] === undefined)) { - console.error( - 'You are setting the style `{ ' + key + ': ... }` as a prop. You ' + - 'should nest it in a style object. ' + - 'E.g. `{ style: { ' + key + ': ... } }`' - ); - } - } -} +var { + mountSafeCallback, + throwOnStylesProp, + warnForStyleProps, +} = require('NativeMethodsMixinUtils'); + +import type { + MeasureInWindowOnSuccessCallback, + MeasureLayoutOnSuccessCallback, + MeasureOnSuccessCallback, + NativeMethodsInterface, +} from 'NativeMethodsMixinUtils'; +import type { ReactNativeBaseComponentViewConfig } from 'ReactNativeViewConfigRegistry'; /** * `NativeMethodsMixin` provides methods to access the underlying native @@ -65,7 +44,7 @@ function warnForStyleProps(props, validAttributes) { * information, see [Direct * Manipulation](docs/direct-manipulation.html). */ -var NativeMethodsMixin = { +var NativeMethodsMixin : NativeMethodsInterface = { /** * Determines the location on screen, width, and height of the given view and * returns the values via an async callback. If successful, the callback will @@ -140,18 +119,34 @@ var NativeMethodsMixin = { * Manipulation](docs/direct-manipulation.html)). */ setNativeProps: function(nativeProps: Object) { + const maybeViewConfig = (ReactNative.getViewConfig(this) : any); + + // If there is no host component beneath this we should fail silently. + // This is not an error; it could mean a class component rendered null. + if (maybeViewConfig === null) { + return; + } + + const viewConfig : ReactNativeBaseComponentViewConfig = + (maybeViewConfig : any); + + const { + uiViewClassName, + validAttributes, + } = viewConfig; + if (__DEV__) { - warnForStyleProps(nativeProps, this.viewConfig.validAttributes); + warnForStyleProps(nativeProps, validAttributes); } var updatePayload = ReactNativeAttributePayload.create( nativeProps, - this.viewConfig.validAttributes + validAttributes ); UIManager.updateView( (ReactNative.findNodeHandle(this) : any), - this.viewConfig.uiViewClassName, + uiViewClassName, updatePayload ); }, @@ -172,19 +167,6 @@ var NativeMethodsMixin = { }, }; -function throwOnStylesProp(component, props) { - if (props.styles !== undefined) { - var owner = component._owner || null; - var name = component.constructor.displayName; - var msg = '`styles` is not a supported property of `' + name + '`, did ' + - 'you mean `style` (singular)?'; - if (owner && owner.constructor && owner.constructor.displayName) { - msg += '\n\nCheck the `' + owner.constructor.displayName + '` parent ' + - ' component.'; - } - throw new Error(msg); - } -} if (__DEV__) { // hide this from Flow since we can't define these properties outside of // __DEV__ without actually implementing them (setting them to undefined @@ -203,20 +185,4 @@ if (__DEV__) { }; } -/** - * In the future, we should cleanup callbacks by cancelling them instead of - * using this. - */ -function mountSafeCallback( - context: ReactComponent, - callback: ?Function -): any { - return function() { - if (!callback || (typeof context.isMounted === 'function' && !context.isMounted())) { - return undefined; - } - return callback.apply(context, arguments); - }; -} - module.exports = NativeMethodsMixin; diff --git a/src/renderers/native/NativeMethodsMixinUtils.js b/src/renderers/native/NativeMethodsMixinUtils.js new file mode 100644 index 00000000000..b5aa4ae8d0a --- /dev/null +++ b/src/renderers/native/NativeMethodsMixinUtils.js @@ -0,0 +1,100 @@ +/** + * 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 NativeMethodsMixinUtils + * @flow + */ +'use strict'; + +export type MeasureOnSuccessCallback = ( + x: number, + y: number, + width: number, + height: number, + pageX: number, + pageY: number +) => void + +export type MeasureInWindowOnSuccessCallback = ( + x: number, + y: number, + width: number, + height: number, +) => void + +export type MeasureLayoutOnSuccessCallback = ( + left: number, + top: number, + width: number, + height: number +) => void + +/** + * Shared between ReactNativeFiberHostComponent and NativeMethodsMixin to keep + * API in sync. + */ +export interface NativeMethodsInterface { + blur() : void, + focus() : void, + measure(callback : MeasureOnSuccessCallback) : void, + measureInWindow(callback : MeasureInWindowOnSuccessCallback) : void, + measureLayout( + relativeToNativeNode: number, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail: () => void /* currently unused */ + ) : void, + setNativeProps(nativeProps: Object) : void, +} + +/** + * In the future, we should cleanup callbacks by cancelling them instead of + * using this. + */ +function mountSafeCallback( + context: any, + callback: ?Function +): any { + return function() { + if (!callback || (typeof context.isMounted === 'function' && !context.isMounted())) { + return undefined; + } + return callback.apply(context, arguments); + }; +} + +function throwOnStylesProp(component : any, props : any) { + if (props.styles !== undefined) { + var owner = component._owner || null; + var name = component.constructor.displayName; + var msg = '`styles` is not a supported property of `' + name + '`, did ' + + 'you mean `style` (singular)?'; + if (owner && owner.constructor && owner.constructor.displayName) { + msg += '\n\nCheck the `' + owner.constructor.displayName + '` parent ' + + ' component.'; + } + throw new Error(msg); + } +} + +function warnForStyleProps(props : any, validAttributes : any) { + for (var key in validAttributes.style) { + if (!(validAttributes[key] || props[key] === undefined)) { + console.error( + 'You are setting the style `{ ' + key + ': ... }` as a prop. You ' + + 'should nest it in a style object. ' + + 'E.g. `{ style: { ' + key + ': ... } }`' + ); + } + } +} + +module.exports = { + mountSafeCallback, + throwOnStylesProp, + warnForStyleProps, +}; diff --git a/src/renderers/native/ReactNativeFiber.js b/src/renderers/native/ReactNativeFiber.js index de7b46af4d5..20807c2f608 100644 --- a/src/renderers/native/ReactNativeFiber.js +++ b/src/renderers/native/ReactNativeFiber.js @@ -12,16 +12,11 @@ 'use strict'; -import type { Element } from 'React'; -import type { Fiber } from 'ReactFiber'; -import type { ReactNodeList } from 'ReactTypes'; -import type { ReactNativeBaseComponentViewConfig } from 'ReactNativeViewConfigRegistry'; - -const NativeMethodsMixin = require('NativeMethodsMixin'); const ReactFiberReconciler = require('ReactFiberReconciler'); const ReactGenericBatching = require('ReactGenericBatching'); const ReactNativeAttributePayload = require('ReactNativeAttributePayload'); const ReactNativeComponentTree = require('ReactNativeComponentTree'); +const ReactNativeFiberHostComponent = require('ReactNativeFiberHostComponent'); const ReactNativeInjection = require('ReactNativeInjection'); const ReactNativeTagHandles = require('ReactNativeTagHandles'); const ReactNativeViewConfigRegistry = require('ReactNativeViewConfigRegistry'); @@ -34,6 +29,11 @@ const findNodeHandle = require('findNodeHandle'); const invariant = require('invariant'); const { injectInternals } = require('ReactFiberDevToolsHook'); + +import type { Element } from 'React'; +import type { Fiber } from 'ReactFiber'; +import type { ReactNativeBaseComponentViewConfig } from 'ReactNativeViewConfigRegistry'; +import type { ReactNodeList } from 'ReactTypes'; const { precacheFiberNode, uncacheFiberNode, @@ -43,7 +43,7 @@ const { ReactNativeInjection.inject(); type Container = number; -type Instance = { +export type Instance = { _children: Array, _nativeTag: number, viewConfig: ReactNativeBaseComponentViewConfig, @@ -51,13 +51,6 @@ type Instance = { type Props = Object; type TextInstance = number; -function NativeHostComponent(tag, viewConfig) { - this._nativeTag = tag; - this._children = []; - this.viewConfig = viewConfig; -} -Object.assign(NativeHostComponent.prototype, NativeMethodsMixin); - function recursivelyUncacheFiberNode(node : Instance | TextInstance) { if (typeof node === 'number') { // Leaf node (eg text) uncacheFiberNode(node); @@ -156,7 +149,7 @@ const NativeRenderer = ReactFiberReconciler({ const viewConfig = ReactNativeViewConfigRegistry.get(type); if (__DEV__) { - for (let key in viewConfig.validAttributes) { + for (const key in viewConfig.validAttributes) { if (props.hasOwnProperty(key)) { deepFreezeAndThrowOnMutationInDev(props[key]); } @@ -175,12 +168,14 @@ const NativeRenderer = ReactFiberReconciler({ updatePayload, // props ); - const component = new NativeHostComponent(tag, viewConfig); + const component = new ReactNativeFiberHostComponent(tag, viewConfig); precacheFiberNode(internalInstanceHandle, tag); updateFiberProps(tag, props); - return component; + // Not sure how to avoid this cast. Flow is okay if the component is defined + // in the same file but if it's external it can't see the types. + return ((component : any) : Instance); }, createTextInstance( @@ -367,17 +362,22 @@ ReactGenericBatching.injection.injectFiberBatchedUpdates( const roots = new Map(); findNodeHandle.injection.injectFindNode( - (fiber: Fiber) => { - const instance: any = NativeRenderer.findHostInstance(fiber); - return instance ? instance._nativeTag : null; - } + (fiber: Fiber) => NativeRenderer.findHostInstance(fiber) ); findNodeHandle.injection.injectFindRootNodeID( (instance) => instance._nativeTag ); const ReactNative = { - findNodeHandle, + getViewConfig(componentOrHandle : any) : ?ReactNativeBaseComponentViewConfig { + const instance: any = findNodeHandle(componentOrHandle); + return instance ? instance.viewConfig : null; + }, + + findNodeHandle(componentOrHandle : any) : ?number { + const instance: any = findNodeHandle(componentOrHandle); + return instance ? instance._nativeTag : null; + }, render(element : Element, containerTag : any, callback: ?Function) { let root = roots.get(containerTag); diff --git a/src/renderers/native/ReactNativeFiberHostComponent.js b/src/renderers/native/ReactNativeFiberHostComponent.js new file mode 100644 index 00000000000..9d32f37af01 --- /dev/null +++ b/src/renderers/native/ReactNativeFiberHostComponent.js @@ -0,0 +1,122 @@ +/** + * Copyright 2013-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 ReactNativeFiberHostComponent + * @flow + * @preventMunge + */ + +'use strict'; + +var ReactNativeAttributePayload = require('ReactNativeAttributePayload'); +var TextInputState = require('TextInputState'); +var UIManager = require('UIManager'); + +var { + mountSafeCallback, + throwOnStylesProp, + warnForStyleProps, +} = require('NativeMethodsMixinUtils'); + +import type { + MeasureInWindowOnSuccessCallback, + MeasureLayoutOnSuccessCallback, + MeasureOnSuccessCallback, + NativeMethodsInterface, +} from 'NativeMethodsMixinUtils'; +import type { Instance } from 'ReactNativeFiber'; +import type { ReactNativeBaseComponentViewConfig } from 'ReactNativeViewConfigRegistry'; + +/** + * This component defines the same methods as NativeMethodsMixin but without the + * findNodeHandle wrapper. This wrapper is unnecessary for HostComponent views + * and would also result in a circular require.js dependency (since + * ReactNativeFiber depends on this component and NativeMethodsMixin depends on + * ReactNativeFiber). + */ +class ReactNativeFiberHostComponent implements NativeMethodsInterface { + _children: Array + _nativeTag: number + viewConfig: ReactNativeBaseComponentViewConfig + + constructor( + tag : number, + viewConfig : ReactNativeBaseComponentViewConfig + ) { + this._nativeTag = tag; + this._children = []; + this.viewConfig = viewConfig; + } + + blur() { + TextInputState.blurTextInput(this._nativeTag); + } + + focus() { + TextInputState.focusTextInput(this._nativeTag); + } + + measure(callback: MeasureOnSuccessCallback) { + UIManager.measure( + this._nativeTag, + mountSafeCallback(this, callback) + ); + } + + measureInWindow(callback: MeasureInWindowOnSuccessCallback) { + UIManager.measureInWindow( + this._nativeTag, + mountSafeCallback(this, callback) + ); + } + + measureLayout( + relativeToNativeNode: number, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail: () => void /* currently unused */ + ) { + UIManager.measureLayout( + this._nativeTag, + relativeToNativeNode, + mountSafeCallback(this, onFail), + mountSafeCallback(this, onSuccess) + ); + } + + setNativeProps(nativeProps: Object) { + if (__DEV__) { + warnForStyleProps(nativeProps, this.viewConfig.validAttributes); + } + + var updatePayload = ReactNativeAttributePayload.create( + nativeProps, + this.viewConfig.validAttributes + ); + + UIManager.updateView( + this._nativeTag, + this.viewConfig.uiViewClassName, + updatePayload + ); + } +} + +if (__DEV__) { + // hide this from Flow since we can't define these properties outside of + // __DEV__ without actually implementing them (setting them to undefined + // isn't allowed by ReactClass) + var prototype = (ReactNativeFiberHostComponent.prototype : any); + prototype.componentWillMount = function() { + throwOnStylesProp(this, this.props); + }; + prototype.componentWillReceiveProps = function(newProps) { + throwOnStylesProp(this, newProps); + }; +} + +module.exports = ReactNativeFiberHostComponent; diff --git a/src/renderers/native/ReactNativeStack.js b/src/renderers/native/ReactNativeStack.js index 55cf71fa1c6..1a0e047dcae 100644 --- a/src/renderers/native/ReactNativeStack.js +++ b/src/renderers/native/ReactNativeStack.js @@ -13,12 +13,14 @@ var ReactNativeComponentTree = require('ReactNativeComponentTree'); var ReactNativeInjection = require('ReactNativeInjection'); -var ReactNativeStackInjection = require('ReactNativeStackInjection'); var ReactNativeMount = require('ReactNativeMount'); +var ReactNativeStackInjection = require('ReactNativeStackInjection'); var ReactUpdates = require('ReactUpdates'); var findNodeHandle = require('findNodeHandle'); +import type { ReactNativeBaseComponentViewConfig } from 'ReactNativeViewConfigRegistry'; + ReactNativeInjection.inject(); ReactNativeStackInjection.inject(); @@ -40,6 +42,13 @@ findNodeHandle.injection.injectFindRootNodeID( var ReactNative = { hasReactNativeInitialized: false, findNodeHandle: findNodeHandle, + + getViewConfig(componentOrHandle : any) : ?ReactNativeBaseComponentViewConfig { + const tag = ReactNative.findNodeHandle(componentOrHandle); + const instance: any = ReactNativeComponentTree.getClosestInstanceFromNode(tag); + return instance ? instance.viewConfig : null; + }, + render: render, unmountComponentAtNode: ReactNativeMount.unmountComponentAtNode, diff --git a/src/renderers/native/ReactNativeViewConfigRegistry.js b/src/renderers/native/ReactNativeViewConfigRegistry.js index 383ee8bba2a..8f4aa559bc8 100644 --- a/src/renderers/native/ReactNativeViewConfigRegistry.js +++ b/src/renderers/native/ReactNativeViewConfigRegistry.js @@ -22,26 +22,23 @@ export type ReactNativeBaseComponentViewConfig = { const viewConfigs = new Map(); -const prefix = 'topsecret-'; - const ReactNativeViewConfigRegistry = { register(viewConfig : ReactNativeBaseComponentViewConfig) { - const name = viewConfig.uiViewClassName; + const uiViewClassName = viewConfig.uiViewClassName; invariant( - !viewConfigs.has(name), + !viewConfigs.has(uiViewClassName), 'Tried to register two views with the same name %s', - name + uiViewClassName ); - const secretName = prefix + name; - viewConfigs.set(secretName, viewConfig); - return secretName; + viewConfigs.set(uiViewClassName, viewConfig); + return uiViewClassName; }, - get(secretName: string) { - const config = viewConfigs.get(secretName); + get(uiViewClassName: string) { + const config = viewConfigs.get(uiViewClassName); invariant( config, 'View config not found for name %s', - secretName + uiViewClassName ); return config; }, diff --git a/src/renderers/native/__tests__/createReactNativeComponentClass-test.js b/src/renderers/native/__tests__/createReactNativeComponentClass-test.js new file mode 100644 index 00000000000..5b81ace0e6a --- /dev/null +++ b/src/renderers/native/__tests__/createReactNativeComponentClass-test.js @@ -0,0 +1,65 @@ +/** + * Copyright 2013-2015, 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. + * + * @emails react-core + */ + +'use strict'; + +var createReactNativeComponentClass; +var React; +var ReactNative; +var ReactNativeFeatureFlags = require('ReactNativeFeatureFlags'); + +describe('createReactNativeComponentClass', () => { + beforeEach(() => { + jest.resetModules(); + + createReactNativeComponentClass = require('createReactNativeComponentClass'); + React = require('React'); + ReactNative = require('ReactNative'); + }); + + it('should register viewConfigs', () => { + const textViewConfig = { + validAttributes: {}, + uiViewClassName: 'Text', + }; + const viewViewConfig = { + validAttributes: {}, + uiViewClassName: 'View', + }; + + const Text = createReactNativeComponentClass(textViewConfig); + const View = createReactNativeComponentClass(viewViewConfig); + + expect(Text).not.toBe(View); + + ReactNative.render(, 1); + ReactNative.render(, 1); + }); + + if (ReactNativeFeatureFlags.useFiber) { + it('should not allow viewConfigs with duplicate uiViewClassNames to be registered', () => { + const textViewConfig = { + validAttributes: {}, + uiViewClassName: 'Text', + }; + const altTextViewConfig = { + validAttributes: {}, + uiViewClassName: 'Text', // Same + }; + + createReactNativeComponentClass(textViewConfig); + + expect(() => { + createReactNativeComponentClass(altTextViewConfig); + }).toThrow('Tried to register two views with the same name Text'); + }); + } +}); diff --git a/src/renderers/native/createReactNativeComponentClass.js b/src/renderers/native/createReactNativeComponentClass.js index 0d560c0553c..3549c750fbe 100644 --- a/src/renderers/native/createReactNativeComponentClass.js +++ b/src/renderers/native/createReactNativeComponentClass.js @@ -13,8 +13,8 @@ 'use strict'; const ReactNativeBaseComponent = require('ReactNativeBaseComponent'); -const ReactNativeViewConfigRegistry = require('ReactNativeViewConfigRegistry'); const ReactNativeFeatureFlags = require('ReactNativeFeatureFlags'); +const ReactNativeViewConfigRegistry = require('ReactNativeViewConfigRegistry'); // See also ReactNativeBaseComponent type ReactNativeBaseComponentViewConfig = { diff --git a/yarn.lock b/yarn.lock index a35b145c6e2..c9241596d1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2175,9 +2175,9 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" -flow-bin@^0.37.0: - version "0.37.4" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.37.4.tgz#3d8da2ef746e80e730d166e09040f4198969b76b" +flow-bin@^0.38.0: + version "0.38.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.38.0.tgz#3ae096d401c969cc8b5798253fb82381e2d0237a" for-in@^0.1.5: version "0.1.6" @@ -3140,7 +3140,7 @@ jest-cli@^19.0.1: worker-farm "^1.3.1" yargs "^6.3.0" -jest-config@^19.0.0, jest-config@^19.0.1: +jest-config@^19.0.1: version "19.0.1" resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-19.0.1.tgz#a50698aca3b70949ff4e3898d339a13e166d8fb8" dependencies: @@ -3191,7 +3191,7 @@ jest-haste-map@^19.0.0: sane "~1.5.0" worker-farm "^1.3.1" -jest-jasmine2@^19.0.0, jest-jasmine2@^19.0.1: +jest-jasmine2@^19.0.1: version "19.0.1" resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-19.0.1.tgz#9a9ee34573fc15c4856ec32e65a0865ee878756e" dependencies: @@ -3246,7 +3246,7 @@ jest-resolve@^19.0.0: jest-haste-map "^19.0.0" resolve "^1.2.0" -jest-runtime@^19.0.0, jest-runtime@^19.0.1: +jest-runtime@^19.0.1: version "19.0.1" resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-19.0.1.tgz#7b584cbc690a500d9da148aba6a109bc9266a6b1" dependencies: