diff --git a/README.md b/README.md index c323784..e95eb54 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ export default connect((props, ref) => ({ ## Usage -### `connect([mapFirebaseToProps])` +### `connect([mapFirebaseToProps], [mergeProps])` Connects a React component to a Firebase App reference. @@ -48,10 +48,15 @@ It does not modify the component class passed to it. Instead, it *returns* a new * [`mapFirebaseToProps(props, ref, firebaseApp): subscriptions`] \(*Object or Function*): Its result, or the argument itself must be a plain object. Each value must either be a path to a location in your database, a query object or a function. If you omit it, the default implementation just passes `firebaseApp` as a prop to your component. + +* [`mergeProps(ownProps, firebaseProps): props`] \(*Function*): If specified, it is passed the parent `props` and current subscription state merged with the result of `mapFirebaseToProps()`. The plain object you return from it will be passed as props to the wrapped component. If you omit it, `Object.assign({}, ownProps, firebaseProps)` is used by default. + #### Returns A React component class that passes subscriptions and actions as props to your component according to the specified options. +> Note: "actions" are any function values returned by `mapFirebaseToProps()` which are typically used to modify data in Firebase. + ##### Static Properties * `WrappedComponent` *(Component)*: The original component class passed to `connect()`. diff --git a/src/connect.js b/src/connect.js index c68ca93..125b608 100644 --- a/src/connect.js +++ b/src/connect.js @@ -6,10 +6,9 @@ import 'firebase/database' import { firebaseAppShape } from './PropTypes' import { applyMethods, getDisplayName } from './utils' -const mergeProps = (actionProps, subscriptionProps, ownProps) => ({ +const defaultMergeProps = (ownProps, firebaseProps) => ({ ...ownProps, - ...actionProps, - ...subscriptionProps, + ...firebaseProps, }) const mapSubscriptionsToQueries = subscriptions => ( @@ -21,7 +20,7 @@ const mapSubscriptionsToQueries = subscriptions => ( const defaultMapFirebaseToProps = (props, ref, firebaseApp) => ({ firebaseApp }) -export default (mapFirebaseToProps = defaultMapFirebaseToProps) => { +export default (mapFirebaseToProps = defaultMapFirebaseToProps, mergeProps = defaultMergeProps) => { const mapFirebase = ( isFunction(mapFirebaseToProps) ? mapFirebaseToProps : () => mapFirebaseToProps ) @@ -132,7 +131,7 @@ export default (mapFirebaseToProps = defaultMapFirebaseToProps) => { const firebaseProps = mapFirebase(this.props, this.ref, this.firebaseApp) const actionProps = pickBy(firebaseProps, isFunction) const subscriptionProps = this.state.subscriptionsState - const props = mergeProps(actionProps, subscriptionProps, this.props) + const props = mergeProps(this.props, { ...actionProps, ...subscriptionProps }) return createElement(WrappedComponent, props) } diff --git a/src/tests/connect-test.js b/src/tests/connect-test.js index 6216245..cc33910 100644 --- a/src/tests/connect-test.js +++ b/src/tests/connect-test.js @@ -9,14 +9,14 @@ import { findRenderedComponentWithType, renderIntoDocument } from 'react-addons- import connect from '../connect' import { createMockApp, createMockSnapshot } from './helpers' -const renderStub = (mapFirebaseToProps, firebaseApp, props) => { +const renderStub = ({ mapFirebaseToProps, mergeProps, firebaseApp }, props) => { class Passthrough extends Component { // eslint-disable-line react/prefer-stateless-function render() { return
} } - const WrappedComponent = connect(mapFirebaseToProps)(Passthrough) + const WrappedComponent = connect(mapFirebaseToProps, mergeProps)(Passthrough) const container = renderIntoDocument() const stub = findRenderedComponentWithType(container, Passthrough) @@ -33,7 +33,7 @@ test('Should throw if no initialized Firebase app instance was found', assert => // Default app instance assert.doesNotThrow(() => { const defaultApp = firebase.initializeApp({}) - const WrappedComponent = connect()('div') + const WrappedComponent = connect()(() =>
) const container = renderIntoDocument() const stub = findRenderedComponentWithType(container, WrappedComponent) @@ -66,16 +66,59 @@ test('Should subscribe to a single path', assert => { }, on: (event, callback) => { assert.equal(event, 'value') - callback(createMockSnapshot('foo value')) + callback(createMockSnapshot({ bar: 'bar' })) }, } const mapFirebaseToProps = () => ({ foo: 'foo' }) const firebaseApp = createMockApp(mockDatabase) - const { state, props } = renderStub(mapFirebaseToProps, firebaseApp) + const { state, props } = renderStub({ mapFirebaseToProps, firebaseApp }) - assert.deepEqual(state, { foo: 'foo value' }) - assert.equal(props.foo, 'foo value') + assert.deepEqual(state, { foo: { bar: 'bar' } }) + assert.deepEqual(props.foo, { bar: 'bar' }) + assert.end() +}) + +test('Should return null if a subscribed path does not exist', assert => { + const mockDatabase = { + ref: path => { + assert.equal(path, 'foo') + + return mockDatabase + }, + on: (event, callback) => { + assert.equal(event, 'value') + callback(createMockSnapshot(null)) + }, + } + + const mapFirebaseToProps = () => ({ foo: 'foo' }) + const firebaseApp = createMockApp(mockDatabase) + const { state, props } = renderStub({ mapFirebaseToProps, firebaseApp }) + + assert.deepEqual(state, { foo: null }) + assert.equal(props.foo, null) + assert.end() +}) + +test('Should not pass unresolved subscriptions from result of mapFirebaseToProps', assert => { + const mockDatabase = { + ref: path => { + assert.equal(path, 'foo') + + return mockDatabase + }, + on: event => { + assert.equal(event, 'value') + }, + } + + const mapFirebaseToProps = () => ({ foo: 'foo' }) + const firebaseApp = createMockApp(mockDatabase) + const first = renderStub({ mapFirebaseToProps, firebaseApp }) + + assert.equal(first.state, null) + assert.equal(first.props.foo, undefined) assert.end() }) @@ -112,7 +155,7 @@ test('Should subscribe to a query', assert => { }) const firebaseApp = createMockApp(mockDatabase) - const { state, props } = renderStub(mapFirebaseToProps, firebaseApp) + const { state, props } = renderStub({ mapFirebaseToProps, firebaseApp }) assert.deepEqual(state, { bar: 'bar value' }) assert.equal(props.bar, 'bar value') @@ -126,7 +169,7 @@ test('Should not subscribe to functions', assert => { }) const firebaseApp = createMockApp() - const { state, props } = renderStub(mapFirebaseToProps, firebaseApp) + const { state, props } = renderStub({ mapFirebaseToProps, firebaseApp }) assert.deepEqual(state, { foo: 'foo value' }) assert.equal(props.foo, 'foo value') @@ -152,7 +195,7 @@ test('Should unsubscribe when component unmounts', assert => { const mapFirebaseToProps = () => ({ baz: 'baz' }) const firebaseApp = createMockApp(mockDatabase) - const { container } = renderStub(mapFirebaseToProps, firebaseApp) + const { container } = renderStub({ mapFirebaseToProps, firebaseApp }) assert.notEqual(container.listeners.baz, undefined) unmountComponentAtNode(findDOMNode(container).parentNode) @@ -171,7 +214,7 @@ test('Should pass props, ref and firebaseApp to mapFirebaseToProps', assert => { } const firebaseApp = createMockApp() - const { props } = renderStub(mapFirebaseToProps, firebaseApp, { foo: 'foo prop' }) + const { props } = renderStub({ mapFirebaseToProps, firebaseApp }, { foo: 'foo prop' }) assert.equal(props.foo, 'foo value') assert.end() @@ -179,23 +222,35 @@ test('Should pass props, ref and firebaseApp to mapFirebaseToProps', assert => { test('Should update subscriptions when props change', assert => { const mapFirebaseToProps = props => ({ foo: props.foo, bar: props.bar }) - const firebaseApp = createMockApp() - const initial = renderStub(mapFirebaseToProps, firebaseApp, { foo: 'foo' }) + const stubOptions = { mapFirebaseToProps, firebaseApp } + + const initial = renderStub(stubOptions, { foo: 'foo' }) assert.equal(initial.props.foo, 'foo value') assert.equal(initial.props.bar, undefined) - const added = renderStub(mapFirebaseToProps, firebaseApp, { foo: 'foo', bar: 'bar' }) + const added = renderStub(stubOptions, { foo: 'foo', bar: 'bar' }) assert.equal(added.props.foo, 'foo value') assert.equal(added.props.bar, 'bar value') - const changed = renderStub(mapFirebaseToProps, firebaseApp, { foo: 'foo', bar: 'baz' }) + const changed = renderStub(stubOptions, { foo: 'foo', bar: 'baz' }) assert.equal(changed.props.foo, 'foo value') assert.equal(changed.props.bar, 'baz value') - const removed = renderStub(mapFirebaseToProps, firebaseApp, { bar: 'baz' }) + const removed = renderStub(stubOptions, { bar: 'baz' }) assert.equal(removed.props.foo, undefined) assert.equal(removed.props.bar, 'baz value') assert.end() }) + +test('Should use custom mergeProps function if provided', assert => { + const mapFirebaseToProps = props => ({ foo: props.foo }) + const mergeProps = () => ({ bar: 'bar merge props' }) + + const firebaseApp = createMockApp() + const { props } = renderStub({ mapFirebaseToProps, mergeProps, firebaseApp }, { foo: 'foo prop' }) + + assert.deepEqual(props, { bar: 'bar merge props' }) + assert.end() +}) diff --git a/src/tests/helpers.js b/src/tests/helpers.js index a0b4c07..200bcdc 100644 --- a/src/tests/helpers.js +++ b/src/tests/helpers.js @@ -1,6 +1,6 @@ -export const createMockSnapshot = (val, ...otherProps) => ({ +export const createMockSnapshot = (value, ...otherProps) => ({ ...otherProps, - val: () => val, + val: () => value, }) const defaultDatabaseProps = {