Package Exports
- vuex-easy-firestore
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (vuex-easy-firestore) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Vuex Easy Firestore 🔥
In just 4 lines of code, get your vuex module in complete 2-way sync with firestore.
What it does:
You literally only need to add these 4 lines to your vuex module and you'll have automatic sync with firestore!
const userModule = {
firestorePath: 'users/{userId}/data',
firestoreRefType: 'collection', // or 'doc'
moduleName: 'user',
statePropName: 'docs',
// the rest of your module here
}
// add userModule as vuex plugin wrapped in vuex-easy-firestoreand Alakazam! Now you have a vuex module called user with state: {docs: {}}.
All firestore documents in your collection will be added with the doc's id as key inside docs in your state.
Now you just update and add docs with dispatch('user/set', newItem) and forget about the rest!
Other features include hooks, fillables (limit props to sync), default values (add props on sync), a fetch function and much more...
Table of contents
Motivation
I didn't like writing an entire an API wrapper from scratch for firestore every single project. If only a vuex module could be in perfect sync with firestore without having to code all the boilerplate yourself...
And that's how Vuex Easy Firestore was born.
Installation
npm i --save vuex-easy-firestoreSetup
import createEasyFirestore from 'vuex-easy-firestore'
const userDataModule = {
firestorePath: 'users/{userId}/data',
firestoreRefType: 'collection', // or 'doc'
moduleName: 'userData',
statePropName: 'docs',
// for more options see below
// you can also add state/getters/mutations/actions
}
// do the magic 🧙🏻♂️
const easyFirestore = createEasyFirestore(userDataModule)
// and include as PLUGIN in your vuex store:
store: {
// ... your store
plugins: [easyFirestore]
}Usage
Automatic 2-way sync
After Firebase finds a user through onAuthStateChanged you need to dispatch openDBChannel once to open the channel to your firestore:
// Be sure to initialise Firebase first
Firebase.auth().onAuthStateChanged(user => {
if (user) {
// user is logged in
store.dispatch('userData/openDBChannel')
.then(console.log)
.catch(console.error)
}
})This doesn't require any callback in particular; the results will be saved in your vuex store at the path you have set:
moduleName + statePropName which is in this example 'userData/docs'.
To automatically edit your vuex store & have firebase always in sync you just need to use the actions that were set up for you:
Editing
Basically with just 4 actions (set, patch, insert, delete) you can make changes to your vuex store and everything will automatically stay up to date with your firestore!
There are two ways to use vuex-easy-firestore, in 'collection' or 'doc' mode. You can only choose one because this points to the path you sync your vuex module to:
firestoreRefType: 'collection'for a firestore collectionfirestoreRefType: 'doc'for a single firestore document
Depending on which mode there are some small changes, but the syntax is mostly the same.
The sync is fully robust and automatically groups any api calls per 1000 ms. You don't have to worry about optimising/limiting the api calls, it's all done automatically! (Only one api call per 1000ms will be made for a maximum of 500 changes, if there are more changes queued it will automatically be split over 2 api calls).
Editing in 'collection' mode
With these 4 actions: set, patch, insert and delete, you can edit single docs in your vuex module. Any updates made with these actions will keep your firestore in sync!
dispatch('moduleName/set', doc) // will choose to dispatch either `patch` OR `insert` automatically
dispatch('moduleName/patch', doc) // doc needs an 'id' prop
dispatch('moduleName/insert', doc)
dispatch('moduleName/delete', id)There are two ways you can give a payload to set, patch or insert:
const id = '123'
// Add the `id` as a prop to the item you want to set/update:
dispatch('moduleName/set', {id, name: 'my new name'})
// Or use the `id` as [key] and the item as its value:
dispatch('moduleName/set', {[id]: {name: 'my new name'}})
// Please note that only the `name` will be updated, and other fields are left alone!There are two ways to delete things: the whole item or just a sub-property!
// Delete the whole item:
dispatch('moduleName/delete', id)
// Delete a sub-property of an item:
dispatch('moduleName/delete', `${id}.tags.water`)
// the items looks like:
{
id: '123',
tags: {
fire: true,
water: true, // only `water` will be deleted from the item!
}
}In the above example you can see that you can delete a sub-property by passing a string and separate sub-props with .
Batch updates/inserts/deletions
In cases you don't want to loop through items you can also use the special batch actions below. The main difference is you will have separate hooks (see hooks), and batches are optimised to update the vuex store first for all changes and the syncs to firestore last.
dispatch('moduleName/insertBatch', docs) // an array of docs
dispatch('moduleName/patchBatch', {doc: {}, ids: []}) // `doc` is an object with the fields to patch, `ids` is an array
dispatch('moduleName/deleteBatch', ids) // an array of idsAuto-generated fields
When working with collections, each document insert or update will automatically receive these fields:
created_at/updated_atboth use:Firebase.firestore.FieldValue.serverTimestamp()created_by/updated_bywill automatically fill in the userId
Editing in 'doc' mode
In 'doc' mode all changes will take effect on the single document you have passed in the firestorePath. You will be able to use these actions:
dispatch('moduleName/set', {name: 'my new name'}) // same as `patch`
dispatch('moduleName/patch', {status: 'awesome'})
// Only the props you pass will be updated.
dispatch('moduleName/delete', 'status') // pass a prop-name
// Only the propName (string) you pass will be deletedAnd yes, just like in 'collection' mode, you can pass a prop-name with sub-props like so:
dispatch('moduleName/delete', 'settings.banned')
// the doc looks like:
{
userName: 'Satoshi',
settings: {
showStatus: true,
banned: true, // only `banned` will be deleted from the item!
}
}Shortcut: set(path, doc)
Inside Vue component templates you can also access the set action through a shortcut: $store.set(path, doc). Or with our path: $store.set('userData', doc).
For this shortcut usage, import the npm module 'vuex-easy-access' and just add {vuexEasyFirestore: true} in its options. Please check the relevant documentation!
Please note that it is important to pass the 'vuex-easy-firestore' plugin first, and the 'vuex-easy-access' plugin second for it to work properly.
Fetching
Say that you have a default filter set on the documents you are syncing when you openDBChannel (see Filters). And you want to fetch extra documents with other filters. (eg. archived posts) In this case you can use the fetch action to retrieve documents from the same firestore path your module is synced to:
dispatch('user/favourites/fetch', {whereFilters = [], orderBy = []})
.then(console.log)
.catch(console.error)- whereFilters: The same as firestore's
.where(). An array of arrays with the filters you want. eg.[['field', '==', false], ...] - orderBy: The same as firestore's
.orderBy(). eg.['created_date']
You have to manually write the logic for what you want to do with the fetched documents.s
Multiple modules with 2way sync
Of course you can have multiple vuex modules, all in sync with different firestore paths.
const userDataModule = {/* config */}
const anotherModule = {/* config */}
const aThirdModule = {/* config */}
// make sure you choose a different moduleName and firestorePath each time!
const easyFirestores = createEasyFirestore([userDataModule, anotherModule, aThirdModule])
// and include as PLUGIN in your vuex store:
store: {
// ... your store
plugins: [easyFirestores]
}Sync directly to module state
You can sync the doc(s) directly to the module state as well! Syncing directly to the state means that the doc(s) will not be added to the statePropName you can define, but instead be added directly to the state of the module.
This can be useful to prevent cases where you have: items/items where the first is the module and the second is the stateProp that holds all docs. You can simple leave the statePropName blank (set to empty string) and the docs will be synced to the state directly!
A more in depth example:
Say your have a vuex-easy-firestore module for user with the following settings:
const userModule = {
firestorePath: 'userSettings/{userId}',
firestoreRefType: 'doc',
moduleName: 'user',
statePropName: 'settings',
state: {
settings: {ui: {mode: 'dark'}}
}
}To update the ui mode to 'light' and have it patch automatically through Vuex Easy Firestore, you would have to use the set actions on the user module like so: dispatch('user/set', {ui: {mode: 'light'}})
This is kind of weird because the word "settings" is nowhere to be found... It just says 'user/set'. It would be much clearer if we can set the settings with dispatch('user/settings/set').
To do this we would have to separate the settings into a settings module. But if we change the moduleName to 'user/settings' we don't want to set a statePropName to 'settings' as well! Otherwise we'd have to access it by user/settings.settings... Kinda weird huh.
So the best solution is to sync the settings doc directly to the settings-module's state. You can do this like so:
const settingsModule = {
// ...
moduleName: 'user/settings',
statePropName: '', // Leave statePropName blank!
state: {
ui: {mode: 'dark'}
}
}Please note that if you have other state-props in settings that you don't want to be synced you have to add it to the guard array (see guard config).
const settingsModule = {
// ...
sync: {
guard: ['modalOpenend'] // will not be synced to firestore
},
state: {
ui: {mode: 'dark'},
modalOpened: false
}
}Extra features
Filters
The filters set in sync: {} are applied before the DB Channel is openend. They are only available for syncing 'collections'.
- where: The same as firestore's
.where(). An array of arrays with the filters you want. eg.[['field', '==', false], ...] - orderBy: The same as firestore's
.orderBy(). eg.['created_date']
{
// your other config...
sync: {
where: [], // an array of arrays
orderBy: [],
}
}You can also use '{userId}' as third param for a where filter. Eg.:
where: [
['created_by', '==', '{userId}']
]Fillables and guard
- Fillables: Array of keys - the props which may be synced to the server. Any other props will not be synced!
- Guard: Array of keys - the props which should not be synced to the server. (the opposite of 'fillables')
{
// your other config...
sync: {
fillables: [],
guard: [],
}
}Hooks before insert/patch/delete
A function where you can check something or even change the doc before the store mutation occurs.
! Must call updateStore(doc) to make the store mutation.
But you may choose not to call this to abort the mutation.
{
// your other config...
sync: {
insertHook: function (updateStore, doc, store) { updateStore(doc) },
patchHook: function (updateStore, doc, store) { updateStore(doc) },
deleteHook: function (updateStore, id, store) { updateStore(id) },
// Batches have separate hooks!
insertBatchHook: function (updateStore, doc, store) { updateStore(doc) },
patchBatchHook: function (updateStore, doc, ids, store) { updateStore(doc, ids) },
deleteBatchHook: function (updateStore, ids, store) { updateStore(ids) },
}
}Hooks after changes on the server
Exactly the same as above, but for changes that have occured on the server. You also have some extra parameters to work with:
- id: the doc id returned in
change.doc.id(see firestore documentation for more info) - doc: the doc returned in
change.doc.data()(see firestore documentation for more info) - source: of the change. Can be 'local' or 'server'
{
// your other config...
serverChange: {
addedHook: function (updateStore, doc, id, store, source, change) { updateStore(doc) },
modifiedHook: function (updateStore, doc, id, store, source, change) { updateStore(doc) },
removedHook: function (updateStore, doc, id, store, source, change) { updateStore(doc) },
}
}defaultValues set after server retrieval
defaultValues is an object with props that will be set on each doc that comes from the server. You HAVE to set the props you want to be reactive if some items in firestore don't have those props. The retrieved docs will be deep merged on top of these default values.
{
// your other config...
serverChange: {
defaultValues: {},
}
}All config options
Here is a list with all possible config options:
const firestoreModule = {
firestorePath: '',
// The path to a collection or doc in firestore. You can use `{userId}` which will be replaced with the user Id.
firestoreRefType: '',
// `'collection'` or `'doc'`. Depending on your `firestorePath`.
moduleName: '',
// The module name. Can be nested, eg. `'user/items'`
statePropName: '',
// The name of the property where the docs or doc will be synced to. If left blank it will be synced on the state of the module. (Please see [Sync directly to module state](#sync-directly-to-module-state) for more info)
// Related to the 2-way sync:
sync: {
where: [],
orderBy: [],
fillables: [],
guard: [],
// HOOKS for local changes:
insertHook: function (updateStore, doc, store) { return updateStore(doc) },
patchHook: function (updateStore, doc, store) { return updateStore(doc) },
deleteHook: function (updateStore, id, store) { return updateStore(id) },
// for batches
insertBatchHook: function (updateStore, docs, store) { return updateStore(docs) },
patchBatchHook: function (updateStore, doc, ids, store) { return updateStore(doc, ids) },
deleteBatchHook: function (updateStore, ids, store) { return updateStore(ids) },
},
// When items on the server side are changed:
serverChange: {
defaultValues: {},
// HOOKS for changes on SERVER:
addedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc) },
modifiedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc) },
removedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc) },
},
// When items are fetched through `dispatch('module/fetch', filters)`.
fetch: {
// The max amount of documents to be fetched. Defaults to 50.
docLimit: 50,
},
// You can also add custom state/getters/mutations/actions. These will be added to your module.
state: {},
getters: {},
mutations: {},
actions: {},
}Feedback
Do you have questions, comments, suggestions or feedback? Or any feature that's missing that you'd love to have? Feel free to open an issue! ♥
Planned future features:
- Make a blog post
- Add promise resolve callback possible on batch api calls
- Probably have to extract all batch call logic into a custom class
- Improve setting nested props of items with ID's
- Already possible with Vuex Easy Access, but need to think about how this library can handle it.
- Maybe something like
set('items/${id}.field', newVal)
- Maybe add possibility to force full patch on docs:
dispatch('module/fullPatch') - Improve error handling
- Warn about wrong config props
- Warn when there is a
_confstate prop
- Improve tests: test different configurations
- Improve tests: use a firestore mock
- Improve under the hood syntax (
_dbConfinstead of_conf) - Action to duplicate item(s)
- Improve 'patching' documentation for loaders/spinners
Also be sure to check out the sister vuex-plugin Vuex Easy Access!
--
Happy Vuexing!
-Luca Ban