diff --git a/.eslintignore b/.eslintignore index 741fce2d2..d1a2d76c8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ -example/** +examples/** coverage/** node_modules/** *.spec.js diff --git a/examples/snippets/decorators/App.js b/examples/snippets/decorators/App.js index d9c67ff63..d50960f99 100644 --- a/examples/snippets/decorators/App.js +++ b/examples/snippets/decorators/App.js @@ -4,10 +4,10 @@ import TodoItem from './TodoItem' // redux/firebase import { connect } from 'react-redux' -import { firebase, helpers } from 'react-redux-firebase' +import { firebaseConnect, helpers } from 'react-redux-firebase' const { isLoaded, isEmpty, pathToJS, dataToJS } = helpers -@firebase([ +@firebaseConnect([ '/todos' ]) @connect( diff --git a/examples/snippets/populates/App.js b/examples/snippets/populates/App.js new file mode 100644 index 000000000..2ac3c6213 --- /dev/null +++ b/examples/snippets/populates/App.js @@ -0,0 +1,57 @@ +import React, { PropTypes, Component } from 'react' +import { map } from 'lodash' +import TodoItem from './TodoItem' + +// redux/firebase +import { connect } from 'react-redux' +import { firebaseConnect, helpers } from 'react-redux-firebase' +const { populatedDataToJS, isLoaded, pathToJS, dataToJS } = helpers +const populates = [ + { child: 'owner', root: 'users' }, + // or if you want a param of the populate child such as user's display name + // { child: 'owner', root: 'users', childParam: 'displayName' } +] + +@firebaseConnect([ + { path: '/projects', populates }, +]) +@connect( + ({firebase}) => ({ + projects: populatedDataToJS(firebase, '/projects', populates), + }) +) +export default class App extends Component { + static propTypes = { + projects: PropTypes.shape({ + name: PropTypes.string, + owner: PropTypes.object // string if using childParam: 'displayName' + }), + firebase: PropTypes.shape({ + set: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired + }) + } + render () { + const { firebase, projects } = this.props + + const projectsList = (!isLoaded(projects)) + ? 'Loading' + : (isEmpty(projects)) + ? 'Todo list is empty' + : map(projects, (todo, id) => ( +
+ Name: {project.name} + Owner: { owner.displayName || owner } +
+ )) + return ( +
+

react-redux-firebase populate snippet

+
+

Projects List

+ {projectsList} +
+
+ ) + } +} diff --git a/package.json b/package.json index 3f4260d20..d100a15ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-redux-firebase", - "version": "1.2.2", + "version": "1.2.3", "description": "Redux integration for Firebase. Comes with a Higher Order Component for use with React.", "main": "dist/index.js", "module": "src/index.js", diff --git a/src/helpers.js b/src/helpers.js index 890ef027e..3bc764d0f 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -184,16 +184,19 @@ export const dataToJS = (data, path, notSetValue) => { * @param {Object} list - Path of parameter to load * @param {Object} populate - Object with population settings */ -export const buildChildList = (data, list, populate) => +export const buildChildList = (data, list, p) => mapValues(list, (val, key) => { let getKey = val // Handle key: true lists if (val === true) { getKey = key } + const pathString = p.childParam + ? `${p.root}/${getKey}/${p.childParam}` + : `${p.root}/${getKey}` // Set to child under key if populate child exists - if (dataToJS(data, `${populate.root}/${getKey}`)) { - return dataToJS(data, `${populate.root}/${getKey}`) + if (dataToJS(data, pathString)) { + return dataToJS(data, pathString) } // Populate child does not exist return val === true ? val : getKey @@ -230,7 +233,7 @@ export const populatedDataToJS = (data, path, populates, notSetValue) => { } // Handle undefined child if (!dataToJS(data, path, notSetValue)) { - return undefined + return dataToJS(data, path, notSetValue) } const populateObjs = getPopulateObjs(populates) // reduce array of populates to object of combined populated data @@ -238,6 +241,20 @@ export const populatedDataToJS = (data, path, populates, notSetValue) => { map(populateObjs, (p, obj) => { // single item with iterable child if (dataToJS(data, path)[p.child]) { + // populate child is key + if (isString(dataToJS(data, path)[p.child])) { + const pathString = p.childParam + ? `${p.root}/${dataToJS(data, path)[p.child]}/${p.childParam}` + : `${p.root}/${dataToJS(data, path)[p.child]}` + if (dataToJS(data, pathString)) { + return { + ...dataToJS(data, path), + [p.child]: dataToJS(data, pathString) + } + } + // matching child does not exist + return dataToJS(data, path) + } return { ...dataToJS(data, path), [p.child]: buildChildList(data, dataToJS(data, path)[p.child], p) @@ -251,10 +268,13 @@ export const populatedDataToJS = (data, path, populates, notSetValue) => { } // populate child is key if (isString(child[p.child])) { - if (dataToJS(data, `${p.root}/${child[p.child]}`)) { + const pathString = p.childParam + ? `${p.root}/${child[p.child]}/${p.childParam}` + : `${p.root}/${child[p.child]}` + if (dataToJS(data, pathString)) { return { ...child, - [p.child]: dataToJS(data, `${p.root}/${child[p.child]}`) + [p.child]: dataToJS(data, pathString) } } // matching child does not exist diff --git a/src/utils/populate.js b/src/utils/populate.js index 4e639b524..9ab9b0125 100644 --- a/src/utils/populate.js +++ b/src/utils/populate.js @@ -77,34 +77,24 @@ export const getPopulateChild = (firebase, populate, id) => * @param {Object} populate - Object containing populate information * @param {Object} results - Object containing results of population from other populates */ -export const populateList = (firebase, originalData, p, results) => { - const mainChild = p.child.split('[]')[0] - const childParam = p.child.split('[]')[1] +export const populateList = (firebase, list, p, results) => { + // Handle root not being defined + if (!results[p.root]) { + set(results, p.root, {}) + } return Promise.all( - map(get(originalData, mainChild), (id, childKey) => { + map(list, (id, childKey) => { // handle list of keys const populateKey = id === true ? childKey : id return getPopulateChild( firebase, p, - childParam - ? get(id, childParam) // get child parameter if [] notation - : populateKey + populateKey ) .then(pc => { if (pc) { // write child to result object under root name if it is found - if (!childParam) { - return set(results, `${p.root}.${populateKey}`, pc) - } - // handle child param - return ({ - [childKey]: set( - id, - childParam, - Object.assign(pc, { key: get(id, childParam) }) - ) - }) + return set(results, `${p.root}.${populateKey}`, pc) } return results }) @@ -131,7 +121,9 @@ export const promisesForPopulate = (firebase, originalData, populatesIn) => { // Single parameter with list if (has(originalData, mainChild)) { - return promisesArray.push(populateList(firebase, originalData, p, results)) + return promisesArray.push( + populateList(firebase, originalData[mainChild], p, results) + ) } // Loop over each object in list forEach(originalData, (d, key) => { @@ -161,7 +153,7 @@ export const promisesForPopulate = (firebase, originalData, populatesIn) => { if (isArray(idOrList) || isObject(idOrList)) { // Create single promise that includes a promise for each child return promisesArray.push( - populateList(firebase, originalData, p, results) + populateList(firebase, idOrList, p, results) ) } }) diff --git a/test/unit/helpers.spec.js b/test/unit/helpers.spec.js index 34525d47e..652f827b2 100644 --- a/test/unit/helpers.spec.js +++ b/test/unit/helpers.spec.js @@ -127,48 +127,112 @@ describe('Helpers:', () => { .equal(exampleData.data[path]) }) - it('populates child', () => { - const path = 'projects' - const rootName = 'users' - const valName = 'CDF' - expect(helpers.populatedDataToJS(exampleState, path, [{ child: 'owner', root: rootName }])[valName].owner) - .to - .have - .property('displayName', 'scott') - }) + describe('single', () => { + describe('single param', () => { + it('populates value', () => { + const path = 'projects/CDF' + const rootName = 'users' + console.log('--------', helpers.populatedDataToJS(exampleState, path, [{ child: 'owner', root: rootName }]).owner) + expect(helpers.populatedDataToJS(exampleState, path, [{ child: 'owner', root: rootName }]).owner) + .to + .have + .property('displayName', 'scott') + }) + + it('populates childParam', () => { + const path = 'projects/CDF' + const rootName = 'users' + expect(helpers.populatedDataToJS(exampleState, path, [{ child: 'owner', root: rootName, childParam: 'displayName' }]).owner) + .to + .have + .equal('scott') + }) + it('keeps non-existant children', () => { + const path = 'projects/OKF' + const rootName = 'users' + expect(helpers.populatedDataToJS(exampleState, path, [{ child: 'owner', root: rootName }]).owner) + .to + .have + .equal('asdfasdf') + }) + }) + describe('list param', () => { + it('populates values', () => { + const path = 'projects/OKF' + const rootName = 'users' + const populates = [ + { child: 'collaborators', root: rootName }, + ] + const populatedData = helpers.populatedDataToJS(exampleState, path, populates) + expect(populatedData) + .to + .have + .deep + .property(`collaborators.ABC.displayName`, exampleData.data[rootName].ABC.displayName) + }) + }) - it('populates child list', () => { - const path = 'projects' - const rootName = 'users' - const valName = 'OKF' - const populates = [ - { child: 'collaborators', root: rootName }, - ] - const populatedData = helpers.populatedDataToJS(exampleState, path, populates) - expect(populatedData) - .to - .have - .deep - .property(`${valName}.collaborators.ABC.displayName`, exampleData.data[rootName].ABC.displayName) }) - it('handles non existant children', () => { - const path = 'projects' - const rootName = 'users' - const valName = 'OKF' - const populates = [ - { child: 'collaborators', root: rootName }, - ] - expect(helpers.populatedDataToJS(exampleState, path, populates)) - .to - .have - .deep - .property(`${valName}.collaborators.abc`, true) - expect(helpers.populatedDataToJS(exampleState, path, populates)) - .to - .have - .deep - .property(`${valName}.collaborators.ABC.displayName`, exampleData.data[rootName].ABC.displayName) + describe('list', () => { + + describe('single param', () => { + it('populates value', () => { + const path = 'projects' + const rootName = 'users' + const valName = 'CDF' + expect(helpers.populatedDataToJS(exampleState, path, [{ child: 'owner', root: rootName }])[valName].owner) + .to + .have + .property('displayName', 'scott') + }) + + it('populates childParam', () => { + const path = 'projects' + const rootName = 'users' + const valName = 'CDF' + expect(helpers.populatedDataToJS(exampleState, path, [{ child: 'owner', root: rootName, childParam: 'displayName' }])[valName].owner) + .to + .have + .equal('scott') + }) + }) + + describe('list param', () => { + it('populates values', () => { + const path = 'projects' + const rootName = 'users' + const valName = 'OKF' + const populates = [ + { child: 'collaborators', root: rootName }, + ] + const populatedData = helpers.populatedDataToJS(exampleState, path, populates) + expect(populatedData) + .to + .have + .deep + .property(`${valName}.collaborators.ABC.displayName`, exampleData.data[rootName].ABC.displayName) + }) + + it('keeps non-existant children', () => { + const path = 'projects' + const rootName = 'users' + const valName = 'OKF' + const populates = [ + { child: 'collaborators', root: rootName }, + ] + expect(helpers.populatedDataToJS(exampleState, path, populates)) + .to + .have + .deep + .property(`${valName}.collaborators.abc`, true) + expect(helpers.populatedDataToJS(exampleState, path, populates)) + .to + .have + .deep + .property(`${valName}.collaborators.ABC.displayName`, exampleData.data[rootName].ABC.displayName) + }) + }) }) it('populates multiple children', () => { @@ -179,6 +243,7 @@ describe('Helpers:', () => { { child: 'owner', root: rootName }, { child: 'collaborators', root: rootName }, ] + // TODO: Test both children are populated expect(helpers.populatedDataToJS(exampleState, path, populates)) .to .have @@ -223,6 +288,10 @@ describe('Helpers:', () => { it('returns true when is loaded', () => { expect(helpers.isLoaded('some')).to.be.true }) + + it('returns false when on argument is not loaded', () => { + expect(helpers.isLoaded(undefined, {})).to.be.false + }) }) describe('isEmpty', () => { diff --git a/test/unit/utils/populate.spec.js b/test/unit/utils/populate.spec.js index fbddf0074..4a8109167 100644 --- a/test/unit/utils/populate.spec.js +++ b/test/unit/utils/populate.spec.js @@ -3,6 +3,7 @@ import { getPopulateObj, getPopulates, getPopulateChild, + getPopulateObjs, promisesForPopulate } from '../../../src/utils/populate' @@ -11,15 +12,30 @@ describe('Utils: Populate', () => { it('returns object with child and root', () => { expect(getPopulateObj('some:value')).to.have.keys('child', 'root') }) + it('returns object if passed', () => { + const inputObj = { child: 'some', root: 'some' } + expect(getPopulateObj(inputObj)).to.equal(inputObj) + }) + }) + + describe('getPopulateObj', () => { + it('returns object with child and root', () => { + expect(getPopulateObjs(['some:value'])[0]).to.have.keys('child', 'root') + }) + it('handles basic populates', () => { + const inputString = 'populate=uid:users' + expect(getPopulateObjs(inputString)).to.equal(inputString) + }) }) describe('getPopulates', () => { - it('no populates', () => { + it('handles no populates', () => { expect(getPopulates(['orderByPriority'])) }) - it('basic populates', () => { + it('handles basic populates', () => { expect(getPopulates(['populate=uid:users'])) }) + }) describe('getPopulateChild', () => { @@ -30,22 +46,54 @@ describe('Utils: Populate', () => { }) describe('promisesForPopulate', () => { - it('none existant child', () => - promisesForPopulate(Firebase, {uid: '123123'}, [{child: 'random', root: 'users'}]) + it('handles non-existant single child', () => + promisesForPopulate(Firebase, { uid: '123123' }, [{child: 'random', root: 'users'}]) .then((v) => { expect(JSON.stringify(v)).to.equal(JSON.stringify({})) }) ) - it('string populate', () => + + it('populates single property containing a list', () => + promisesForPopulate(Firebase, { collaborators: { 'Iq5b0qK2NtgggT6U3bU6iZRGyma2': true, '123': true } }, [{child: 'collaborators', root: 'users'}]) + .then((v) => { + expect(v).to.exist + expect(v).to.have.keys('users') + expect(v.users['Iq5b0qK2NtgggT6U3bU6iZRGyma2']).to.be.an.object + }) + ) + + it('populates list with single property populate', () => promisesForPopulate(Firebase, { 1: { owner: 'Iq5b0qK2NtgggT6U3bU6iZRGyma2' } }, [{child: 'owner', root: 'users'}]) .then((v) => { expect(v).to.have.keys('users') + expect(v.users['Iq5b0qK2NtgggT6U3bU6iZRGyma2']).to.be.an.object }) ) - it('array populate', () => + + it('populates list with property containing array property', () => promisesForPopulate(Firebase, { 1: { collaborators: ['Iq5b0qK2NtgggT6U3bU6iZRGyma2', '123'] } }, [{child: 'collaborators', root: 'users'}]) .then((v) => { expect(v).to.exist + expect(v).to.have.keys('users') + expect(v.users['Iq5b0qK2NtgggT6U3bU6iZRGyma2']).to.be.an.object + }) + ) + + it('populates list with property containing firebase list', () => + promisesForPopulate(Firebase, { 1: { collaborators: { 'Iq5b0qK2NtgggT6U3bU6iZRGyma2': true, '123': true } } }, [{child: 'collaborators', root: 'users'}]) + .then((v) => { + expect(v).to.exist + expect(v).to.have.keys('users') + expect(v.users['Iq5b0qK2NtgggT6U3bU6iZRGyma2']).to.be.an.object + }) + ) + + it('populates list with property containing invalid child id', () => + promisesForPopulate(Firebase, { 1: { collaborators: ['1111', '123'] } }, [{child: 'collaborators', root: 'users'}]) + .then((v) => { + expect(v).to.exist + expect(v.users).to.have.keys('123') // sets valid child + expect(v.users).to.not.have.keys('111') // does not set invalid child }) ) })