diff --git a/.gitignore b/.gitignore index 0ba5fdbb0..0f77d84c9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,9 @@ node_modules examples/**/node_modules dist coverage +_book # Logs *.log .DS_Store -**/**/.DS_Store diff --git a/CNAME b/CNAME new file mode 100644 index 000000000..596df7f4f --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +react-redux-firebase.com diff --git a/LICENSE.md b/LICENSE.md index 5d066e015..470e59e73 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Prescott Prue +Copyright (c) 2016-present Prescott Prue Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 44ca6acfa..50b2b9a91 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,10 @@ View deployed version of [Material Example](https://github.com/prescottprue/react-redux-firebase/tree/master/examples/complete/material) [here](https://redux-firebasev3.firebaseapp.com/) - ## Features - Integrated into redux - Support for updating and nested props -- [Population capability](https://prescottprue.gitbooks.io/react-redux-firebase/content/populate.html) (similar to mongoose's `populate` or SQL's `JOIN`) +- [Population capability](http://react-redux-firebase.com/docs/populate) (similar to mongoose's `populate` or SQL's `JOIN`) - Out of the box support for authentication (with auto load user profile) - Firebase Storage Support - Support small data ( using `value` ) or large datasets ( using `child_added`, `child_removed`, `child_changed` ) @@ -120,7 +119,7 @@ const { isLoaded, isEmpty, dataToJS } = helpers todos: dataToJS(firebase, '/todos'), }) ) -class Todos extends Component { +export default class Todos extends Component { static propTypes = { todos: PropTypes.object, firebase: PropTypes.object @@ -161,7 +160,6 @@ class Todos extends Component { ) } } -export default Todos ``` Alternatively, if you choose not to use decorators: @@ -179,8 +177,14 @@ export default connect( ``` -## [API](https://prescottprue.gitbooks.io/react-redux-firebase/content/) -See [API Docs](https://prescottprue.gitbooks.io/react-redux-firebase/content/) +## [Documentation](http://react-redux-firebase.com) +See [react-redux-firebase.com](http://react-redux-firebase.com) + +* [Getting Started](http://react-redux-firebase.com/docs/getting_started) +* [Auth](http://react-redux-firebase.com/docs/auth) +* [Queries](http://react-redux-firebase.com/docs/queries) +* [Populate](http://react-redux-firebase.com/docs/populate) +* [API Reference](http://react-redux-firebase.com/docs/api) ## [Examples](examples) @@ -200,7 +204,11 @@ A simple example that was created using [create-react-app](https://github.com/fa #### [Material App Example](examples/complete/material) -An example that user Material UI built on top of the output of [create-react-app](https://github.com/facebookincubator/create-react-app)'s eject command. Shows a list of todo items and allows you to add to them. This is what is deployed to [react-redux-firebase.firebaseapp.com](https://react-redux-firebase.firebaseapp.com/). +An example that user Material UI built on top of the output of [create-react-app](https://github.com/facebookincubator/create-react-app)'s eject command. Shows a list of todo items and allows you to add to them. This is what is deployed to [redux-firebasev3.firebaseapp.com](https://redux-firebasev3.firebaseapp.com/). + +## Discussion + +Join the [redux-firebase gitter](https://gitter.im/redux-firebase/Lobby). ## Using with `redux-thunk` If you are using `redux-thunk`, make sure to set up your thunk middleware using it's redux-thunk's `withExtraArgument` method so that firebase is available within your actions. Here is an example `createStore` function that adds `getFirebase` as third argument along with a thunk that uses it: @@ -214,7 +222,10 @@ import { reactReduxFirebase, getFirebase } from 'react-redux-firebase'; import makeRootReducer from './reducers'; const fbConfig = {} // your firebase config - +const config = { + userProfile: 'users', + enableLogging: false +} const store = createStore( makeRootReducer(), initialState, @@ -222,7 +233,7 @@ const store = createStore( applyMiddleware([ thunk.withExtraArgument(getFirebase) // Pass getFirebase function as extra argument ]), - reactReduxFirebase(fbConfig, { userProfile: 'users', enableLogging: false }) + reactReduxFirebase(fbConfig, ) ) ); @@ -273,17 +284,17 @@ const somethingEpic = (action$, store, getFirebase) => 1. How is this different than [`redux-react-firebase`](https://github.com/tiberiuc/redux-react-firebase)? This library was actually originally forked from redux-react-firebase, but adds extended functionality such as: - * [populate functionality](https://prescottprue.gitbooks.io/react-redux-firebase/content/populate.html) (similar to mongoDB or SQL JOIN) - * [`profileDecorator`](https://prescottprue.gitbooks.io/react-redux-firebase/content/config.html) - change format of profile stored on Firebase - * [`getFirebase`](https://prescottprue.gitbooks.io/react-redux-firebase/content/thunks.html) - access to firebase instance that fires actions when methods are called - * [integrations](https://prescottprue.gitbooks.io/react-redux-firebase/content/thunks.html) for [`redux-thunk`](https://github.com/gaearon/redux-thunk) and [`redux-observable`](https://redux-observable.js.org) - using `getFirebase` - * [access to firebase's `storage`](https://prescottprue.gitbooks.io/react-redux-firebase/content/storage.html) method` + * [populate functionality](http://react-redux-firebase.com/docs/populate) (similar to mongoDB or SQL JOIN) + * [`profileDecorator`](http://react-redux-firebase.com/docs/config) - change format of profile stored on Firebase + * [`getFirebase`](http://react-redux-firebase.com/docs/thunks) - access to firebase instance that fires actions when methods are called + * [integrations](http://react-redux-firebase.com/docs/thunks) for [`redux-thunk`](https://github.com/gaearon/redux-thunk) and [`redux-observable`](https://redux-observable.js.org) - using `getFirebase` + * [access to firebase's `storage`](http://react-redux-firebase.com/docs/storage) method` * `uniqueSet` method helper for only setting if location doesn't already exist * Object or String notation for paths (`[{ path: '/todos' }]` equivalent to `['/todos']`) - * Action Types and other Constants are exposed for external usage (such as `redux-observable`) + * Action Types and other Constants are exposed for external usage (such as with `redux-observable`) #### Well why not combine? - I have been talking to the author of [redux-react-firebase]() about combining, but we are not sure that the users of both want that at this point. Join us on [the redux-firebase gitter](https://gitter.im/redux-firebase/Lobby) if you haven't already since a ton of this type of discussion goes on there. + I have been talking to the author of [redux-react-firebase](https://github.com/tiberiuc/redux-react-firebase) about combining, but we are not sure that the users of both want that at this point. Join us on the [redux-firebase gitter](https://gitter.im/redux-firebase/Lobby) if you haven't already since a ton of this type of discussion goes on there. **Bottom line:** The author of redux-react-firebase was absent when functionality was needed by me and others, so this library was created. diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 000000000..b2030d1e1 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,20 @@ +# Summary + +* [Read Me](/README.md) +* [Getting Started](/docs/getting_started.md) +* [Auth](/docs/auth.md) +* [Queries](/docs/queries.md) +* [Populate](/docs/populate.md) +* [Storage](/docs/storage.md) +* [Recipes](/docs/recipes/README.md) + * [Upload](/docs/recipes/upload.md) + * [Actions](/docs/recipes/actions.md) + * [Thunks](/docs/recipes/thunks.md) +* [API Reference](/docs/api/README.md) + * [constants](/docs/api/constants.md) + * [firebaseConnect](/docs/api/connect.md) + * [firebaseStateReducer](/docs/api/reducer.md) + * [reactReduxFirebase](/docs/api/compose.md) + * [helpers](/docs/api/helpers.md) +* [Roadmap](/docs/roadmap.md) +* [Contributing](/docs/contributing.md) diff --git a/bin/api-docs-generate.js b/bin/api-docs-generate.js new file mode 100644 index 000000000..d8ecbce89 --- /dev/null +++ b/bin/api-docs-generate.js @@ -0,0 +1,47 @@ +const exec = require('child_process').exec +const files = [ + { + src: 'connect.js', + dest: 'connect.md' + }, + { + src: 'compose.js', + dest: 'compose.md' + }, + { + src: 'helpers.js', + dest: 'helpers.md' + }, + { + src: 'reducer.js', + dest: 'reducer.md' + }, + { + src: 'constants.js', + dest: 'constants.md' + } +] +const pathToDocumentationJs = 'node_modules/documentation/bin/documentation.js' + +const generateDocForFile = (file) => { + return new Promise((resolve, reject) => { + exec(`${pathToDocumentationJs} build src/${file.src} -f md -o docs/api/${file.dest} --shallow`, (error, stdout) => { + if (error !== null) { + return reject(error) + } + resolve(stdout) + }) + }) +} + +(function () { + files.forEach(file => { + generateDocForFile(file) + .then((res) => { + console.log('Successfully generated', file) // eslint-disable-line no-console + }) + .catch((err) => { + console.log('error generating doc: ', err) // eslint-disable-line no-console + }) + }) +})() diff --git a/book.json b/book.json new file mode 100644 index 000000000..357293a8b --- /dev/null +++ b/book.json @@ -0,0 +1,19 @@ +{ + "gitbook": ">=3.2.1", + "title": "React Redux Firebase", + "plugins": ["edit-link", "prism", "-highlight", "github", "anchorjs"], + "pluginsConfig": { + "edit-link": { + "base": "https://github.com/prescottprue/react-redux-firebase/tree/master", + "label": "Edit This Page" + }, + "github": { + "url": "https://github.com/prescottprue/react-redux-firebase/" + }, + "theme-default": { + "styles": { + "website": "build/gitbook.css" + } + } + } +} diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md new file mode 100644 index 000000000..479663615 --- /dev/null +++ b/docs/GLOSSARY.md @@ -0,0 +1,4 @@ +# Glossary + +## ## `profileDecorator` + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..b2030d1e1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ +# Summary + +* [Read Me](/README.md) +* [Getting Started](/docs/getting_started.md) +* [Auth](/docs/auth.md) +* [Queries](/docs/queries.md) +* [Populate](/docs/populate.md) +* [Storage](/docs/storage.md) +* [Recipes](/docs/recipes/README.md) + * [Upload](/docs/recipes/upload.md) + * [Actions](/docs/recipes/actions.md) + * [Thunks](/docs/recipes/thunks.md) +* [API Reference](/docs/api/README.md) + * [constants](/docs/api/constants.md) + * [firebaseConnect](/docs/api/connect.md) + * [firebaseStateReducer](/docs/api/reducer.md) + * [reactReduxFirebase](/docs/api/compose.md) + * [helpers](/docs/api/helpers.md) +* [Roadmap](/docs/roadmap.md) +* [Contributing](/docs/contributing.md) diff --git a/docs/api/README.md b/docs/api/README.md new file mode 100644 index 000000000..b1900ff17 --- /dev/null +++ b/docs/api/README.md @@ -0,0 +1,25 @@ +# API Reference + +Just like [redux](http://redux.js.org/docs/api/index.html), the react-redux-firebase API surface is small. + +## Top-Level Exports +* [firebaseConnect](/docs/api/connect.md) +* [firebaseStateReducer](/docs/api/reducer.md) +* [reactReduxFirebase](/docs/api/compose.md) +* [constants](/docs/api/constants.md) +* [actionTypes](/docs/api/constants.md) +* [helpers](/docs/api/helpers.md) + +## Importing + +Every function described above is a top-level export. You can import any of them like this: + +### ES6 +```js +import { firebaseConnect } from 'react-redux-firebase' +``` + +### ES5 (CommonJS) +```js +var firebaseConnect = require('react-redux-firebase').firebaseConnect +``` diff --git a/docs/api/compose.md b/docs/api/compose.md new file mode 100644 index 000000000..cacb67d80 --- /dev/null +++ b/docs/api/compose.md @@ -0,0 +1,51 @@ + + +# reactReduxFirebase + +Middleware that handles configuration (placed in redux's `compose` call) + +**Parameters** + +- `fbConfig` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Object containing Firebase config including databaseURL + - `fbConfig.apiKey` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Firebase apiKey + - `fbConfig.authDomain` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Firebase auth domain + - `fbConfig.databaseURL` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Firebase database url + - `fbConfig.storageBucket` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Firebase storage bucket +- `config` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Containing react-redux-firebase specific config such as userProfile + - `config.userProfile` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Location on firebase to store user profiles + - `config.enableLogging` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Location on firebase to store user profiles. default: `false` + - `config.profileDecorator` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** Location on firebase to store user profiles. default: `false` + - `config.updateProfileOnLogin` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Whether or not to update profile when logging in. default: `false` + - `config.profileParamsToPopulate` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Whether or not to update profile when logging in. default: `false` + +**Examples** + +_Data_ + +```javascript +import { createStore, compose } from 'redux' +import { reactReduxFirebase } from 'react-redux-firebase' + +// Firebase config +const fbConfig = { + apiKey: '', + authDomain: '', + databaseURL: '', + storageBucket: '' +} + +// React Redux Firebase Config +const config = { + userProfile: 'users' +} + +// Add react-redux-firebase to compose +const createStoreWithFirebase = compose( + reactReduxFirebase(fbConfig, config), +)(createStore) + +// Use Function later to create store +const store = createStoreWithFirebase(rootReducer, initialState) +``` + +Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** That accepts a component a returns a wrapped version of component diff --git a/docs/api/connect.md b/docs/api/connect.md new file mode 100644 index 000000000..b5cde4997 --- /dev/null +++ b/docs/api/connect.md @@ -0,0 +1,56 @@ + + +# firebaseConnect + +**Extends React.Component** + +Higher Order Component that automatically listens/unListens +to provided firebase paths using React's Lifecycle hooks. + +**Parameters** + +- `watchArray` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** Array of objects or strings for paths to sync from Firebase + +**Examples** + +_Basic_ + +```javascript +// this.props.firebase set on App component as firebase object with helpers +import { firebaseConnect } from 'react-redux-firebase' +export default firebaseConnect()(App) +``` + +_Paths_ + +```javascript +import { connect } from 'react-redux' +import { firebaseConnect, helpers } from 'react-redux-firebase' +const { pathToJS } = helpers + +// pass todos list from redux as this.props.todosList +export default connect(({ firebase }) => ({ + profile: pathToJS(firebase, 'profile'), + auth: pathToJS(firebase, 'auth') +}))(App) +``` + +_Data_ + +```javascript +import { connect } from 'react-redux' +import { firebaseConnect, helpers } from 'react-redux-firebase' +const { dataToJS } = helpers + +// sync /todos from firebase into redux +const fbWrapped = firebaseConnect([ + 'todos' +])(App) + +// pass todos list from redux as this.props.todosList +export default connect(({ firebase }) => ({ + todosList: dataToJS(firebase, 'todos') +}))(fbWrapped) +``` + +Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** that accepts a component to wrap and returns the wrapped component diff --git a/docs/api/constants.md b/docs/api/constants.md new file mode 100644 index 000000000..9ec939d08 --- /dev/null +++ b/docs/api/constants.md @@ -0,0 +1,28 @@ + + +# actionsPrefix + +Prefix for all actions within library + +**Examples** + +```javascript +import { constants } from 'react-redux-firebase' +constants.actionsPrefix === '@@reactReduxFirebase' // true +``` + +# actionTypes + +Object containing all action types + +**Examples** + +```javascript +import { actionTypes } from 'react-redux-firebase' +actionTypes.SET === '@@reactReduxFirebase/SET' // true +``` + +```javascript +import { constants } from 'react-redux-firebase' +constants.actionTypes.SET === '@@reactReduxFirebase/SET' // true +``` diff --git a/docs/api/helpers.md b/docs/api/helpers.md new file mode 100644 index 000000000..236adc2b1 --- /dev/null +++ b/docs/api/helpers.md @@ -0,0 +1,137 @@ + + +# isLoaded + +Detect whether items are loaded yet or not + +**Parameters** + +- `item` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Item to check loaded status of. A comma seperated list is also acceptable. + +**Examples** + +```javascript +import React, { Component, PropTypes } from 'react' +import { connect } from 'react-redux' +import { firebaseConnect, helpers } from 'react-redux-firebase' +const { isLoaded, dataToJS } = helpers +``` + +Returns **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Whether or not item is loaded + +# isEmpty + +Detect whether items are empty or not + +**Parameters** + +- `item` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Item to check loaded status of. A comma seperated list is also acceptable. +- `data` + +**Examples** + +```javascript +import React, { Component, PropTypes } from 'react' +import { connect } from 'react-redux' +import { firebaseConnect, helpers } from 'react-redux-firebase' +const { isEmpty, dataToJS } = helpers +``` + +Returns **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Whether or not item is empty + +# toJS + +Convert Immutable Map to a Javascript object + +**Parameters** + +- `data` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Immutable Map to be converted to JS object (state.firebase) + +Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** data - Javascript version of Immutable Map + +Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Data located at path within Immutable Map + +# pathToJS + +Convert parameter from Immutable Map to a Javascript object + +**Parameters** + +- `firebase` **[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)** Immutable Map to be converted to JS object (state.firebase) +- `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Path from state.firebase to convert to JS object +- `notSetValue` **([Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean))** Value to use if data is not available +- `data` + +**Examples** + +_Basic_ + +```javascript +import { connect } from 'react-redux' +import { firebaseConnect, helpers } from 'react-redux-firebase' +const { pathToJS } = helpers +const fbWrapped = firebaseConnect()(App) +export default connect(({ firebase }) => ({ + profile: pathToJS(firebase, 'profile'), + auth: pathToJS(firebase, 'auth') +}))(fbWrapped) +``` + +Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Data located at path within Immutable Map + +# dataToJS + +Convert parameter under "data" path of Immutable Map to a Javascript object + +**Parameters** + +- `firebase` **[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)** Immutable Map to be converted to JS object (state.firebase) +- `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Path of parameter to load +- `notSetValue` **([Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean))** Value to return if value is not found +- `data` + +**Examples** + +_Basic_ + +```javascript +import { connect } from 'react-redux' +import { firebaseConnect, helpers } from 'react-redux-firebase' +const { dataToJS } = helpers + +const fbWrapped = firebaseConnect(['/todos'])(App) + +export default connect(({ firebase }) => ({ + // this.props.todos loaded from state.firebase.data.todos + todos: dataToJS(firebase, 'todos') +}))(fbWrapped) +``` + +Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Data located at path within Immutable Map + +# customToJS + +Load custom object from within store + +**Parameters** + +- `firebase` **[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)** Immutable Map to be converted to JS object (state.firebase) +- `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Path of parameter to load +- `customPath` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Part of store from which to load +- `notSetValue` **([Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean))** Value to return if value is not found +- `data` +- `custom` + +Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Data located at path within Immutable Map + +# snapshotToJS + +Convert Immutable Map to a Javascript object + +**Parameters** + +- `snapshot` **[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)** Snapshot from store +- `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Path of snapshot to load +- `notSetValue` **([Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean))** Value to return if value is not found + +Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Data located at path within Immutable Map diff --git a/docs/api/reducer.md b/docs/api/reducer.md new file mode 100644 index 000000000..849e29f5b --- /dev/null +++ b/docs/api/reducer.md @@ -0,0 +1,17 @@ + + +# firebaseStateReducer + +Reducer for react redux firebase. This function is called +automatically by redux every time an action is fired. Based on which action +is called and its payload, the reducer will update redux state with relevant +changes. + +**Parameters** + +- `state` **[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)** Current Redux State +- `action` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Action which will modify state + - `action.type` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of Action being called + - `action.data` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of Action which will modify state + +Returns **[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)** State diff --git a/docs/auth.md b/docs/auth.md new file mode 100644 index 000000000..1e5fe9cc2 --- /dev/null +++ b/docs/auth.md @@ -0,0 +1,180 @@ +# Authentication Methods + +Authentication data is attached to `auth`, and errors are attached to `authError`. You can get them within components like so: + +```js +import { connect } from 'react-redux' +import { helpers } from 'react-redux-firebase' +const { pathToJS } = helpers +@connect( + // Map state to props + ({ firebase }) => ({ + authError: pathToJS(firebase, 'authError'), + auth: pathToJS(firebase, 'auth'), + profile: pathToJS(firebase, 'profile') + }) +) +``` + +#### NOTE +All examples below assume you have wrapped your component using `firebaseConnect`. This will make `this.props.firebase` available within your component: + +###### Decorators + +```js +import React, { Component, PropTypes } from 'react' +import { firebaseConnect } from 'react-redux-firebase' + +@firebaseConnect() +export default class SomeComponent extends Component { + render() { + // this.props.firebase contains API + } +} +``` + +###### No Decorators + +```js +import React, { Component, PropTypes } from 'react' +import { firebaseConnect } from 'react-redux-firebase' + +class SomeComponent extends Component { + render() { + // this.props.firebase contains API + } +} +export default firebaseConnect()(SomeComponent) +``` + + +## `login(credentials)` + +##### Parameters + + * `credentials` ([**String**]() | [**Object**]()) + * [**String**]() - `ref.authWithCustomToken(credentials)` is used + * [**Object**]() - cases: + * email and password (runs `ref.authWithPassword(credentials)`) : + ```js + { + email: String + password: String + } + ``` + * provider (runs `ref.authWithOAuthPopup(provider)` or `ref.authWithOAuthRedirect(provider)`) : + ```js + { + provider: "facebook | google | twitter", + type: "popup | redirect", // redirect is default + } + ``` + * provider and token (runs `ref.authWithOAuthToken(provider, token)`) : + ```js + { + provider: "facebook | google | twitter", + token : String + } + ``` + + +##### Returns +[**Promise**]() with authData in case of success or the error otherwise. + +##### Examples + + *Email* +```js +// Call with info +this.props.firebase.login({ + email: 'test@test.com', + password: 'testest1' +}) +``` + + *OAuth Provider Redirect* +```js + // Call with info + this.props.firebase.login({ + provider: 'google' + }) + ``` + + *OAuth Provider Popup* +```js +// Call with info +this.props.firebase.login({ + provider: 'google', + type: 'popup' +}) +``` + + *Token* +```js +// Call with info +this.props.firebase.login('someJWTAuthToken') +``` + +## `createUser(credentials, profile)` + +Similar to Firebase's `ref.createUser(credentials)` but with support for automatic profile setup (based on your userProfile config). + +##### Parameters + +* `credentials` [**Object**]() + * `credentials.email` [**String**]() - User's email + * `credentials.password` [**String**]() - User's password + +* `profile` [**Object**]() + * `profile.username` [**String**]() + +##### Examples +```js +const createNewUser = ({ email, password, username }) => { + this.props.firebase.createUser( + { email, password }, + { username, email } + ) +} + +// Call with info +createNewUser({ + email: 'test@test.com', + password: 'testest1', + username: 'tester' +}) +``` + +##### Returns +[**Promise**]() with `userData` + +## `logout()` +Logout from Firebase and delete all data from the store (`state.firebase.data` and `state.firebase.auth` are set to `null`). + +##### Examples + +```js +// logout and remove profile and auth from state +firebase.logout() +``` + +## `resetPassword(credentials)` +Calls Firebase's `ref.resetPassword(credentials)` then adds the output into redux state under `state.firebase.authError` + +##### Examples + +```js +firebase.resetPassword({ + email: 'test@test.com', + password: 'testest1', + username: 'tester' +}) +``` + +##### Parameters + * `credentials` [**Object**]() - Credentials same as described in firebase docs + * `profile` [**Object**]() - if initialized with userProfile support then profile will be saved into `${userProfile}/${auth.uid}` + +##### Returns + [**Promise**]() with user's UID in case of success or the error otherwise. + Always authenticate the new user in case of success diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 000000000..ae7434c89 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,7 @@ +# Contributing + +1. Fork to your account +2. Make changes and push to YOUR fork of the repo (linting is run automatically on push, and tests/coverage are run on [Travis](https://travis-ci.org/prescottprue/react-redux-firebase)) +3. Create a pull request on [react-redux-firebase](https://github.com/prescottprue/react-redux-firebase) with a description of your changes +4. Confirm that you have no merge conflicts that will keep the code from being merged +5. Keep an eye on the Pull Request for comments/updates diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 000000000..e77625884 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,165 @@ +# Getting Started + +## Before Use + +### Peer Dependencies + +Install peer dependencies: `npm i --save redux react-redux` + +### Decorators + +Though they are optional, it is highly recommended that you used decorators with this library. [The Simple Example](examples/simple) shows implementation without decorators, while [the Decorators Example](examples/decorators) shows the same application with decorators implemented. + +A side by side comparison using [react-redux](https://github.com/reactjs/react-redux)'s `connect` function/HOC is the best way to illustrate the difference: + +```javascript +class SomeComponent extends Component { + +} +export default connect()(SomeComponent) +``` +vs. + +```javascript +@connect() +export default class SomeComponent extends Component { + +} +``` + +In order to enable this functionality, you will most likely need to install a plugin (depending on your build setup). For Webpack and Babel, you will need to make sure you have installed and enabled [babel-plugin-transform-decorators-legacy](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy) by doing the following: + +1. run `npm i --save-dev babel-plugin-transform-decorators-legacy` +2. Add the following line to your `.babelrc`: +```json +{ + "plugins": ["transform-decorators-legacy"] +} +``` + + +## Install +```bash +npm install --save react-redux-firebase +``` + +## Add Reducer + +Include `firebase` in your combine reducers function: + + +```js +import { combineReducers } from 'redux' +import { firebaseStateReducer } from 'react-redux-firebase' + +// Add firebase to reducers +const rootReducer = combineReducers({ + firebase: firebaseStateReducer +}) +``` + +## Compose Function + +```js +import { compose } from 'redux' +import { reactReduxFirebase } from 'react-redux-firebase' + +// Firebase config +const firebaseConfig = { + apiKey: '', + authDomain: '', + databaseURL: '', + storageBucket: '' +} +// react-redux-firebase options +const config = { + userProfile: 'users', // firebase root where user profiles are stored + enableLogging: false, // enable/disable Firebase's database logging +} + +// Add redux Firebase to compose +const createStoreWithFirebase = compose( + reactReduxFirebase(firebaseConfig, config) +)(createStore) + +// Create store with reducers and initial state +const store = createStoreWithFirebase(rootReducer, initialState) +``` + +View the [config section](/config.html) for full list of configuration options. + +## Use in Components + +```javascript +import React, { Component, PropTypes } from 'react' +import { connect } from 'react-redux' +import { firebaseConnect, helpers } from 'react-redux-firebase' +const { isLoaded, isEmpty, dataToJS } = helpers + +@firebaseConnect([ + '/todos' // corresponds to 'todos' root on firebase +]) +@connect( + ({ firebase }) => ({ + // todos prop set to firebase data in redux under '/todos' + todos: dataToJS(firebase, '/todos'), + }) +) +export default class Todos extends Component { + static propTypes = { + todos: PropTypes.object, + firebase: PropTypes.object + } + + handleAdd = () => { + const {newTodo} = this.refs + const { firebase } = this.props + // Add a new todo to firebase + firebase.push('/todos', { text: newTodo.value, done: false }) + newTodo.value = '' + } + + render() { + const { todos } = this.props; + + // Build Todos list if todos exist and are loaded + const todosList = !isLoaded(todos) + ? 'Loading' + : isEmpty(todos) + ? 'Todo list is empty' + : Object.keys(todos).map( + (key, id) => ( + + ) + ) + + return ( +
+

Todos

+
    + {todosList} +
+ + +
+ ) + } +} +``` + +Alternatively, if you choose not to use decorators, your connect function will look like so: + +```javascript +const wrappedTodos = firebaseConnect([ + '/todos' +])(Todos) + +export default connect( + ({firebase}) => ({ + todos: dataToJS(firebase, '/todos'), + }) +)(wrappedTodos) + +``` diff --git a/docs/populate.md b/docs/populate.md new file mode 100644 index 000000000..9c3dc6d73 --- /dev/null +++ b/docs/populate.md @@ -0,0 +1,103 @@ +# Populate + +Populate allows you to replace IDs within your data with other data from Firebase. This is very useful when trying to keep your data flat. Some would call it a _join_, but it was modeled after [the mongo populate method](http://mongoosejs.com/docs/populate.html). + +List of todo items where todo item can contain an owner parameter which is a user's UID like so: + +```json +{ text: 'Some Todo Item', owner: "Iq5b0qK2NtgggT6U3bU6iZRGyma2" } +``` + +Populate allows you to replace the owner parameter with another value on Firebase under that key. That value you can be a string \(number and boolean treated as string\), or an object + +##### Example Data +```javascript +todos: { + 123: { + text: 'Some Todo Item', + owner: "Iq5b0qK2NtgggT6U3bU6iZRGyma2" + } + } + displayNames: { + Iq5b0qK2NtgggT6U3bU6iZRGyma2: 'Scott Prue', + 6Ra53mf3U9Qmdwah6rXBMgY8smu1: 'Rick Sanchez' + } + users: { + Iq5b0qK2NtgggT6U3bU6iZRGyma2: { + displayName: 'Scott Prue', + email: 'scott@prue.io' + }, + 6Ra53mf3U9Qmdwah6rXBMgY8smu1: { + displayName: 'Rick Sanchez', + email: 'rick@email.com' + } + } +``` + +### String, Number, or Boolean +When trying to replace the owner parameter with a string such as a displayName from a `/displayNames` root follow the pattern of `#populate=paramToPopulate:populateRoot`. + +##### Example Query +```javascript +@firebaseConnect([ + { path: '/todos', populates: [{ child: 'owner', root: 'displayNames' }] } + // '/todos#populate=owner:displayNames', // equivalent string notation + ]) +``` + +##### Result +```javascript +123: { + text: 'Some Todo Item', + owner: 'Scott Prue' + } +``` + +### Object +Population can also be used to populate a parameter with an object. An example of this would be populating the owner parameter, which is an ID, with the matching key from the users list. + +##### Example Query +```javascript +@firebaseConnect([ + { path: '/todos', populates: [{ child: 'owner', root: 'users' }] } + // '/todos#populate=owner:users' // equivalent string notation + ]) +``` + +##### Example Result + +```javascript +123: { + text: 'Some Todo Item', + owner: { + displayName: 'Scott Prue', + email: 'scott@prue.io' + } +} +``` + +### Object's Parameter + +There is also the option to load a parameter from within a population object. An example of this could be populating the owner parameter with the displayName property of the user with a matching ID: + +##### Example Query +```javascript +@firebaseConnect([ + { + path: '/todos', + populates: [ + { child: 'owner', root: 'users', childParam: 'email' } + ] + } + // '/todos#populate=owner:users:email' // equivalent string notation +]) +``` + +##### Example Result + +```javascript +123: { + text: 'Some Todo Item', + owner: 'scott@prue.io' +} +``` diff --git a/docs/queries.md b/docs/queries.md new file mode 100644 index 000000000..93745d39f --- /dev/null +++ b/docs/queries.md @@ -0,0 +1,139 @@ +# Queries + +When listening to paths, it is possible to modify the query with any of [Firebase's included query methods](https://firebase.google.com/docs/reference/js/firebase.database.Query). Below are examples using Firebase query methods as well as other methods that are included (such as 'populate'). + +## orderByChild +To order the query by a child within each object, user orderByChild. + +#### Example +Ordering a list of todos by the text parameter of the todo item (placing them in alphabetical order). + +```javascript +@firebaseConnect([ + '/todos#orderByChild=text' +]) +``` + +## orderByKey +Order a list by the key of each item. Since push keys contain time, this is also a way of ordering by when items were created. + +#### Example +Ordering a list of todos based on their key (puts them in order of when they were created) + +```javascript +@firebaseConnect([ + '/todos#orderByKey' +]) +``` + +## orderByValue +Runs [Firebase's orderByValue](https://firebase.google.com/docs/reference/js/firebase.database.Query#orderByValue) + +Order a list by the value of each object + +#### Example +Ordering a list of score's based on score's value + +```javascript +@firebaseConnect([ + `scores#orderByValue` +]) +``` +## orderByPriority +Runs [Firebase's orderByPriority](https://firebase.google.com/docs/reference/js/firebase.database.Query#orderByPriority) + +#### Example +Ordering a list based on priorities + +```javascript +@firebaseConnect([ + `scores#orderByPriority` +]) +``` + +## limitToFirst +Limit query results to the first n number of results + +#### Examples +1. Displaying only the first todo item + + ```javascript + @firebaseConnect([ + '/todos#limitToFirst' + ]) + ``` +2. Displaying only the first 10 todo items + + ```javascript + @firebaseConnect([ + '/todos#limitToFirst=10' + ]) + ``` + +## limitToLast +Limit query results to the last n number of results + +#### Examples +1. Only the **last** todo item + + ```javascript + @firebaseConnect([ + '/todos#limitToFirst' + ]) + ``` +2. Only the **last 10** todo items + + ```javascript + @firebaseConnect([ + '/todos#limitToFirst=10' + ]) + ``` + +## startAt + +Limit query results to include a range starting at a specific number + +#### Example + +1. Starting at the fifth item + ```js + @firebaseConnect([ + 'todos#startAt=5&limitToFirst=2' + ]) + ``` +2. Paginate results + ```js + @firebaseConnect([ + 'todos#startAt=5&limitToFirst=10' + ]) + ``` + +## endAt +```js +@firebaseConnect([ + 'todos#orderByChild=added&startAt=1&endAt=5' +]) +``` + +## equalTo +```js +@firebaseConnect([ + 'todos#orderByChild=added&startAt=5&limitToFirst=2' +]) +``` + +## Once +To load a firebase location once instead of binding, the once option can be used: + +```javascript +@firebaseConnect([ + { type: 'once', path: '/todos' } +]) + +``` + +## Populate {#populate} + +Populate allows you to replace IDs within your data with other data from Firebase. This is very useful when trying to keep your data flat. Some would call it a _join_, but it was modeled after [the mongo populate method](http://mongoosejs.com/docs/populate.html). + +Visit [Populate Section](/docs/populate.md) for full documentation. diff --git a/docs/recipes/README.md b/docs/recipes/README.md new file mode 100644 index 000000000..62c5ab881 --- /dev/null +++ b/docs/recipes/README.md @@ -0,0 +1,38 @@ +# Recipes + +This section includes some recipes for using react-redux-firebase within real applications. + +## [Actions](/docs/recipes/actions.md) + +Standard actions for interacting with Firebase including `push`, `set`, `uniqueSet`, `update`, and `remove`. + +#### Examples +* Write data +* Remove Data +* Writing key from a push to another location +* Writing to multiple locations + +## [Upload](/docs/recipes/upload.md) +Actions for uploading files with Firebase storage including `uploadFiles` and `uploadFile` as well as direct access to `Firebase.storage()`. + +#### Examples +* Upload Files +* Upload a String as a file + +## [Thunks](/docs/recipes/thunks.md) + +Actions that dispatch other actions and have access to redux state + +#### Examples +* Async API Calls (`REST` or Javascript API) +* Actions based on state (including browser history) +* Displaying error after invalid write attempt + +## Epics + +Middleware that listens for Actions and dispatches other, often async, actions. + +#### Examples +* Debounced persisting of user input: Listen for typing actions from `redux-form` -> call `firebase.update()` +* Throttled/Debouced API calls +* Displaying a system wide error: Listen for error actions -> display error message diff --git a/docs/recipes/actions.md b/docs/recipes/actions.md new file mode 100644 index 000000000..6ef6618c3 --- /dev/null +++ b/docs/recipes/actions.md @@ -0,0 +1,6 @@ +# Actions +react-redux-firebase comes with built in actions including `set`, `push`, and `update` + +## Advanced Actions + +If you are looking to write advanced actions (i.e. multiple steps contained within one action), look at the [thunks section](/docs/recipes/thunks) diff --git a/docs/recipes/thunks.md b/docs/recipes/thunks.md new file mode 100644 index 000000000..d915f376d --- /dev/null +++ b/docs/recipes/thunks.md @@ -0,0 +1,49 @@ +# Thunks + +### redux-thunk integration + +In order to get the most out of writing your thunks, make sure to set up your thunk middleware using it's redux-thunk's `withExtraArgument` method like so: + +```javascript +import { applyMiddleware, compose, createStore } from 'redux'; +import thunk from 'redux-thunk'; +import { reduxReactFirebase } from 'react-redux-firebase'; +import makeRootReducer from './reducers'; +import { getFirebase } from 'react-redux-firebase'; + +const fbConfig = {} // your firebase config + +const store = createStore( + makeRootReducer(), + initialState, + compose( + applyMiddleware([ + thunk.withExtraArgument(getFirebase) // Pass getFirebase function as extra argument + ]), + reduxReactFirebase(fbConfig, { userProfile: 'users', enableLogging: false }) + ) +); + +``` + +## Example Thunk + +After following the setup above, `getFirebase` function becomes available within your thunks as the third argument: + +```javascript +const sendNotification = (payload) => { + type: NOTIFICATION, + payload +} +export const addTodo = (newTodo) => + (dispatch, getState, getFirebase) => { + const firebase = getFirebase() + firebase + .helpers + .push('todos', newTodo) + .then(() => { + dispatch(sendNotification('Todo Added')) + }) + }; + +``` diff --git a/docs/recipes/upload.md b/docs/recipes/upload.md new file mode 100644 index 000000000..e087887ec --- /dev/null +++ b/docs/recipes/upload.md @@ -0,0 +1,73 @@ +# Upload + +### File Drag/Drop Upload with Delete + +```javascript +import React, { PropTypes, Component } from 'react' +import { connect } from 'react-redux' +import { firebaseConnect, helpers } from 'react-redux-firebase' +import { map } from 'lodash' +import Dropzone from 'react-dropzone' + +const { dataToJS } = helpers + +const filesPath = 'uploadedFiles' +@firebaseConnect([ + filesPath +]) +@connect( + ({ firebase }) => ({ + uploadedFiles: dataToJS(firebase, filesPath) + }) +) +export default class Uploader extends Component { + static propTypes = { + firebase: PropTypes.object.isRequired, + uploadedFiles: PropTypes.object + } + + onFilesDrop = (files) => { + // uploadFiles(storagePath, files, dbPath) + // Uploads files and push's objects containing metadata to database at dbPath + this.props.firebase.uploadFiles(filesPath, files, filesPath) + } + + onFileDelete = (file, key) => { + // deleteFile(storagePath, dbPath) + // Deletes file and removes file object from database + this.props.firebase.deleteFile(file.fullPath, `${filesPath}/${key}`) + } + + render () { + const { uploadedFiles } = this.props + return ( +
+ +
+ Drag and drop files here + or click to select +
+
+ { + uploadedFiles && +
+

+ Uploaded file(s): +

+ { + map(uploadedFiles, (file, key) => ( +
+ {file.name} + +
+ )) + } +
+ } +
+ ) + } +} +``` diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 000000000..48cccb9e8 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,10 @@ +# Roadmap + +### Short Term +* Huge App Example with passing of props to child routes + +### Long Term +* Improve docs readability (maybe include REPL for editable examples) +* Multi-level population +* Population rules suggestion/generation +* Population performance measurement diff --git a/docs/storage.md b/docs/storage.md new file mode 100644 index 000000000..4b0062e96 --- /dev/null +++ b/docs/storage.md @@ -0,0 +1,39 @@ +# Storage + +Firebase Storage is available within components by using `this.props.firebase.storage()`. This method is equivalent to Firebase's `firebase.storage()` method, meaning you can reference the [Firebase Storage Docs](https://firebase.google.com/docs/storage/web/upload-files) for full list of methods and examples. + +### File String Upload + +```javascript +import React, { Component, PropTypes } from 'react' +import { firebaseConnect } from 'react-redux-firebase' + +@firebaseConnect() +export default class Uploader extends Component { + static propTypes = { + firebase: PropTypes.object + } + + render() { + const { firebase: { storage } } = this.props; + + const addTestFile = () => { + const {newTodo} = this.refs + const storageRef = storage().ref() + const fileRef = storageRef.child('test.txt') + fileRef.putString('Some File Contents') + .then(snap => console.log('upload successful', snap)) + .catch(err => console.error('error uploading file', err)) + } + + return ( +
+

Example Upload

+ +
+ ) + } +} +``` diff --git a/package.json b/package.json index 2a3d65218..178ab42b3 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,13 @@ "test": "mocha -R spec --compilers js:babel-core/register ./test/setup.js ./test/**/*.spec.js", "test:cov": "istanbul cover ./node_modules/mocha/bin/_mocha -- ./test/** --recursive --report lcov --compilers js:babel-register --require babel-polyfill", "codecov": "cat coverage/*/lcov.info | codecov", - "prepush": "npm run lint:fix" + "prepush": "npm run lint:fix", + "docs:clean": "rimraf _book", + "docs:prepare": "gitbook install", + "docs:api": "node bin/api-docs-generate", + "docs:build": "npm run docs:prepare && gitbook build -g prescottprue/react-redux-firebase && npm run docs:api", + "docs:watch": "npm run docs:prepare && gitbook serve", + "docs:publish": "npm run docs:clean && npm run docs:build && cp CNAME _book && cd _book && git init && git commit --allow-empty -m 'update book' && git checkout -b gh-pages && touch .nojekyll && git add . && git commit -am 'update book' && git push git@github.com:prescottprue/react-redux-firebase gh-pages --force" }, "contributors": [ { @@ -64,6 +70,9 @@ "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "codecov": "^1.0.1", + "docdown": "^0.7.2", + "documentation": "^4.0.0-beta15", + "documentation-markdown-api-theme": "^1.0.2", "eslint": "^3.10.2", "eslint-config-standard": "^6.2.1", "eslint-config-standard-react": "^4.2.0", @@ -71,6 +80,7 @@ "eslint-plugin-promise": "^3.0.0", "eslint-plugin-react": "^6.7.1", "eslint-plugin-standard": "^2.0.1", + "gitbook-cli": "^2.3.0", "istanbul": "^1.1.0-alpha.1", "jsdom": "^9.8.3", "mocha": "^3.1.2", diff --git a/src/compose.js b/src/compose.js index 1839cd5b5..60e958011 100644 --- a/src/compose.js +++ b/src/compose.js @@ -2,6 +2,47 @@ import Firebase from 'firebase' import { authActions, queryActions, storageActions } from './actions' let firebaseInstance +/** + * @name reactReduxFirebase + * @external + * @description Middleware that handles configuration (placed in redux's `compose` call) + * @param {Object} fbConfig - Object containing Firebase config including databaseURL + * @param {String} fbConfig.apiKey - Firebase apiKey + * @param {String} fbConfig.authDomain - Firebase auth domain + * @param {String} fbConfig.databaseURL - Firebase database url + * @param {String} fbConfig.storageBucket - Firebase storage bucket + * @param {Object} config - Containing react-redux-firebase specific config such as userProfile + * @param {String} config.userProfile - Location on firebase to store user profiles + * @param {Boolean} config.enableLogging - Location on firebase to store user profiles. default: `false` + * @param {Function} config.profileDecorator - Location on firebase to store user profiles. default: `false` + * @param {Boolean} config.updateProfileOnLogin - Whether or not to update profile when logging in. default: `false` + * @param {Boolean} config.profileParamsToPopulate - Whether or not to update profile when logging in. default: `false` + * @return {Function} That accepts a component a returns a wrapped version of component + * @example Data + * import { createStore, compose } from 'redux' + * import { reactReduxFirebase } from 'react-redux-firebase' + * + * // Firebase config + * const fbConfig = { + * apiKey: '', + * authDomain: '', + * databaseURL: '', + * storageBucket: '' + * } + * + * // React Redux Firebase Config + * const config = { + * userProfile: 'users' + * } + * + * // Add react-redux-firebase to compose + * const createStoreWithFirebase = compose( + * reactReduxFirebase(fbConfig, config), + * )(createStore) + * + * // Use Function later to create store + * const store = createStoreWithFirebase(rootReducer, initialState) + */ export default (config, otherConfig) => next => (reducer, initialState, middleware) => { const defaultConfig = { @@ -124,7 +165,11 @@ export default (config, otherConfig) => next => return store } -// Expose Firebase instance +/** + * @description Expose Firebase instance. + * Warning: This is going to be rewritten in coming versions. + * @private +*/ export const getFirebase = () => { // TODO: Handle recieveing config and creating firebase instance if it doesn't exist /* istanbul ignore next: Firebase instance always exists during tests */ diff --git a/src/connect.js b/src/connect.js index 6142e7fd2..633ecfd59 100644 --- a/src/connect.js +++ b/src/connect.js @@ -3,6 +3,42 @@ import { isEqual } from 'lodash' import { watchEvents, unWatchEvents } from './actions/query' import { getEventsFromInput, createCallable } from './utils' +/** + * @name firebaseConnect + * @extends React.Component + * @description Higher Order Component that automatically listens/unListens + * to provided firebase paths using React's Lifecycle hooks. + * @param {Array} watchArray - Array of objects or strings for paths to sync from Firebase + * @return {Function} - that accepts a component to wrap and returns the wrapped component + * @example Basic + * // this.props.firebase set on App component as firebase object with helpers + * import { firebaseConnect } from 'react-redux-firebase' + * export default firebaseConnect()(App) + * @example Paths + * import { connect } from 'react-redux' + * import { firebaseConnect, helpers } from 'react-redux-firebase' + * const { pathToJS } = helpers + * + * // pass todos list from redux as this.props.todosList + * export default connect(({ firebase }) => ({ + * profile: pathToJS(firebase, 'profile'), + * auth: pathToJS(firebase, 'auth') + * }))(App) + * @example Data + * import { connect } from 'react-redux' + * import { firebaseConnect, helpers } from 'react-redux-firebase' + * const { dataToJS } = helpers + * + * // sync /todos from firebase into redux + * const fbWrapped = firebaseConnect([ + * 'todos' + * ])(App) + * + * // pass todos list from redux as this.props.todosList + * export default connect(({ firebase }) => ({ + * todosList: dataToJS(firebase, 'todos') + * }))(fbWrapped) + */ export default (dataOrFn = []) => WrappedComponent => { class FirebaseConnect extends Component { diff --git a/src/constants.js b/src/constants.js index ea1a3c01e..23dd9ba4c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,5 +1,61 @@ -const prefix = '@@reactReduxFirebase/' +/** @constant + * @description Prefix for all actions within library + * @type {String} + * @example + * import { constants } from 'react-redux-firebase' + * constants.actionsPrefix === '@@reactReduxFirebase' // true +*/ +export const actionsPrefix = '@@reactReduxFirebase/' +/** @constant + * @description Object containing all action types + * @type {Object} + * @example + * import { actionTypes } from 'react-redux-firebase' + * actionTypes.SET === '@@reactReduxFirebase/SET' // true + * @example + * import { constants } from 'react-redux-firebase' + * constants.actionTypes.SET === '@@reactReduxFirebase/SET' // true +*/ +export const actionTypes = { + START: `${actionsPrefix}START`, + SET: `${actionsPrefix}SET`, + SET_PROFILE: `${actionsPrefix}SET_PROFILE`, + LOGIN: `${actionsPrefix}LOGIN`, + LOGOUT: `${actionsPrefix}LOGOUT`, + LOGIN_ERROR: `${actionsPrefix}LOGIN_ERROR`, + NO_VALUE: `${actionsPrefix}NO_VALUE`, + UNAUTHORIZED_ERROR: `${actionsPrefix}UNAUTHORIZED_ERROR`, + ERROR: `${actionsPrefix}ERROR`, + INIT_BY_PATH: `${actionsPrefix}INIT_BY_PATH`, + AUTHENTICATION_INIT_STARTED: `${actionsPrefix}AUTHENTICATION_INIT_STARTED`, + AUTHENTICATION_INIT_FINISHED: `${actionsPrefix}AUTHENTICATION_INIT_FINISHED`, + FILE_UPLOAD_START: `${actionsPrefix}FILE_UPLOAD_START`, + FILE_UPLOAD_ERROR: `${actionsPrefix}FILE_UPLOAD_ERROR`, + FILE_UPLOAD_PROGRESS: `${actionsPrefix}FILE_UPLOAD_PROGRESS`, + FILE_UPLOAD_COMPLETE: `${actionsPrefix}FILE_UPLOAD_COMPLETE`, + FILE_DELETE_START: `${actionsPrefix}FILE_DELETE_START`, + FILE_DELETE_ERROR: `${actionsPrefix}FILE_DELETE_ERROR`, + FILE_DELETE_COMPLETE: `${actionsPrefix}FILE_DELETE_COMPLETE` +} + +/** @constant + * @description List of all external auth providers that are supported (firebase's email/anonymous included by default) + * @type {Array} + * @private +*/ +export const supportedAuthProviders = [ + 'google', + 'github', + 'twitter', + 'facebook' +] + +/** @constant + * @description Default keys returned within JSON Web Token recieved when authenticating + * @type {Array} + * @private +*/ export const defaultJWTKeys = [ 'aud', 'auth_time', @@ -11,36 +67,6 @@ export const defaultJWTKeys = [ 'user_id' ] -export const actionTypes = { - START: `${prefix}START`, - SET: `${prefix}SET`, - SET_PROFILE: `${prefix}SET_PROFILE`, - LOGIN: `${prefix}LOGIN`, - LOGOUT: `${prefix}LOGOUT`, - LOGIN_ERROR: `${prefix}LOGIN_ERROR`, - NO_VALUE: `${prefix}NO_VALUE`, - UNAUTHORIZED_ERROR: `${prefix}UNAUTHORIZED_ERROR`, - ERROR: `${prefix}ERROR`, - INIT_BY_PATH: `${prefix}INIT_BY_PATH`, - AUTHENTICATION_INIT_STARTED: `${prefix}AUTHENTICATION_INIT_STARTED`, - AUTHENTICATION_INIT_FINISHED: `${prefix}AUTHENTICATION_INIT_FINISHED`, - FILE_UPLOAD_START: `${prefix}FILE_UPLOAD_START`, - FILE_UPLOAD_ERROR: `${prefix}FILE_UPLOAD_ERROR`, - FILE_UPLOAD_PROGRESS: `${prefix}FILE_UPLOAD_PROGRESS`, - FILE_UPLOAD_COMPLETE: `${prefix}FILE_UPLOAD_COMPLETE`, - FILE_DELETE_START: `${prefix}FILE_DELETE_START`, - FILE_DELETE_ERROR: `${prefix}FILE_DELETE_ERROR`, - FILE_DELETE_COMPLETE: `${prefix}FILE_DELETE_COMPLETE` -} - -// List of all external auth providers that are supported (firebase's email/anonymous included by default) -export const supportedAuthProviders = [ - 'google', - 'github', - 'twitter', - 'facebook' -] - export default { defaultJWTKeys, actionTypes, diff --git a/src/helpers.js b/src/helpers.js index 2449a1912..303cbfaff 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,17 +1,95 @@ import { size, map } from 'lodash' +/** + * @description Detect whether items are loaded yet or not + * @param {Object} item - Item to check loaded status of. A comma seperated list is also acceptable. + * @return {Boolean} Whether or not item is loaded + * @example + * import React, { Component, PropTypes } from 'react' + * import { connect } from 'react-redux' + * import { firebaseConnect, helpers } from 'react-redux-firebase' + * const { isLoaded, dataToJS } = helpers + * + * @firebaseConnect(['/todos']) + * @connect( + * ({ firebase }) => ({ + * todos: dataToJS(firebase, '/todos'), + * }) + * ) + * class Todos extends Component { + * static propTypes = { + * todos: PropTypes.object + * } + * + * render() { + * const { todos } = this.props; + * + * // Show loading while todos are loading + * if(!isLoaded(todos)) { + * return Loading... + * } + * + * return
    {todosList}
+ * } + * } + */ +export const isLoaded = function () { + if (!arguments || !arguments.length) { + return true + } + + return map(arguments, a => a !== undefined).reduce((a, b) => a && b) +} + +/** + * @description Detect whether items are empty or not + * @param {Object} item - Item to check loaded status of. A comma seperated list is also acceptable. + * @return {Boolean} Whether or not item is empty + * @example + * import React, { Component, PropTypes } from 'react' + * import { connect } from 'react-redux' + * import { firebaseConnect, helpers } from 'react-redux-firebase' + * const { isEmpty, dataToJS } = helpers + * + * @firebaseConnect(['/todos']) + * @connect( + * ({ firebase }) => ({ + * todos: dataToJS(firebase, '/todos'), + * }) + * ) + * class Todos extends Component { + * static propTypes = { + * todos: PropTypes.object + * } + * + * render() { + * const { todos } = this.props; + * + * // Message for if todos are empty + * if(isEmpty(todos)) { + * return No Todos Found + * } + * + * return
    {todosList}
+ * } + * } + */ +export const isEmpty = data => !(data && size(data)) + /** * @description Fix path by adding "/" to path if needed * @param {String} path - Path string to fix - * @return {String} path - Fixed path + * @return {String} - Fixed path + * @private */ export const fixPath = path => ((path.substring(0, 1) === '/') ? '' : '/') + path /** * @description Convert Immutable Map to a Javascript object - * @param {Object} data - Immutable Map to be converted to JS object + * @param {Object} data - Immutable Map to be converted to JS object (state.firebase) * @return {Object} data - Javascript version of Immutable Map + * @return {Object} Data located at path within Immutable Map */ export const toJS = data => { if (data && data.toJS) { @@ -23,8 +101,19 @@ export const toJS = data => { /** * @description Convert parameter from Immutable Map to a Javascript object - * @param {Object} data - Immutable Map to be converted to JS object - * @return {Object} data - Javascript version of Immutable Map + * @param {Map} firebase - Immutable Map to be converted to JS object (state.firebase) + * @param {String} path - Path from state.firebase to convert to JS object + * @param {Object|String|Boolean} notSetValue - Value to use if data is not available + * @return {Object} Data located at path within Immutable Map + * @example Basic + * import { connect } from 'react-redux' + * import { firebaseConnect, helpers } from 'react-redux-firebase' + * const { pathToJS } = helpers + * const fbWrapped = firebaseConnect()(App) + * export default connect(({ firebase }) => ({ + * profile: pathToJS(firebase, 'profile'), + * auth: pathToJS(firebase, 'auth') + * }))(fbWrapped) */ export const pathToJS = (data, path, notSetValue) => { if (!data) { @@ -42,10 +131,21 @@ export const pathToJS = (data, path, notSetValue) => { /** * @description Convert parameter under "data" path of Immutable Map to a Javascript object - * @param {Object} data - Immutable Map to be converted to JS object + * @param {Map} firebase - Immutable Map to be converted to JS object (state.firebase) * @param {String} path - Path of parameter to load * @param {Object|String|Boolean} notSetValue - Value to return if value is not found - * @return {Object} data - Javascript version of Immutable Map + * @return {Object} Data located at path within Immutable Map + * @example Basic + * import { connect } from 'react-redux' + * import { firebaseConnect, helpers } from 'react-redux-firebase' + * const { dataToJS } = helpers + * + * const fbWrapped = firebaseConnect(['/todos'])(App) + * + * export default connect(({ firebase }) => ({ + * // this.props.todos loaded from state.firebase.data.todos + * todos: dataToJS(firebase, 'todos') + * }))(fbWrapped) */ export const dataToJS = (data, path, notSetValue) => { if (!data) { @@ -65,11 +165,11 @@ export const dataToJS = (data, path, notSetValue) => { /** * @description Load custom object from within store - * @param {Object} data - Immutable Map from store to be converted to JS object + * @param {Map} firebase - Immutable Map to be converted to JS object (state.firebase) * @param {String} path - Path of parameter to load * @param {String} customPath - Part of store from which to load * @param {Object|String|Boolean} notSetValue - Value to return if value is not found - * @return {Object} data - Javascript version of custom path within Immutable Map + * @return {Object} Data located at path within Immutable Map */ export const customToJS = (data, path, custom, notSetValue) => { if (!(data && data.getIn)) { @@ -89,9 +189,10 @@ export const customToJS = (data, path, custom, notSetValue) => { /** * @description Convert Immutable Map to a Javascript object - * @param {Object} snapshot - Snapshot from store + * @param {Map} snapshot - Snapshot from store * @param {String} path - Path of snapshot to load - * @return {Object} notSetValue - Value to return if snapshot is not found + * @param {Object|String|Boolean} notSetValue - Value to return if value is not found + * @return {Object} Data located at path within Immutable Map */ export const snapshotToJS = (snapshot, path, notSetValue) => { if (!snapshot) { @@ -109,28 +210,6 @@ export const snapshotToJS = (snapshot, path, notSetValue) => { return snapshot } -/** - * @description Detect whether items are loaded yet or not - * @param {Object} item - Item to check loaded status of. A comma seperated list is also acceptable. - * @return {Boolean} isLoaded - Whether or not item is loaded - */ -export const isLoaded = function () { - if (!arguments || !arguments.length) { - return true - } - - return map(arguments, a => a !== undefined).reduce((a, b) => a && b) -} - -/** - * @description Detect whether items are empty or not - * @param {Object} item - Item to check loaded status of. A comma seperated list is also acceptable. - * @return {Boolean} isEmpty - Whether or not item is empty - */ -export const isEmpty = data => { - return !(data && size(data)) -} - export default { toJS, pathToJS, diff --git a/src/reducer.js b/src/reducer.js index 87108abc8..31e444373 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -1,5 +1,6 @@ import { fromJS } from 'immutable' import { actionTypes } from './constants' + const { SET, SET_PROFILE, @@ -24,6 +25,18 @@ const initialState = fromJS(emptyState) const pathToArr = path => path.split(/\//).filter(p => !!p) +/** + * @name firebaseStateReducer + * @description Reducer for react redux firebase. This function is called + * automatically by redux every time an action is fired. Based on which action + * is called and its payload, the reducer will update redux state with relevant + * changes. + * @param {Map} state - Current Redux State + * @param {Object} action - Action which will modify state + * @param {String} action.type - Type of Action being called + * @param {String} action.data - Type of Action which will modify state + * @return {Map} State + */ export default (state = initialState, action = {}) => { const { path } = action let pathArr