diff --git a/.gitignore b/.gitignore index fce7d1d993be4..415e2b7c4b16c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_STORE node_modules *~ +*.swp *.pyc static .grunt diff --git a/src/ReactWithAddons.js b/src/ReactWithAddons.js index 3b92d59f813ac..43a41a1034417 100644 --- a/src/ReactWithAddons.js +++ b/src/ReactWithAddons.js @@ -28,13 +28,15 @@ var LinkedStateMixin = require('LinkedStateMixin'); var React = require('React'); var ReactTransitionGroup = require('ReactTransitionGroup'); +var defineClass = require('defineClass'); var cx = require('cx'); React.addons = { classSet: cx, LinkedStateMixin: LinkedStateMixin, - TransitionGroup: ReactTransitionGroup + TransitionGroup: ReactTransitionGroup, + defineClass: defineClass }; module.exports = React; diff --git a/src/addons/defineClass/Advice.js b/src/addons/defineClass/Advice.js new file mode 100644 index 0000000000000..ae07953506416 --- /dev/null +++ b/src/addons/defineClass/Advice.js @@ -0,0 +1,94 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule Advice + */ + +'use strict'; + +var Advice = { + /** + * Creates a function which invokes the callback before the base function. + * The callback is invoked with the same context and arguments as the wrapper + * function. The return value is the same as it would have been otherwise. + * + * @param {function} base Function to be wrapped + * @param {function} callback Function to be invoked before base + * @return {function} Wrapped function + */ + before: function wrappedBefore(base, callback) { + return function() { + callback.apply(this, arguments); + return base.apply(this, arguments); + }; + }, + + /** + * Creates a function which invokes the callback after the base function. + * The callback is invoked with the same context and arguments as the wrapper + * function. The return value is the same as it would have been otherwise. + * + * @param {function} base Function to be wrapped + * @param {function} callback Function to be invoked after base + * @return {function} Wrapped function + */ + after: function(base, callback) { + return function wrappedAfter() { + var result = base.apply(this, arguments); + callback.apply(this, arguments); + return result; + }; + }, + + /** + * Creates a function which invokes the callback with the base function + * unshifted onto its arguments. The remaining arguments and context are + * callback are preserved, but the return value is replaced with the return + * value of the callback. + * + * @param {function} base Function to be wrapped + * @param {function} callback Function to be invoked with base as first arg + * @return {function} Wrapped function + */ + around: function(base, callback) { + return function wrappedAround() { + var args = []; + args.push(base.bind(this)); + for (var i = 0, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + return callback.apply(this, args); + }; + }, + + /** + * Creates a function which takes a predicate, evaluates the predicate with + * the same arguments and context as the base, and turns the function into a + * no-op if the predicate evaluates to false. + * + * @param {function} base Function to be wrapped + * @param {function(): boolean} predicate A predicate function + * @return {function} Wrapped function + */ + filter: function(base, predicate) { + return function wrappedFiltered() { + if (predicate.apply(this, arguments)) { + return base.apply(this, arguments); + } + }; + } +}; + +module.exports = Advice; diff --git a/src/addons/defineClass/AdviceDefinition.js b/src/addons/defineClass/AdviceDefinition.js new file mode 100644 index 0000000000000..ecec12240d5e4 --- /dev/null +++ b/src/addons/defineClass/AdviceDefinition.js @@ -0,0 +1,155 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule AdviceDefinition + */ + +'use strict'; + +// For more information on some of the terminology/variable names used in this +// file, see the Wikipedia page on aspect-oriented programming. +// http://en.wikipedia.org/wiki/Aspect-oriented_programming + +var Advice = require('Advice'); +var ReactCompositeComponent = require('ReactCompositeComponent'); +var SpecPolicy = ReactCompositeComponent.SpecPolicy; +var ReactCompositeComponentInterface = ReactCompositeComponent.Interface; + +var invariant = require('invariant'); +var mergeMethod = require('mergeMethod'); + +/** + * These are shims to simulate pointcuts on ReactCompositeComponent which + * correspond to its lifecycle. @see ReactCompositeComponent#LifeCycle. + * Refactoring ReactCompositeComponent to actually use these methods names is + * left as an exercise for the reader. + * I apologize for the repetition. I apologize. + */ +var RESERVED_ADVICE_KEYS = { + mount: function(joinPoint, callback) { + invariant( + joinPoint === 'before' || + joinPoint === 'after', + 'AdviceDefinition: The %s method is not supported for "mount"', + joinPoint + ); + switch (joinPoint) { + case 'before': + mergeMethod(this, 'componentWillMount', callback); + break; + case 'after': + mergeMethod(this, 'componentDidMount', callback); + break; + default: + break; + } + }, + + receiveProps: function(joinPoint, callback) { + invariant( + joinPoint === 'before', + 'AdviceDefinition: The %s method is not supported for "receiveProps"', + joinPoint + ); + switch (joinPoint) { + case 'before': + mergeMethod(this, 'componentWillReceiveProps', callback); + break; + default: + break; + } + }, + + update: function(joinPoint, callback) { + invariant( + joinPoint === 'before' || + joinPoint === 'after' || + joinPoint === 'filter', + 'AdviceDefinition: The %s method is not supported for "update"', + joinPoint + ); + switch (joinPoint) { + case 'before': + mergeMethod(this, 'componentWillUpdate', callback); + break; + case 'after': + mergeMethod(this, 'componentDidUpdate', callback); + break; + case 'filter': + mergeMethod(this, 'shouldComponentUpdate', callback); + break; + default: + break; + } + }, + + unmount: function(joinPoint, callback) { + invariant( + joinPoint === 'before', + 'AdviceDefinition: The % method is not supported for "unmount"', + joinPoint + ); + switch (joinPoint) { + case 'before': + mergeMethod(this, 'componentWillUnmount', callback); + default: + break; + } + } +}; + +function validateAdvice(spec, joinPoint, methodName) { + invariant( + typeof spec[methodName] === 'function', + 'AdviceDefinition: You are attempting to use advice methods on %s, when ' + + 'its type is %s. Advice methods can only work on spec properties which' + + 'are functions.', + methodName, typeof spec[methodName] + ); + + // We should not use around and filter with DEFINE_ONCE methods because they + // might modify the method in unacceptable ways (e.g. modify the return value, + // turn the method into a no-op). + var specPolicy = ReactCompositeComponentInterface[methodName]; + if (specPolicy === SpecPolicy.DEFINE_ONCE) { + invariant( + joinPoint === 'before' || + joinPoint === 'after', + 'AdviceDefinition: You may not use the %s method to override %s. ' + + 'Use the before or after methods instead.', + joinPoint, + methodName + ); + } +} + +function wrapMethod(joinPoint, methodName, callback) { + if (RESERVED_ADVICE_KEYS.hasOwnProperty(methodName)) { + RESERVED_ADVICE_KEYS[methodName].call(this, joinPoint, callback); + } else { + validateAdvice(this, joinPoint, methodName); + this[methodName] = Advice[joinPoint](this[methodName], callback); + } +} + +function AdviceDefinition() { + 'before after around filter'.split(' ').forEach(function(joinPoint) { + this[joinPoint] = function(methodName, callback) { + wrapMethod.call(this, joinPoint, methodName, callback); + }; + }.bind(this)); +} + +module.exports = AdviceDefinition; diff --git a/src/addons/defineClass/BaseDefinition.js b/src/addons/defineClass/BaseDefinition.js new file mode 100644 index 0000000000000..1a5327b3247ae --- /dev/null +++ b/src/addons/defineClass/BaseDefinition.js @@ -0,0 +1,37 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule BaseDefinition + */ + +'use strict'; + +var mergeMethod = require('mergeMethod'); + +function BaseDefinition() { + this.initialState = function(stateObj) { + mergeMethod(this, 'getInitialState', function() { + return stateObj; + }); + }; + + this.defaultProps = function(propsObj) { + mergeMethod(this, 'getDefaultProps', function() { + return propsObj; + }); + }; +} + +module.exports = BaseDefinition; diff --git a/src/addons/defineClass/__tests__/Advice-test.js b/src/addons/defineClass/__tests__/Advice-test.js new file mode 100644 index 0000000000000..4ab66908ad988 --- /dev/null +++ b/src/addons/defineClass/__tests__/Advice-test.js @@ -0,0 +1,157 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @emails react-core + */ + +"use strict"; + +var Advice = require('Advice'); +var mocks = require('mocks'); + +describe('Advice', function() { + var base, callback; + + beforeEach(function() { + base = mocks.getMockFunction(); + callback = mocks.getMockFunction(); + }); + + describe('wrappedBefore', function() { + it('should invoke callback before base', function() { + var stack = []; + + base.mockImplementation(function() { + stack.push(base); + }); + callback.mockImplementation(function() { + stack.push(callback); + }); + + Advice.before(base, callback)(); + expect(stack[0]).toBe(callback); + expect(stack[1]).toBe(base); + }); + + it('should pass `this` and `arguments` to base and callback', function() { + var args = ['Tom', 'Dick', 'Harry']; + var context = {}; + + Advice.before(base, callback).apply(context, args); + expect(base.mock.calls[0]).toEqual(args); + expect(base.mock.instances[0]).toBe(context); + expect(callback.mock.calls[0]).toEqual(args); + expect(callback.mock.instances[0]).toBe(context); + }); + + it('should return whatever base returns', function() { + var result = {}; + base.mockDefaultReturnValue(result); + expect(Advice.before(base, callback)()).toBe(base()); + }); + }); + + describe('wrappedAfter', function() { + it('should invoke callback after base', function() { + var stack = []; + + base.mockImplementation(function() { + stack.push(base); + }); + callback.mockImplementation(function() { + stack.push(callback); + }); + + Advice.after(base, callback)(); + expect(stack[0]).toBe(base); + expect(stack[1]).toBe(callback); + }); + + it('should pass `this` and `arguments` to base and callback', function() { + var args = ['Tom', 'Dick', 'Harry']; + var context = {}; + + Advice.after(base, callback).apply(context, args); + expect(base.mock.calls[0]).toEqual(args); + expect(base.mock.instances[0]).toBe(context); + expect(callback.mock.calls[0]).toEqual(args); + expect(callback.mock.instances[0]).toBe(context); + }); + + it('should return whatever base returns', function() { + var result = {}; + base.mockDefaultReturnValue(result); + expect(Advice.after(base, callback)()).toBe(base()); + }); + }); + + describe('wrappedAround', function() { + it('should invoke callback with base as first argument', function() { + var stack = []; + callback.mockImplementation(function(first) { + stack.push(first); + first('pizza'); + }); + Advice.around(base, callback)(); + expect(base.mock.calls[0]).toEqual(['pizza']); + }); + + it('should bind base to context', function() { + var context = {}; + callback.mockImplementation(function(first) { + first.call(null); + }); + Advice.around(base, callback).call(context); + expect(base.mock.instances[0]).toBe(context); + }); + + it('should invoke callback with `arguments` unshifted but same `this`', + function() { + var args = ['Tom', 'Dick', 'Harry']; + var context1 = {}; + var context2 = {}; + Advice.around(base, callback).apply(context1, args); + Advice.around(base, callback).apply(context2, args); + expect(callback.mock.calls[0].slice(1)).toEqual(args); + expect(callback.mock.calls[1].slice(1)).toEqual(args); + expect(callback.mock.instances[0]).toBe(context1); + expect(callback.mock.instances[1]).toBe(context2); + }); + + it('should return whatever the callback returns', function() { + var result = {}; + base.mockDefaultReturnValue(null); + callback.mockDefaultReturnValue(result); + expect(Advice.around(base, callback)()).toBe(result); + }); + }); + + describe('wrappedFilter', function() { + it('should work as normal if callback returns truthy', function() { + var result = {}; + base.mockDefaultReturnValue(result); + callback.mockDefaultReturnValue(1); + expect(Advice.filter(base, callback)()).toBe(result); + expect(base.mock.calls.length).toEqual(1); + }); + + it('should disable base if callback returns falsy', function() { + base.mockDefaultReturnValue(null); + callback.mockDefaultReturnValue(0); + expect(Advice.filter(base, callback)()).toBeUndefined(); + expect(base.mock.calls.length).toEqual(0); + }); + }); +}); diff --git a/src/addons/defineClass/__tests__/defineClass-test.js b/src/addons/defineClass/__tests__/defineClass-test.js new file mode 100644 index 0000000000000..df408150d7457 --- /dev/null +++ b/src/addons/defineClass/__tests__/defineClass-test.js @@ -0,0 +1,61 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @emails react-core + * @jsx React.DOM + */ + +var mocks = require('mocks'); +var React = require('React'); +var defineClass = require('defineClass'); + +describe('defineClass', function() { + var initialState, defaultProps; + beforeEach(function() { + initialState = { + activated: false + } + defaultProps = { + displayActive: 'Hooray!', + displayInactive: 'Boo!' + }; + }); + + it('should create a valid React component', function() { + var afterMount = mocks.getMockFunction(); + + var Component = defineClass(function() { + this.initialState(initialState); + this.defaultProps(defaultProps); + + this.render = function() { + var text = this.state.activated + ? this.props.displayActive + : this.props.displayInactive + return
{text}
+ }; + + this.after('mount', afterMount); + }); + + var container = document.createElement('div'); + var component = React.renderComponent(, container); + expect(component.state).toEqual(initialState); + expect(component.props.displayActive).toEqual(defaultProps.displayActive); + expect(component.props.displayInactive) + .toEqual(defaultProps.displayInactive); + expect(afterMount.mock.calls[0].value).toEqual(component.getDOMNode().value); + }); +}); diff --git a/src/addons/defineClass/__tests__/mergeMethod-test.js b/src/addons/defineClass/__tests__/mergeMethod-test.js new file mode 100644 index 0000000000000..139ac6d95c17b --- /dev/null +++ b/src/addons/defineClass/__tests__/mergeMethod-test.js @@ -0,0 +1,50 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @emails react-core + */ + +"use strict"; + +var mergeMethod = require('mergeMethod'); +var mocks = require('mocks'); +describe('mergeMethod', function() { + var spec, mockFunction; + + beforeEach(function() { + spec = {}; + mockFunction = mocks.getMockFunction(); + }); + + it('should add function to spec if named spec is undefined', function() { + mergeMethod(spec, 'pizza', mockFunction); + expect(spec.pizza).toBe(mockFunction); + }); + + it('should add function to spec if named spec is null', function() { + spec.pizza = null; + mergeMethod(spec, 'pizza', mockFunction); + expect(spec.pizza).toBe(mockFunction); + }); + + it('should throw if named spec is not a function', function() { + spec.pizza = 'pasta'; + expect(function() { + mergeMethod(spec, 'pizza', mockFunction); + }).toThrow(); + }); + + // TODO(brainkim): How do we unit-test behavior which relies on SpecPolicy? +}); diff --git a/src/addons/defineClass/defineClass.js b/src/addons/defineClass/defineClass.js new file mode 100644 index 0000000000000..089584cf3721d --- /dev/null +++ b/src/addons/defineClass/defineClass.js @@ -0,0 +1,64 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule defineClass + */ + +"use strict"; + +/** + * A 'definition' is a function which is invoked with a spec as its context. + * Therefore, all definitions are mixins, insofar as multiple definitions can + * be called on the same spec. + */ + +var BaseDefinition = require('BaseDefinition'); +var AdviceDefinition = require('AdviceDefinition'); +var ReactCompositeComponent = require('ReactCompositeComponent'); + +/** + * Adds a definition to the spec if it hasn't already been added. + * + * @param {object} spec + * @param {function} definition + */ +function addDefinition(spec, definition) { + if (spec._definitions.indexOf(definition) === -1) { + definition.call(spec, spec); + spec._definitions.push(definition); + } +} + +/** + * Creates a composite component given one or more definitions. + * + * @param {...[function]} definitions + * @return {function} Component constructor function. + * @public + */ +function defineClass(/* ...definitions */) { + var spec = { + _definitions: [] + }; + addDefinition(spec, BaseDefinition); + addDefinition(spec, AdviceDefinition); + for (var i = 0, l = arguments.length; i < l; i++) { + addDefinition(spec, arguments[i]); + } + + return ReactCompositeComponent.createClass(spec); +} + +module.exports = defineClass; diff --git a/src/addons/defineClass/mergeMethod.js b/src/addons/defineClass/mergeMethod.js new file mode 100644 index 0000000000000..052483536a3f4 --- /dev/null +++ b/src/addons/defineClass/mergeMethod.js @@ -0,0 +1,51 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule mergeMethod + */ + +'use strict'; + +var ReactCompositeComponent = require('ReactCompositeComponent'); +var SpecPolicy = ReactCompositeComponent.SpecPolicy; +var ReactCompositeComponentInterface = ReactCompositeComponent.Interface; +var invariant = require('invariant'); +var objMap = require('objMap'); +var createMergedResultFunction = require('createMergedResultFunction'); +var createChainedFunction = require('createChainedFunction'); + +function mergeMethod(spec, name, fn) { + if (spec[name] == null) { + spec[name] = fn; + } else { + var specPolicy = ReactCompositeComponentInterface[name]; + invariant( + typeof spec[name] === 'function' && ( + specPolicy === SpecPolicy.DEFINE_MANY || + specPolicy === SpecPolicy.DEFINE_MANY_MERGED + ), + 'mergeMethod: You are attempting to define %s more than once, or it is ' + + 'not a function. This conflict might be due to a mixin.', + name + ); + if (specPolicy === SpecPolicy.DEFINE_MANY_MERGED) { + spec[name] = createMergedResultFunction(spec[name], fn); + } else { + spec[name] = createChainedFunction(spec[name], fn); + } + } +} + +module.exports = mergeMethod; diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index df865104061b3..fc2344b3ae5a7 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -30,6 +30,8 @@ var keyMirror = require('keyMirror'); var merge = require('merge'); var mixInto = require('mixInto'); var objMap = require('objMap'); +var createChainedFunction = require('createChainedFunction'); +var createMergedResultFunction = require('createMergedResultFunction'); /** * Policies that describe methods in `ReactCompositeComponentInterface`. @@ -388,63 +390,6 @@ function mixSpecIntoComponent(Constructor, spec) { } } -/** - * Merge two objects, but throw if both contain the same key. - * - * @param {object} one The first object, which is mutated. - * @param {object} two The second object - * @return {object} one after it has been mutated to contain everything in two. - */ -function mergeObjectsWithNoDuplicateKeys(one, two) { - invariant( - one && two && typeof one === 'object' && typeof two === 'object', - 'mergeObjectsWithNoDuplicateKeys(): Cannot merge non-objects' - ); - - objMap(two, function(value, key) { - invariant( - one[key] === undefined, - 'mergeObjectsWithNoDuplicateKeys(): ' + - 'Tried to merge two objects with the same key: %s', - key - ); - one[key] = value; - }); - return one; -} - -/** - * Creates a function that invokes two functions and merges their return values. - * - * @param {function} one Function to invoke first. - * @param {function} two Function to invoke second. - * @return {function} Function that invokes the two argument functions. - * @private - */ -function createMergedResultFunction(one, two) { - return function mergedResult() { - return mergeObjectsWithNoDuplicateKeys( - one.apply(this, arguments), - two.apply(this, arguments) - ); - }; -} - -/** - * Creates a function that invokes two functions and ignores their return vales. - * - * @param {function} one Function to invoke first. - * @param {function} two Function to invoke second. - * @return {function} Function that invokes the two argument functions. - * @private - */ -function createChainedFunction(one, two) { - return function chainedFunction() { - one.apply(this, arguments); - two.apply(this, arguments); - }; -} - /** * `ReactCompositeComponent` maintains an auxiliary life cycle state in * `this._compositeLifeCycleState` (which can be null). @@ -941,6 +886,10 @@ mixInto(ReactCompositeComponentBase, ReactCompositeComponentMixin); */ var ReactCompositeComponent = { + SpecPolicy: SpecPolicy, + + Interface: ReactCompositeComponentInterface, + LifeCycle: CompositeLifeCycle, Base: ReactCompositeComponentBase, diff --git a/src/utils/createChainedFunction.js b/src/utils/createChainedFunction.js new file mode 100644 index 0000000000000..780f3077739d8 --- /dev/null +++ b/src/utils/createChainedFunction.js @@ -0,0 +1,34 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule createChainedFunction + */ + +/** + * Creates a function that invokes two functions and ignores their return vales. + * + * @param {function} one Function to invoke first. + * @param {function} two Function to invoke second. + * @return {function} Function that invokes the two argument functions. + * @private + */ +function createChainedFunction(one, two) { + return function chainedFunction() { + one.apply(this, arguments); + two.apply(this, arguments); + }; +} + +module.exports = createChainedFunction; diff --git a/src/utils/createMergedResultFunction.js b/src/utils/createMergedResultFunction.js new file mode 100644 index 0000000000000..61f12f05f0f93 --- /dev/null +++ b/src/utils/createMergedResultFunction.js @@ -0,0 +1,40 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule createMergedResultFunction + */ + +"use strict"; + +var mergeObjectsWithNoDuplicates = require('mergeObjectsWithNoDuplicates'); + +/** + * Creates a function that invokes two functions and merges their return values. + * + * @param {function} one Function to invoke first. + * @param {function} two Function to invoke second. + * @return {function} Function that invokes the two argument functions. + * @private + */ +function createMergedResultFunction(one, two) { + return function mergedResult() { + return mergeObjectsWithNoDuplicates( + one.apply(this, arguments), + two.apply(this, arguments) + ); + }; +} + +module.exports = createMergedResultFunction; diff --git a/src/utils/mergeObjectsWithNoDuplicates.js b/src/utils/mergeObjectsWithNoDuplicates.js new file mode 100644 index 0000000000000..449462a6423d3 --- /dev/null +++ b/src/utils/mergeObjectsWithNoDuplicates.js @@ -0,0 +1,49 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule mergeObjectsWithNoDuplicates + */ + +"use strict"; + +var invariant = require('invariant'); +var objMap = require('objMap'); + +/** + * Merge two objects, but throw if both contain the same key. + * + * @param {object} one The first object, which is mutated. + * @param {object} two The second object + * @return {object} one after it has been mutated to contain everything in two. + */ +function mergeObjectsWithNoDuplicates(one, two) { + invariant( + one && two && typeof one === 'object' && typeof two === 'object', + 'mergeObjectsWithNoDuplicateKeys(): Cannot merge non-objects' + ); + + objMap(two, function(value, key) { + invariant( + one[key] === undefined, + 'mergeObjectsWithNoDuplicateKeys(): ' + + 'Tried to merge two objects with the same key: %s', + key + ); + one[key] = value; + }); + return one; +} + +module.exports = mergeObjectsWithNoDuplicates;