├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── dist └── vue-firestore.js ├── jest.config.js ├── jest.setup.js ├── package.json ├── src ├── defaultOptions.js ├── main.js ├── utils │ └── utils.js └── vue-firestore.js ├── tests ├── TestCase.js ├── bind.spec.js └── vue-firestore.spec.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "env": { 4 | "test": { 5 | "plugins": ["@babel/plugin-transform-runtime"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true 5 | }, 6 | extends: ['plugin:vue/essential', 'airbnb-base', 'prettier'], 7 | globals: { 8 | Atomics: 'readonly', 9 | SharedArrayBuffer: 'readonly' 10 | }, 11 | parserOptions: { 12 | ecmaVersion: 2018, 13 | sourceType: 'module' 14 | }, 15 | plugins: ['vue', 'prettier'], 16 | rules: { 17 | 'prettier/prettier': ['error'], 18 | 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], 19 | 'no-param-reassign': ['warn'], 20 | 'guard-for-in': 0, 21 | 'no-restricted-syntax': 0 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | npm-debug.log 4 | package-lock.json 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | tests/ 3 | coverage/ 4 | webpack.config.js 5 | yarn.lock 6 | package-lock.json 7 | .travis.yml 8 | .eslintrc 9 | .babelrc -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | nguage: node_js 2 | node_js: 3 | - 6.10.0 4 | services: 5 | - xvfb 6 | before_install: 7 | - export DISPLAY=:99.0 8 | before_script: 9 | - npm install 10 | script: npm run test 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 GDG Tangier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 |

11 | 12 | ## vue-firestore 13 | 14 | Vue.js binding for firebase cloud firestore. 15 | 16 | ### Prerequisites 17 | 18 | Firebase `^7.6.1` 19 | 20 | ### Try it out: [Demo](http://jsbin.com/noviduy/7/edit?html,js,output) 21 | 22 | ### Installation 23 | 24 | #### Globally (Browser) 25 | 26 | vue-firestore will be installed automatically. 27 | 28 | ```html 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 52 | ``` 53 | 54 | #### npm 55 | 56 | Installation via npm : `npm install vue-firestore --save` 57 | 58 | ### Usage 59 | 60 | vue-firestore supports binding for the both (collections/docs) in realtime, you can bind your properties in two ways using `firestore` option or bind them manually with `$binding`. 61 | 62 | 1. using `firestore` option. 63 | 64 | ```javascript 65 | 66 | import Vue from 'vue' 67 | import VueFirestore from 'vue-firestore' 68 | import Firebase from 'firebase' 69 | 70 | require('firebase/firestore') 71 | 72 | Vue.use(VueFirestore) 73 | 74 | var firebaseApp = Firebase.initializeApp({ ... }) 75 | 76 | const firestore = firebaseApp.firestore(); 77 | 78 | var vm = new Vue({ 79 | el: '#app', 80 | firestore () { 81 | return { 82 | // Collection 83 | persons: firestore.collection('persons'), 84 | // Doc 85 |        ford: firestore.collection('cars').doc('ford') 86 | } 87 | } 88 | }) 89 | ``` 90 | 91 | You can pass an object to the `firestore()` function. 92 | 93 | As you may know, firestore source returns a promise, so you can handle it if it's resolved by `resolve` function 94 | or rejected by `reject` function, this case is really useful when we want to wait for data to be rendered and do some specific actions. 95 | 96 | ```javascript 97 | firestore () { 98 | return { 99 | persons: { 100 | // collection reference. 101 | ref: firestore.collection('persons'), 102 | // Bind the collection as an object if you would like to. 103 | objects: true, 104 | resolve: (data) => { 105 | // collection is resolved 106 | }, 107 | reject: (err) => { 108 | // collection is rejected 109 | } 110 | } 111 | } 112 | } 113 | 114 | ``` 115 | 116 | 2. Manually binding 117 | 118 | You can bind your docs/collection manually using `this.$binding`, and wait for data to be resolved, this case is really useful when we want to wait for data to be rendered and do some specific actions. 119 | 120 | ```javascript 121 | ... 122 | mounted () { 123 | // Binding Collections 124 | this.$binding("users", firestore.collection("users")) 125 | .then((users) => { 126 | console.log(users) // => __ob__: Observer 127 | }) 128 | 129 | // Binding Docs 130 | this.$binding("Ford", firestore.collection("cars").doc("ford")) 131 | .then((ford) => { 132 | console.log(ford) // => __ob__: Observer 133 | }).catch(err => { 134 | console.error(err) 135 | }) 136 | } 137 | ... 138 | ``` 139 | 140 | Vue firestore latest release supports binding collections as objects, you can bind the collection manually by `this.$bindCollectionAsObject(key, source)` or you can explicitly do that by adding `{objects: true}` to `firestore()` function, see the previous example above. 141 | 142 | The normalized resutls of `$bindCollectionAsObject`: 143 | 144 | ```javascript 145 | { 146 | tjlAXoQ3VAoNiJcka9: { 147 | firstname: "Jhon", 148 | lastname: "Doe" 149 | }, 150 | fb7AcoG3QAoCiJcKa9: { 151 | firstname: "Houssain", 152 | lastname: "Amrani" 153 | } 154 | } 155 | ``` 156 | 157 | 158 | You can get access to firestore properties with `this.$firestore`. 159 | 160 | #### Adding Data to collections 161 | 162 | ```javascript 163 | var vm = new Vue({ 164 | el: '#app', 165 | firestore: function () { 166 | return { 167 | persons: firestore.collection('persons') 168 | } 169 | }, 170 | methods:{ 171 | addData: function () { 172 | this.$firestore.persons.add({ 173 | firstname: "Amrani", 174 | lastname: "Houssain" 175 | }) 176 | } 177 | } 178 | }) 179 | ``` 180 | 181 | Each record of the array will contain a `.key` property which specifies the key where the record is stored. 182 | 183 | The Result of `persons` collection will be normalized as : 184 | 185 | ```json 186 | [ 187 | { 188 | ".key": "-Jtjl482BaXBCI7brMT8", 189 | "firstname": "Amrani", 190 | "lastname": "Houssain" 191 | }, 192 | { 193 | ".key": "-JtjlAXoQ3VAoNiJcka9", 194 | "firstname": "John", 195 | "lastname": "Doe" 196 | } 197 | ] 198 | ``` 199 | 200 | You could delete or update a json document of a collection using the property `.key` of a given object. 201 | 202 | ```javascript 203 | // Vue methods 204 | deletePerson: function (person) { 205 | this.$firestore.persons.doc(person['.key']).delete() 206 | }, 207 | updatePerson: function (person) { 208 | this.$firestore.persons.doc(person['.key']).update({ 209 | name: "Amrani Houssain" 210 | github: "@amranidev" 211 | }) 212 | } 213 | ``` 214 | 215 | You can customize the name of the `.key` property by passing an option when initializing vue-firestore: 216 | 217 | ```javascript 218 | require('firebase/firestore') 219 | 220 | Vue.use(VueFirestore, { 221 | key: 'id', // the name of the property. Default is '.key'. 222 | enumerable: true // whether it is enumerable or not. Default is true. 223 | }) 224 | ``` 225 | 226 | This would allow you to do `person.id` instead of `person['.key']`. 227 | 228 | 229 | 230 | ## More Resources 231 | - [Quick Start on Alligator.io](https://alligator.io/vuejs/vue-cloud-firestore/) 232 | 233 | ## LICENSE 234 | [MIT](https://opensource.org/licenses/MIT) 235 | -------------------------------------------------------------------------------- /dist/vue-firestore.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.VueFirestore=t():e.VueFirestore=t()}(window,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";r.r(t);var n,o=function(e){return"[object Object]"===Object.prototype.toString.call(e)},i=function(e,t){var r=e.doc?e.doc.data():e.data(),n=o(r)?r:{".value":r};return Object.defineProperty(n,t.keyName,{value:e.doc?e.doc.id:e.id,enumerable:t.enumerable}),n},c={keyName:".key",enumerable:!0};function u(e,t,r){t in e?e[t]=r:n.util.defineReactive(e,t,r)}function s(e){var t=e.vm,r=e.key,n=e.source,o=e.resolve,s=e.reject;t.$firestore[r]=n;var f=[];u(t,r,f),n.onSnapshot((function(e){e.docChanges().forEach((function(e){switch(e.type){case"added":f.splice(e.newIndex,0,i(e,c));break;case"removed":f.splice(e.oldIndex,1);break;case"modified":e.oldIndex!==e.newIndex?(f.splice(e.oldIndex,1),f.splice(e.newIndex,0,i(e,c))):f.splice(e.newIndex,1,i(e,c))}}),(function(e){s(e)})),o(f)}),(function(e){s(e)}))}function f(e){var t=e.vm,r=e.key,o=e.source,i=e.resolve,c=e.reject;t.$firestore[r]=o;var s={};u(t,r,s),o.onSnapshot((function(e){e.docChanges().forEach((function(e){switch(e.type){case"added":n.set(t[r],e.doc.id,e.doc.data());break;case"removed":n.delete(t[r],e.doc.id);break;case"modified":n.set(t[r],e.doc.id,e.doc.data())}}),(function(e){c(e)})),i(s)}),(function(e){c(e)}))}function a(e){var t=e.vm,r=e.key,n=e.source,o=e.resolve,s=e.reject;t.$firestore[r]=n;var f=[];u(t,r,f),n.onSnapshot((function(e){e.exists?(f=i(e,c),t[r]=f):(delete t.$firestore[r],s(new Error("This document (".concat(r,") is not exist or permission denied.")))),o(t[r])}),(function(e){s(e)}))}function d(e,t,r){var n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},i=null,c=null,u=!!n.objects||null;o(r)&&Object.prototype.hasOwnProperty.call(r,"ref")&&(i=r.resolve?r.resolve:function(){},c=r.reject?r.reject:function(){},u=!!r.objects||null,r=r.ref);var d=new Promise((function(n,o){u?f({vm:e,key:t,source:r,resolve:n,reject:o}):r.where?s({vm:e,key:t,source:r,resolve:n,reject:o}):a({vm:e,key:t,source:r,resolve:n,reject:o})}));return i||c?d.then((function(e){return i(e)})).catch((function(e){return c(e)})):d}var l={created:function(){var e,t=this.$options.firestore;if("function"==typeof t&&(t=t.call(this)),t)for(var r in(e=this).$firestore||(e.$firestore=Object.create(null)),t)d(this,r,t[r])},beforeDestroy:function(){if(this.$firestore){for(var e in this.$firestore)this.$firestore[e]&&this.$unbind(e);this.$firestore=null}}},p=function(e,t){n=e,t&&t.key&&(c.keyName=t.key),t&&void 0!==t.enumerable&&(c.enumerable=t.enumerable),n.mixin(l);var r=n.config.optionMergeStrategies;r.fireStore=r.methods,n.prototype.$binding=function(e,t){return this.$firestore||(this.$firestore=Object.create(null)),d(this,e,t)},n.prototype.$bindCollectionAsObject=function(e,t){return this.$firestore||(this.$firestore=Object.create(null)),d(this,e,t,{objects:!0})},n.prototype.$unbind=function(e){delete this.$firestore[e]}};"undefined"!=typeof window&&window.Vue&&p(window.Vue);var v=p;t.default=v}])})); -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ['./jest.setup.js'] 3 | } 4 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(100000) 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-firestore", 3 | "version": "0.3.30", 4 | "description": "Vue plugin for firestore", 5 | "main": "./dist/vue-firestore.js", 6 | "scripts": { 7 | "test": "jest --forceExit --detectOpenHandles", 8 | "lint": "eslint --fix src", 9 | "build": "webpack" 10 | }, 11 | "keywords": [ 12 | "vue", 13 | "firebase", 14 | "cloud", 15 | "cloud firestore", 16 | "realtime" 17 | ], 18 | "author": "Amrani Houssain", 19 | "license": "MIT", 20 | "peerDependencies": { 21 | "firebase": "^7.6.1" 22 | }, 23 | "dependencies": { 24 | "@babel/core": "^7.7.7", 25 | "@babel/preset-env": "^7.7.7", 26 | "babel-loader": "^8.0.6", 27 | "cross-env": "^6.0.3", 28 | "firebase": "^7.6.1", 29 | "vue": "^2.6.11" 30 | }, 31 | "devDependencies": { 32 | "@babel/plugin-transform-runtime": "^7.7.6", 33 | "@babel/runtime": "^7.7.7", 34 | "codecov": "^3.6.1", 35 | "eslint": "^5.16.0", 36 | "eslint-config-airbnb-base": "^14.0.0", 37 | "eslint-config-prettier": "^6.9.0", 38 | "eslint-plugin-import": "^2.19.1", 39 | "eslint-plugin-prettier": "^3.1.2", 40 | "eslint-plugin-vue": "^6.1.2", 41 | "jest": "^24.9.0", 42 | "prettier": "^1.19.1", 43 | "webpack": "^4.41.5", 44 | "webpack-cli": "^3.3.10" 45 | }, 46 | "jest": { 47 | "collectCoverage": true, 48 | "collectCoverageFrom": [ 49 | "/src/*.js" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/defaultOptions.js: -------------------------------------------------------------------------------- 1 | // Plugin options 2 | export default { 3 | keyName: '.key', 4 | enumerable: true 5 | }; 6 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import VueFireStore from './vue-firestore'; 2 | 3 | export default VueFireStore; 4 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if a record is an object. 3 | * 4 | * @param {*} val 5 | * @return {boolean} 6 | */ 7 | const isObject = val => { 8 | return Object.prototype.toString.call(val) === '[object Object]'; 9 | }; 10 | 11 | /** 12 | * Normalize Firebase snapshot into a bindable data record. 13 | * 14 | * @param {FirebaseSnapshot} snapshot 15 | * @param {object} options 16 | * @return {object} 17 | */ 18 | const normalize = (snapshot, options) => { 19 | const value = snapshot.doc ? snapshot.doc.data() : snapshot.data(); 20 | const out = isObject(value) ? value : { '.value': value }; 21 | Object.defineProperty(out, options.keyName, { 22 | value: snapshot.doc ? snapshot.doc.id : snapshot.id, 23 | enumerable: options.enumerable 24 | }); 25 | return out; 26 | }; 27 | 28 | /** 29 | * Ensure firebasestore option on a vue instance. 30 | * 31 | * @param {Vue} vm 32 | */ 33 | const ensureRefs = vm => { 34 | if (!vm.$firestore) { 35 | vm.$firestore = Object.create(null); 36 | } 37 | }; 38 | 39 | export { isObject, normalize, ensureRefs }; 40 | -------------------------------------------------------------------------------- /src/vue-firestore.js: -------------------------------------------------------------------------------- 1 | import { normalize, ensureRefs, isObject } from './utils/utils'; 2 | import defaultOptions from './defaultOptions'; 3 | 4 | // Vue binding 5 | let Vue; 6 | 7 | /** 8 | * Define a reactive property to a given Vue instance. 9 | * 10 | * @param {Vue} vm 11 | * @param {string} key 12 | * @param {*} val 13 | */ 14 | function defineReactive(vm, key, val) { 15 | if (key in vm) { 16 | vm[key] = val; 17 | } else { 18 | Vue.util.defineReactive(vm, key, val); 19 | } 20 | } 21 | 22 | /** 23 | * Bind firestore collection. 24 | * 25 | * @param {Vue} options.vm 26 | * @param {string} options.key 27 | * @param {object} options.source 28 | * @param {function} options.resolve 29 | * @param {function} options.reject 30 | */ 31 | function collections({ vm, key, source, resolve, reject }) { 32 | vm.$firestore[key] = source; 33 | const container = []; 34 | defineReactive(vm, key, container); 35 | source.onSnapshot( 36 | doc => { 37 | doc.docChanges().forEach( 38 | snapshot => { 39 | switch (snapshot.type) { 40 | case 'added': 41 | container.splice(snapshot.newIndex, 0, normalize(snapshot, defaultOptions)); 42 | break; 43 | case 'removed': 44 | container.splice(snapshot.oldIndex, 1); 45 | break; 46 | case 'modified': 47 | if (snapshot.oldIndex !== snapshot.newIndex) { 48 | container.splice(snapshot.oldIndex, 1); 49 | container.splice(snapshot.newIndex, 0, normalize(snapshot, defaultOptions)); 50 | } else { 51 | container.splice(snapshot.newIndex, 1, normalize(snapshot, defaultOptions)); 52 | } 53 | break; 54 | default: 55 | break; 56 | } 57 | }, 58 | error => { 59 | reject(error); 60 | } 61 | ); 62 | resolve(container); 63 | }, 64 | error => { 65 | reject(error); 66 | } 67 | ); 68 | } 69 | 70 | /** 71 | * Bind as a collection of objects. 72 | * 73 | * @param {Vue} options.vm 74 | * @param {string} options.key 75 | * @param {object} options.source 76 | * @param {function} options.resolve 77 | * @param {function} options.reject 78 | */ 79 | function collectionOfObjects({ vm, key, source, resolve, reject }) { 80 | vm.$firestore[key] = source; 81 | const container = {}; 82 | defineReactive(vm, key, container); 83 | source.onSnapshot( 84 | doc => { 85 | doc.docChanges().forEach( 86 | snapshot => { 87 | switch (snapshot.type) { 88 | case 'added': 89 | Vue.set(vm[key], snapshot.doc.id, snapshot.doc.data()); 90 | break; 91 | case 'removed': 92 | Vue.delete(vm[key], snapshot.doc.id); 93 | break; 94 | case 'modified': 95 | Vue.set(vm[key], snapshot.doc.id, snapshot.doc.data()); 96 | break; 97 | default: 98 | break; 99 | } 100 | }, 101 | error => { 102 | reject(error); 103 | } 104 | ); 105 | resolve(container); 106 | }, 107 | error => { 108 | reject(error); 109 | } 110 | ); 111 | } 112 | 113 | /** 114 | * Bind firestore document. 115 | * 116 | * @param {Vue} options.vm 117 | * @param {string} options.key 118 | * @param {object} options.source 119 | * @param {function} options.resolve 120 | * @param {function} options.reject 121 | */ 122 | function documents({ vm, key, source, resolve, reject }) { 123 | vm.$firestore[key] = source; 124 | let container = []; 125 | defineReactive(vm, key, container); 126 | source.onSnapshot( 127 | doc => { 128 | if (doc.exists) { 129 | container = normalize(doc, defaultOptions); 130 | vm[key] = container; 131 | } else { 132 | delete vm.$firestore[key]; 133 | reject(new Error(`This document (${key}) is not exist or permission denied.`)); 134 | } 135 | resolve(vm[key]); 136 | }, 137 | error => { 138 | reject(error); 139 | } 140 | ); 141 | } 142 | 143 | /** 144 | * Listen for changes, and bind firestore doc source to a key on a Vue instance. 145 | * 146 | * @param {Vue} vm 147 | * @param {string} key 148 | * @param {object} source 149 | * @param {Object} params 150 | */ 151 | function bind(vm, key, source, params = {}) { 152 | let resolve = null; 153 | let reject = null; 154 | let objects = params.objects ? true : null; 155 | 156 | if (isObject(source) && Object.prototype.hasOwnProperty.call(source, 'ref')) { 157 | // if the firebase source has (ref) key, we gets the the resolve and reject functions as callbacks 158 | // and use them when the promise is resolved or rejected. 159 | resolve = source.resolve ? source.resolve : () => {}; 160 | reject = source.reject ? source.reject : () => {}; 161 | objects = source.objects ? true : null; 162 | source = source.ref; 163 | } 164 | 165 | const binding = new Promise((resolve, reject) => { 166 | if (objects) { 167 | collectionOfObjects({ vm, key, source, resolve, reject }); 168 | } else if (source.where) { 169 | collections({ vm, key, source, resolve, reject }); 170 | } else { 171 | documents({ vm, key, source, resolve, reject }); 172 | } 173 | }); 174 | 175 | if (resolve || reject) { 176 | return binding.then(res => resolve(res)).catch(err => reject(err)); 177 | } 178 | 179 | return binding; 180 | } 181 | 182 | /** 183 | * Initialize. 184 | */ 185 | const created = function created() { 186 | let bindings = this.$options.firestore; 187 | if (typeof bindings === 'function') bindings = bindings.call(this); 188 | if (!bindings) return; 189 | ensureRefs(this); 190 | for (const key in bindings) { 191 | bind(this, key, bindings[key]); 192 | } 193 | }; 194 | 195 | /** 196 | * Before Destroy. 197 | */ 198 | const beforeDestroy = function beforeDestroy() { 199 | if (!this.$firestore) return; 200 | for (const key in this.$firestore) { 201 | if (this.$firestore[key]) { 202 | this.$unbind(key); 203 | } 204 | } 205 | this.$firestore = null; 206 | }; 207 | 208 | /** 209 | * Vue Mixin 210 | */ 211 | const Mixin = { 212 | created, 213 | beforeDestroy 214 | }; 215 | 216 | /** 217 | * Install function. 218 | * 219 | * @param {Vue} _Vue 220 | * @param {object} options 221 | */ 222 | const install = function install(_Vue, options) { 223 | Vue = _Vue; 224 | if (options && options.key) defaultOptions.keyName = options.key; 225 | if (options && options.enumerable !== undefined) defaultOptions.enumerable = options.enumerable; 226 | Vue.mixin(Mixin); 227 | const mergeStrats = Vue.config.optionMergeStrategies; 228 | mergeStrats.fireStore = mergeStrats.methods; 229 | 230 | // Manually binding 231 | Vue.prototype.$binding = function binding(key, source) { 232 | if (!this.$firestore) { 233 | this.$firestore = Object.create(null); 234 | } 235 | 236 | return bind(this, key, source); 237 | }; 238 | 239 | // Bind Collection As Object 240 | Vue.prototype.$bindCollectionAsObject = function bindCollectionAsObject(key, source) { 241 | if (!this.$firestore) { 242 | this.$firestore = Object.create(null); 243 | } 244 | 245 | return bind(this, key, source, { objects: true }); 246 | }; 247 | 248 | Vue.prototype.$unbind = function unbind(key) { 249 | delete this.$firestore[key]; 250 | }; 251 | }; 252 | 253 | // Install automatically (browser). 254 | if (typeof window !== 'undefined' && window.Vue) { 255 | install(window.Vue); 256 | } 257 | 258 | export default install; 259 | -------------------------------------------------------------------------------- /tests/TestCase.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import VueFirestore from '../src/vue-firestore.js'; 4 | 5 | import Firebase from 'firebase'; 6 | 7 | var config = { 8 | apiKey: 'AIzaSyB9Trlbrpo48ilkNHZ6MGbofFf2u8uHuRA', 9 | authDomain: 'oss-test-myfirebase.firebaseapp.com', 10 | databaseURL: 'https://oss-test-myfirebase.firebaseio.com', 11 | projectId: 'oss-test-myfirebase', 12 | storageBucket: 'oss-test-myfirebase.appspot.com', 13 | messagingSenderId: '10529373536' 14 | }; 15 | 16 | Vue.use(VueFirestore); 17 | 18 | import 'firebase/firestore'; 19 | 20 | var firebase = Firebase.initializeApp(config); 21 | var firestore = firebase.firestore(); 22 | 23 | export function VueTick() { 24 | return new Promise((resolve, reject) => { 25 | Vue.nextTick(resolve); 26 | }); 27 | } 28 | 29 | export function randomString() { 30 | var string = ''; 31 | var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 32 | 33 | for (var i = 0; i < 5; i++) { 34 | string += possible.charAt(Math.floor(Math.random() * possible.length)); 35 | } 36 | 37 | return string; 38 | } 39 | 40 | export { Vue }; 41 | 42 | export { firebase }; 43 | 44 | export { firestore }; 45 | -------------------------------------------------------------------------------- /tests/bind.spec.js: -------------------------------------------------------------------------------- 1 | import { Vue, firebase, firestore, VueTick, randomString } from './TestCase'; 2 | 3 | let vm, collection, doc; 4 | let collectionName = randomString(); 5 | let objectCollectionName = randomString(); 6 | let documentName = randomString(); 7 | describe('Manual binding', () => { 8 | beforeEach(async () => { 9 | collection = firestore.collection('items'); 10 | doc = firestore.doc(`collectionOfDocs/doc`); 11 | vm = new Vue({ 12 | firestore: () => ({ 13 | persons: { 14 | ref: firestore.collection(objectCollectionName), 15 | objects: true, 16 | resolve: async response => { 17 | await response; 18 | }, 19 | reject: async error => { 20 | await error; 21 | } 22 | }, 23 | projects: { 24 | ref: firestore.collection('projects'), 25 | resolve: async response => { 26 | await response; 27 | }, 28 | reject: async error => { 29 | await error; 30 | } 31 | } 32 | }) 33 | }); 34 | await VueTick(); 35 | }); 36 | 37 | test('Bind collection manually', async () => { 38 | // Bind Collection 39 | await vm.$binding(collectionName, firestore.collection(collectionName)); 40 | expect(vm[collectionName]).toEqual([]); 41 | 42 | // Add To Collection 43 | await vm.$firestore[collectionName].add({ name: 'item' }); 44 | expect(vm[collectionName].length).toEqual(1); 45 | expect(vm[collectionName][0].name).toEqual('item'); 46 | 47 | // Update Collection Item 48 | let updatedItem = vm[collectionName][0]; 49 | await vm.$firestore[collectionName].doc(updatedItem['.key']).update({ name: 'item2' }); 50 | expect(vm[collectionName].length).toEqual(1); 51 | expect(vm[collectionName][0].name).toEqual('item2'); 52 | 53 | // Delete Collection item 54 | let deletedItem = vm[collectionName][0]; 55 | await vm.$firestore[collectionName].doc(deletedItem['.key']).delete(); 56 | expect(vm[collectionName].length).toEqual(0); 57 | }); 58 | 59 | test('Bind document manually', async () => { 60 | // Bind Document 61 | await vm.$binding(documentName, doc); 62 | expect(vm.$firestore[documentName]).toBe(doc); 63 | expect(vm[documentName]).toEqual({ '.key': 'doc', name: 'docName' }); 64 | 65 | // Update Document 66 | await vm.$firestore[documentName].update({ name: 'docName2' }); 67 | expect(vm[documentName].name).toEqual('docName2'); 68 | 69 | // Just making sure that the next tests not going to be failed XD 70 | await vm.$firestore[documentName].update({ name: 'docName' }); 71 | expect(vm[documentName].name).toEqual('docName'); 72 | }); 73 | 74 | test('Binding collection returns promise', async () => { 75 | expect(vm.$binding('someCollections', collection) instanceof Promise).toBe(true); 76 | }); 77 | 78 | test('Binding document returns promise', async () => { 79 | expect(vm.$binding('someCollections', doc) instanceof Promise).toBe(true); 80 | }); 81 | 82 | test('Binding collection as object', async () => { 83 | expect(typeof vm['persons']).toBe('object'); 84 | }); 85 | 86 | test('Add/Update/Delete an item to object collection', async () => { 87 | // Add to Collection 88 | await vm.$firestore.persons.add({ name: 'item' }); 89 | expect(Object.keys(vm['persons']).length).toEqual(1); 90 | let objectKey = Object.keys(vm['persons'])[0]; 91 | expect(vm['persons'][objectKey].name).toEqual('item'); 92 | 93 | // Update item in collection 94 | await vm.$firestore.persons.doc(objectKey).update({ name: 'item2' }); 95 | expect(vm['persons'][objectKey].name).toEqual('item2'); 96 | 97 | // Delete the Item 98 | await vm.$firestore.persons.doc(objectKey).delete(); 99 | expect(Object.keys(vm['persons']).length).toEqual(0); 100 | }); 101 | 102 | test('Can bind multiple references', async () => { 103 | await vm.$binding('cars', firestore.collection('cars')); 104 | await vm.$binding(documentName, doc); 105 | await vm.$bindCollectionAsObject('objects', firestore.collection('objects')); 106 | expect(Object.keys(vm.$firestore)).toEqual([ 107 | 'persons', 108 | 'projects', 109 | 'cars', 110 | documentName, 111 | 'objects' 112 | ]); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /tests/vue-firestore.spec.js: -------------------------------------------------------------------------------- 1 | import { Vue, firebase, firestore, VueTick, randomString } from './TestCase'; 2 | 3 | let vm, collection; 4 | 5 | describe('vue-firestore', () => { 6 | beforeEach(async () => { 7 | collection = firestore.collection('items'); 8 | vm = new Vue({ 9 | data: () => ({ 10 | items: null 11 | }), 12 | firestore() { 13 | return { 14 | items: collection 15 | }; 16 | } 17 | }); 18 | await VueTick(); 19 | }); 20 | 21 | test('setup $firestore', () => { 22 | expect(Object.keys(vm.$firestore).sort()).toEqual(['items']); 23 | expect(vm.$firestore.items).toBe(collection); 24 | }); 25 | 26 | test('unbind $firestore on $destroy', () => { 27 | vm.$destroy(); 28 | expect(vm.$firestore).toEqual(null); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | entry: './src/main.js', 7 | output: { 8 | path: path.resolve(__dirname, './dist'), 9 | filename: 'vue-firestore.js', 10 | library: 'VueFirestore', 11 | libraryTarget: 'umd' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|jsx)$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: 'babel-loader' 20 | } 21 | } 22 | ] 23 | } 24 | }; 25 | --------------------------------------------------------------------------------