├── .gitignore ├── circle.yml ├── tests ├── .eslintrc ├── helpers.js └── wildvue.spec.js ├── ci.sh ├── .eslintrc ├── karma.conf.js ├── LICENSE ├── examples └── todoApp │ └── index.html ├── package.json ├── dist ├── wildvue.min.js └── wildvue.js ├── README.md └── src └── wildvue.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 5 4 | 5 | test: 6 | override: 7 | - bash ./ci.sh 8 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | if [ -z "$CI_PULL_REQUEST" ] 3 | then 4 | npm test 5 | cat ./coverage/lcov.info | ./node_modules/.bin/codecov 6 | else 7 | npm test 8 | fi 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "plugins": ["html"], 4 | "env": { 5 | "amd": true 6 | }, 7 | "rules": { 8 | "no-new": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | exports.invalidFirebaseRefs = [null, undefined, true, false, [], 0, 5, '', 'a', ['hi', 1]] 2 | 3 | /* Returns a random alphabetic string of variable length */ 4 | exports.generateRandomString = function () { 5 | var possibleCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 6 | var numPossibleCharacters = possibleCharacters.length 7 | 8 | var text = '' 9 | for (var i = 0; i < 10; i++) { 10 | text += possibleCharacters.charAt(Math.floor(Math.random() * numPossibleCharacters)) 11 | } 12 | 13 | return text 14 | } 15 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | frameworks: ['mocha', 'sinon-chai'], 6 | browsers: ['PhantomJS'], 7 | reporters: ['spec', 'coverage'], 8 | files: [ 9 | 'tests/wildvue.spec.js' 10 | ], 11 | preprocessors: { 12 | 'tests/wildvue.spec.js': ['webpack', 'sourcemap'] 13 | }, 14 | client: { 15 | mocha: { 16 | timeout: 100000 17 | } 18 | }, 19 | browserNoActivityTimeout: 100000, 20 | webpack: { 21 | devtool: '#inline-source-map', 22 | module: { 23 | loaders: [{ 24 | include: path.resolve(__dirname, 'src/wildvue.js'), 25 | loader: 'istanbul-instrumenter' 26 | }] 27 | } 28 | }, 29 | webpackMiddleware: { 30 | noInfo: true 31 | }, 32 | coverageReporter: { 33 | reporters: [ 34 | { type: 'lcov', subdir: '.' }, 35 | { type: 'text-summary' } 36 | ] 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Evan You 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/todoApp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WildVue Todo App Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 19 |
20 | 21 | 22 |
23 |
24 | 25 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wildvue", 3 | "version": "1.1.1", 4 | "description": "Wilddog bindings for Vue.js", 5 | "main": "dist/wildvue.js", 6 | "files": [ 7 | "dist/**", 8 | "LICENSE", 9 | "README.md" 10 | ], 11 | "scripts": { 12 | "lint": "eslint --ext=js,html src tests examples karma.conf.js", 13 | "test": "npm run lint && karma start karma.conf.js --single-run", 14 | "dev": "webpack src/wildvue.js dist/wildvue.js --watch", 15 | "build-dev": "webpack src/wildvue.js dist/wildvue.js --output-library=WildVue --output-library-target=umd", 16 | "build-prod": "webpack src/wildvue.js dist/wildvue.min.js --output-library=WildVue --output-library-target=umd -p", 17 | "build": "npm run build-dev && npm run build-prod" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/vuejs/wildvue.git" 22 | }, 23 | "keywords": [ 24 | "vue", 25 | "mixin", 26 | "wilddog", 27 | "realtime" 28 | ], 29 | "author": "wilddog", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/vuejs/wildvue/issues" 33 | }, 34 | "homepage": "https://github.com/vuejs/wildvue#readme", 35 | "devDependencies": { 36 | "chai": "^3.5.0", 37 | "codecov": "^1.0.1", 38 | "eslint": "^3.0.0", 39 | "eslint-config-standard": "^5.1.0", 40 | "eslint-plugin-html": "^1.5.2", 41 | "eslint-plugin-promise": "^3.3.0", 42 | "eslint-plugin-standard": "^2.0.1", 43 | "istanbul-instrumenter-loader": "^0.2.0", 44 | "karma": "^0.13.22", 45 | "karma-coverage": "^0.5.5", 46 | "karma-mocha": "^0.2.2", 47 | "karma-phantomjs-launcher": "^1.0.0", 48 | "karma-sinon-chai": "^1.2.0", 49 | "karma-sourcemap-loader": "^0.3.7", 50 | "karma-spec-reporter": "0.0.24", 51 | "karma-webpack": "^1.8.0", 52 | "lolex": "^1.4.0", 53 | "mocha": "^2.4.5", 54 | "phantomjs-prebuilt": "^2.1.3", 55 | "sinon": "^1.17.3", 56 | "sinon-chai": "^2.8.0", 57 | "vue": "^1.0.19", 58 | "webpack": "^1.12.14", 59 | "wilddog": "^2.3.10" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /dist/wildvue.min.js: -------------------------------------------------------------------------------- 1 | !function(e,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.WildVue=o():e.WildVue=o()}(this,function(){return function(e){function o(n){if(i[n])return i[n].exports;var t=i[n]={exports:{},id:n,loaded:!1};return e[n].call(t.exports,t,t.exports,o),t.loaded=!0,t.exports}var i={};return o.m=e,o.c=i,o.p="",o(0)}([function(e,o){function i(e){return"function"==typeof e.key?e.key():e.key}function n(e){return"function"==typeof e.ref?e=e.ref():"object"==typeof e.ref&&(e=e.ref),e}function t(e){return"[object Object]"===Object.prototype.toString.call(e)}function r(e){var o=e.val(),n=t(o)?o:{".value":o};return n[".key"]=i(e),n}function d(e,o){for(var i=0;i` 全局引入: 如果`Vue` 存在会自动安装。 8 | 9 | ``` html 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ``` 19 | 20 | 2. 在模块化环境中,比如CommonJS 21 | 22 | ``` bash 23 | npm install vue wilddog wildvue --save 24 | ``` 25 | 26 | ``` js 27 | var Vue = require('vue') 28 | var WildVue = require('wildvue') 29 | var wilddog = require('wilddog') 30 | // 在模块化环境中需要使用 user 安装 31 | Vue.use(WildVue) 32 | ``` 33 | 34 | ## 使用 35 | 36 | ``` js 37 | var wilddogApp = wilddog.initializeApp({ ... }) 38 | var sync = wilddogApp.sync() 39 | var vm = new Vue({ 40 | el: '#demo', 41 | wilddog: { 42 | //简单语法 43 | //默认作为数组绑定 44 | anArray: sync.ref('url/to/my/collection'), 45 | // 也可以绑定一个查询 46 | // anArray: sync.ref('url/to/my/collection').limitToLast(25) 47 | // 完整语法 48 | anObject: { 49 | source: new Wilddog('url/to/my/object'), 50 | // 可选,作为对象绑定 51 | asObject: true, 52 | // 可选,提供一个回调 53 | cancelCallback: function () {} 54 | } 55 | } 56 | }) 57 | ``` 58 | 59 | ``` html 60 |
61 |
{{ anObject | json }}
62 | 65 |
66 | ``` 67 | 68 | 上面的例子会绑定一个Vue实例的`anObject` 和 `anArray` 到相应的 Wilddog 数据源。另外,Vue实例也可以使用 `$wilddogRefs` property: 69 | 70 | ``` js 71 | // add an item to the array 72 | vm.$wilddogRefs.anArray.push({ 73 | text: 'hello' 74 | }) 75 | ``` 76 | 77 | 另外,你也可以使用 `$bindAsObject` 或 `$bindAsArray` 方法绑定一个Wildog ref: 78 | 79 | ``` js 80 | vm.$bindAsObject('user', myWilddogRef.child('user')) 81 | vm.$bindAsArray('items', myWilddogRef.child('items').limitToLast(25)) 82 | ``` 83 | 84 | ## 数据归一化 85 | 86 | ### 数组绑定 87 | 88 | 在绑定数组中的每一条记录中都会包含一个 `.key` property,用来表示这条记录的key。 所以,如果你有书记在 `/items/-Jtjl482BaXBCI7brMT8/` , 这条记录会存在一个 `.key:"-Jtjl482BaXBCI7brMT8"`。 89 | 如果value是简单数据类型(boolean,string,number)会保存在`.value` property 中。如果value 是对象,value对象中每个property 都会直接存放在记录中: 90 | 91 | ``` json 92 | { 93 | "items": { 94 | "-Jtjl482BaXBCI7brMT8": 100, 95 | "-Jtjl6tmqjNeAnQvyD4l": { 96 | "first": "fred", 97 | "last": "Flintstone" 98 | }, 99 | "-JtjlAXoQ3VAoNiJcka9": "foo" 100 | } 101 | } 102 | ``` 103 | 104 | The resulting bound array stored in `vm.items` will be: 105 | 106 | ``` json 107 | [ 108 | { 109 | ".key": "-Jtjl482BaXBCI7brMT8", 110 | ".value": 100 111 | }, 112 | { 113 | ".key": "-Jtjl6tmqjNeAnQvyD4l", 114 | "first": "Fred", 115 | "last": "Flintstone" 116 | }, 117 | { 118 | ".key": "-JtjlAXoQ3VAoNiJcka9", 119 | ".value": "foo" 120 | } 121 | ] 122 | ``` 123 | 124 | ## 贡献 125 | 126 | Clone the repo, then: 127 | 128 | ```bash 129 | $ npm install # install dependencies 130 | $ npm test # run test suite with coverage report 131 | $ npm run dev # watch and build dist/wildvue.js 132 | $ npm run build # build dist/wildvue.js and wildvue.min.js 133 | ``` 134 | ## License 135 | 136 | [MIT](http://opensource.org/licenses/MIT) -------------------------------------------------------------------------------- /src/wildvue.js: -------------------------------------------------------------------------------- 1 | var Vue // late binding 2 | 3 | /** 4 | * Returns the key of a Wilddog snapshot across SDK versions. 5 | * 6 | * @param {WilddogSnapshot} snapshot 7 | * @return {string|null} 8 | */ 9 | function _getKey (snapshot) { 10 | return typeof snapshot.key === 'function' 11 | ? snapshot.key() 12 | : snapshot.key 13 | } 14 | 15 | /** 16 | * Returns the original reference of a Wilddog reference or query across SDK versions. 17 | * 18 | * @param {WilddogReference|WilddogQuery} refOrQuery 19 | * @return {WilddogReference} 20 | */ 21 | function _getRef (refOrQuery) { 22 | if (typeof refOrQuery.ref === 'function') { 23 | refOrQuery = refOrQuery.ref() 24 | } else if (typeof refOrQuery.ref === 'object') { 25 | refOrQuery = refOrQuery.ref 26 | } 27 | 28 | return refOrQuery 29 | } 30 | 31 | /** 32 | * Check if a value is an object. 33 | * 34 | * @param {*} val 35 | * @return {boolean} 36 | */ 37 | function isObject (val) { 38 | return Object.prototype.toString.call(val) === '[object Object]' 39 | } 40 | 41 | /** 42 | * Convert wilddog snapshot into a bindable data record. 43 | * 44 | * @param {WilddogSnapshot} snapshot 45 | * @return {Object} 46 | */ 47 | function createRecord (snapshot) { 48 | var value = snapshot.val() 49 | var res = isObject(value) 50 | ? value 51 | : { '.value': value } 52 | res['.key'] = _getKey(snapshot) 53 | return res 54 | } 55 | 56 | /** 57 | * Find the index for an object with given key. 58 | * 59 | * @param {array} array 60 | * @param {string} key 61 | * @return {number} 62 | */ 63 | function indexForKey (array, key) { 64 | for (var i = 0; i < array.length; i++) { 65 | if (array[i]['.key'] === key) { 66 | return i 67 | } 68 | } 69 | /* istanbul ignore next */ 70 | return -1 71 | } 72 | 73 | /** 74 | * Bind a wilddog data source to a key on a vm. 75 | * 76 | * @param {Vue} vm 77 | * @param {string} key 78 | * @param {object} source 79 | */ 80 | function bind (vm, key, source) { 81 | var asObject = false 82 | var cancelCallback = null 83 | // check { source, asArray, cancelCallback } syntax 84 | if (isObject(source) && source.hasOwnProperty('source')) { 85 | asObject = source.asObject 86 | cancelCallback = source.cancelCallback 87 | source = source.source 88 | } 89 | if (!isObject(source)) { 90 | throw new Error('WildVue: invalid Wilddog binding source.') 91 | } 92 | // get the original ref for possible queries 93 | var ref = _getRef(source) 94 | vm.$wilddogRefs[key] = ref 95 | vm._wilddogSources[key] = source 96 | // bind based on initial value type 97 | if (asObject) { 98 | bindAsObject(vm, key, source, cancelCallback) 99 | } else { 100 | bindAsArray(vm, key, source, cancelCallback) 101 | } 102 | } 103 | 104 | /** 105 | * Define a reactive property in a given vm if it's not defined 106 | * yet 107 | * 108 | * @param {Vue} vm 109 | * @param {string} key 110 | * @param {*} val 111 | */ 112 | function defineReactive (vm, key, val) { 113 | if (key in vm) { 114 | vm[key] = val 115 | } else { 116 | Vue.util.defineReactive(vm, key, val) 117 | } 118 | } 119 | 120 | /** 121 | * Bind a wilddog data source to a key on a vm as an Array. 122 | * 123 | * @param {Vue} vm 124 | * @param {string} key 125 | * @param {object} source 126 | * @param {function|null} cancelCallback 127 | */ 128 | function bindAsArray (vm, key, source, cancelCallback) { 129 | var array = [] 130 | defineReactive(vm, key, array) 131 | 132 | var onAdd = source.on('child_added', function (snapshot, prevKey) { 133 | var index = prevKey ? indexForKey(array, prevKey) + 1 : 0 134 | array.splice(index, 0, createRecord(snapshot)) 135 | }, cancelCallback) 136 | 137 | var onRemove = source.on('child_removed', function (snapshot) { 138 | var index = indexForKey(array, _getKey(snapshot)) 139 | array.splice(index, 1) 140 | }, cancelCallback) 141 | 142 | var onChange = source.on('child_changed', function (snapshot) { 143 | var index = indexForKey(array, _getKey(snapshot)) 144 | array.splice(index, 1, createRecord(snapshot)) 145 | }, cancelCallback) 146 | 147 | var onMove = source.on('child_moved', function (snapshot, prevKey) { 148 | var index = indexForKey(array, _getKey(snapshot)) 149 | var record = array.splice(index, 1)[0] 150 | var newIndex = prevKey ? indexForKey(array, prevKey) + 1 : 0 151 | array.splice(newIndex, 0, record) 152 | }, cancelCallback) 153 | 154 | vm._wilddogListeners[key] = { 155 | child_added: onAdd, 156 | child_removed: onRemove, 157 | child_changed: onChange, 158 | child_moved: onMove 159 | } 160 | } 161 | 162 | /** 163 | * Bind a wilddog data source to a key on a vm as an Object. 164 | * 165 | * @param {Vue} vm 166 | * @param {string} key 167 | * @param {Object} source 168 | * @param {function|null} cancelCallback 169 | */ 170 | function bindAsObject (vm, key, source, cancelCallback) { 171 | defineReactive(vm, key, {}) 172 | var cb = source.on('value', function (snapshot) { 173 | vm[key] = createRecord(snapshot) 174 | }, cancelCallback) 175 | vm._wilddogListeners[key] = { value: cb } 176 | } 177 | 178 | /** 179 | * Unbind a wilddog-bound key from a vm. 180 | * 181 | * @param {Vue} vm 182 | * @param {string} key 183 | */ 184 | function unbind (vm, key) { 185 | var source = vm._wilddogSources && vm._wilddogSources[key] 186 | if (!source) { 187 | throw new Error( 188 | 'WildVue: unbind failed: "' + key + '" is not bound to ' + 189 | 'a Wilddog reference.' 190 | ) 191 | } 192 | var listeners = vm._wilddogListeners[key] 193 | for (var event in listeners) { 194 | source.off(event, listeners[event]) 195 | } 196 | vm[key] = null 197 | vm.$wilddogRefs[key] = null 198 | vm._wilddogSources[key] = null 199 | vm._wilddogListeners[key] = null 200 | } 201 | 202 | /** 203 | * Ensure the related bookkeeping variables on an instance. 204 | * 205 | * @param {Vue} vm 206 | */ 207 | function ensureRefs (vm) { 208 | if (!vm.$wilddogRefs) { 209 | vm.$wilddogRefs = Object.create(null) 210 | vm._wilddogSources = Object.create(null) 211 | vm._wilddogListeners = Object.create(null) 212 | } 213 | } 214 | 215 | var init = function () { 216 | var bindings = this.$options.wilddog 217 | if (!bindings) return 218 | ensureRefs(this) 219 | for (var key in bindings) { 220 | bind(this, key, bindings[key]) 221 | } 222 | } 223 | 224 | var WildVueMixin = { 225 | init: init, // 1.x 226 | beforeCreate: init, // 2.x 227 | beforeDestroy: function () { 228 | if (!this.$wilddogRefs) return 229 | for (var key in this.$wilddogRefs) { 230 | if (this.$wilddogRefs[key]) { 231 | this.$unbind(key) 232 | } 233 | } 234 | this.$wilddogRefs = null 235 | this._wilddogSources = null 236 | this._wilddogListeners = null 237 | } 238 | } 239 | 240 | /** 241 | * Install function passed to Vue.use() in manual installation. 242 | * 243 | * @param {function} _Vue 244 | */ 245 | function install (_Vue) { 246 | Vue = _Vue 247 | Vue.mixin(WildVueMixin) 248 | 249 | // use object-based merge strategy 250 | var mergeStrats = Vue.config.optionMergeStrategies 251 | mergeStrats.wilddog = mergeStrats.methods 252 | 253 | // extend instance methods 254 | Vue.prototype.$bindAsObject = function (key, source, cancelCallback) { 255 | ensureRefs(this) 256 | bind(this, key, { 257 | source: source, 258 | asObject: true, 259 | cancelCallback: cancelCallback 260 | }) 261 | } 262 | 263 | Vue.prototype.$bindAsArray = function (key, source, cancelCallback) { 264 | ensureRefs(this) 265 | bind(this, key, { 266 | source: source, 267 | cancelCallback: cancelCallback 268 | }) 269 | } 270 | 271 | Vue.prototype.$unbind = function (key) { 272 | unbind(this, key) 273 | } 274 | } 275 | 276 | // auto install 277 | /* istanbul ignore if */ 278 | if (typeof window !== 'undefined' && window.Vue) { 279 | install(window.Vue) 280 | } 281 | 282 | module.exports = install 283 | -------------------------------------------------------------------------------- /dist/wildvue.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define([], factory); 6 | else if(typeof exports === 'object') 7 | exports["WildVue"] = factory(); 8 | else 9 | root["WildVue"] = factory(); 10 | })(this, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports) { 56 | 57 | var Vue // late binding 58 | 59 | /** 60 | * Returns the key of a Wilddog snapshot across SDK versions. 61 | * 62 | * @param {WilddogSnapshot} snapshot 63 | * @return {string|null} 64 | */ 65 | function _getKey (snapshot) { 66 | return typeof snapshot.key === 'function' 67 | ? snapshot.key() 68 | : snapshot.key 69 | } 70 | 71 | /** 72 | * Returns the original reference of a Wilddog reference or query across SDK versions. 73 | * 74 | * @param {WilddogReference|WilddogQuery} refOrQuery 75 | * @return {WilddogReference} 76 | */ 77 | function _getRef (refOrQuery) { 78 | if (typeof refOrQuery.ref === 'function') { 79 | refOrQuery = refOrQuery.ref() 80 | } else if (typeof refOrQuery.ref === 'object') { 81 | refOrQuery = refOrQuery.ref 82 | } 83 | 84 | return refOrQuery 85 | } 86 | 87 | /** 88 | * Check if a value is an object. 89 | * 90 | * @param {*} val 91 | * @return {boolean} 92 | */ 93 | function isObject (val) { 94 | return Object.prototype.toString.call(val) === '[object Object]' 95 | } 96 | 97 | /** 98 | * Convert wilddog snapshot into a bindable data record. 99 | * 100 | * @param {WilddogSnapshot} snapshot 101 | * @return {Object} 102 | */ 103 | function createRecord (snapshot) { 104 | var value = snapshot.val() 105 | var res = isObject(value) 106 | ? value 107 | : { '.value': value } 108 | res['.key'] = _getKey(snapshot); 109 | return res 110 | } 111 | 112 | /** 113 | * Find the index for an object with given key. 114 | * 115 | * @param {array} array 116 | * @param {string} key 117 | * @return {number} 118 | */ 119 | function indexForKey (array, key) { 120 | for (var i = 0; i < array.length; i++) { 121 | if (array[i]['.key'] === key) { 122 | return i 123 | } 124 | } 125 | /* istanbul ignore next */ 126 | return -1 127 | } 128 | 129 | /** 130 | * Bind a wilddog data source to a key on a vm. 131 | * 132 | * @param {Vue} vm 133 | * @param {string} key 134 | * @param {object} source 135 | */ 136 | function bind (vm, key, source) { 137 | var asObject = false 138 | var cancelCallback = null 139 | // check { source, asArray, cancelCallback } syntax 140 | if (isObject(source) && source.hasOwnProperty('source')) { 141 | asObject = source.asObject 142 | cancelCallback = source.cancelCallback 143 | source = source.source 144 | } 145 | if (!isObject(source)) { 146 | throw new Error('WildVue: invalid Wilddog binding source.') 147 | } 148 | // get the original ref for possible queries 149 | var ref = _getRef(source) 150 | vm.$wilddogRefs[key] = ref 151 | vm._wilddogSources[key] = source 152 | // bind based on initial value type 153 | if (asObject) { 154 | bindAsObject(vm, key, source, cancelCallback) 155 | } else { 156 | bindAsArray(vm, key, source, cancelCallback) 157 | } 158 | } 159 | 160 | /** 161 | * Define a reactive property in a given vm if it's not defined 162 | * yet 163 | * 164 | * @param {Vue} vm 165 | * @param {string} key 166 | * @param {*} val 167 | */ 168 | function defineReactive (vm, key, val) { 169 | if (key in vm) { 170 | vm[key] = val 171 | } else { 172 | Vue.util.defineReactive(vm, key, val) 173 | } 174 | } 175 | 176 | /** 177 | * Bind a wilddog data source to a key on a vm as an Array. 178 | * 179 | * @param {Vue} vm 180 | * @param {string} key 181 | * @param {object} source 182 | * @param {function|null} cancelCallback 183 | */ 184 | function bindAsArray (vm, key, source, cancelCallback) { 185 | var array = [] 186 | defineReactive(vm, key, array) 187 | 188 | var onAdd = source.on('child_added', function (snapshot, prevKey) { 189 | var index = prevKey ? indexForKey(array, prevKey) + 1 : 0 190 | array.splice(index, 0, createRecord(snapshot)) 191 | }, cancelCallback) 192 | 193 | var onRemove = source.on('child_removed', function (snapshot) { 194 | var index = indexForKey(array, _getKey(snapshot)) 195 | array.splice(index, 1) 196 | }, cancelCallback) 197 | 198 | var onChange = source.on('child_changed', function (snapshot) { 199 | var index = indexForKey(array, _getKey(snapshot)) 200 | array.splice(index, 1, createRecord(snapshot)) 201 | }, cancelCallback) 202 | 203 | var onMove = source.on('child_moved', function (snapshot, prevKey) { 204 | var index = indexForKey(array, _getKey(snapshot)) 205 | var record = array.splice(index, 1)[0] 206 | var newIndex = prevKey ? indexForKey(array, prevKey) + 1 : 0 207 | array.splice(newIndex, 0, record) 208 | }, cancelCallback) 209 | 210 | vm._wilddogListeners[key] = { 211 | child_added: onAdd, 212 | child_removed: onRemove, 213 | child_changed: onChange, 214 | child_moved: onMove 215 | } 216 | } 217 | 218 | /** 219 | * Bind a wilddog data source to a key on a vm as an Object. 220 | * 221 | * @param {Vue} vm 222 | * @param {string} key 223 | * @param {Object} source 224 | * @param {function|null} cancelCallback 225 | */ 226 | function bindAsObject (vm, key, source, cancelCallback) { 227 | defineReactive(vm, key, {}) 228 | var cb = source.on('value', function (snapshot) { 229 | vm[key] = createRecord(snapshot) 230 | }, cancelCallback) 231 | vm._wilddogListeners[key] = { value: cb } 232 | } 233 | 234 | /** 235 | * Unbind a wilddog-bound key from a vm. 236 | * 237 | * @param {Vue} vm 238 | * @param {string} key 239 | */ 240 | function unbind (vm, key) { 241 | var source = vm._wilddogSources && vm._wilddogSources[key] 242 | if (!source) { 243 | throw new Error( 244 | 'WildVue: unbind failed: "' + key + '" is not bound to ' + 245 | 'a Wilddog reference.' 246 | ) 247 | } 248 | var listeners = vm._wilddogListeners[key] 249 | for (var event in listeners) { 250 | source.off(event, listeners[event]) 251 | } 252 | vm[key] = null 253 | vm.$wilddogRefs[key] = null 254 | vm._wilddogSources[key] = null 255 | vm._wilddogListeners[key] = null 256 | } 257 | 258 | /** 259 | * Ensure the related bookkeeping variables on an instance. 260 | * 261 | * @param {Vue} vm 262 | */ 263 | function ensureRefs (vm) { 264 | if (!vm.$wilddogRefs) { 265 | vm.$wilddogRefs = Object.create(null) 266 | vm._wilddogSources = Object.create(null) 267 | vm._wilddogListeners = Object.create(null) 268 | } 269 | } 270 | 271 | var init = function () { 272 | var bindings = this.$options.wilddog 273 | if (!bindings) return 274 | ensureRefs(this) 275 | for (var key in bindings) { 276 | bind(this, key, bindings[key]) 277 | } 278 | } 279 | 280 | var WildVueMixin = { 281 | init: init, // 1.x 282 | beforeCreate: init, // 2.x 283 | beforeDestroy: function () { 284 | if (!this.$wilddogRefs) return 285 | for (var key in this.$wilddogRefs) { 286 | if (this.$wilddogRefs[key]) { 287 | this.$unbind(key) 288 | } 289 | } 290 | this.$wilddogRefs = null 291 | this._wilddogSources = null 292 | this._wilddogListeners = null 293 | } 294 | } 295 | 296 | /** 297 | * Install function passed to Vue.use() in manual installation. 298 | * 299 | * @param {function} _Vue 300 | */ 301 | function install (_Vue) { 302 | Vue = _Vue 303 | Vue.mixin(WildVueMixin) 304 | 305 | // use object-based merge strategy 306 | var mergeStrats = Vue.config.optionMergeStrategies 307 | mergeStrats.wilddog = mergeStrats.methods 308 | 309 | // extend instance methods 310 | Vue.prototype.$bindAsObject = function (key, source, cancelCallback) { 311 | ensureRefs(this) 312 | bind(this, key, { 313 | source: source, 314 | asObject: true, 315 | cancelCallback: cancelCallback 316 | }) 317 | } 318 | 319 | Vue.prototype.$bindAsArray = function (key, source, cancelCallback) { 320 | ensureRefs(this) 321 | bind(this, key, { 322 | source: source, 323 | cancelCallback: cancelCallback 324 | }) 325 | } 326 | 327 | Vue.prototype.$unbind = function (key) { 328 | unbind(this, key) 329 | } 330 | } 331 | 332 | // auto install 333 | /* istanbul ignore if */ 334 | if (typeof window !== 'undefined' && window.Vue) { 335 | install(window.Vue) 336 | } 337 | 338 | module.exports = install 339 | 340 | 341 | /***/ } 342 | /******/ ]) 343 | }); 344 | ; -------------------------------------------------------------------------------- /tests/wildvue.spec.js: -------------------------------------------------------------------------------- 1 | var Vue = require('vue') 2 | var wilddog = require('wilddog') 3 | var WildVue = require('../src/wildvue') 4 | var helpers = require('./helpers') 5 | 6 | Vue.use(WildVue) 7 | 8 | var wilddogApp = wilddog.initializeApp({ 9 | authDomain: 'test123.wilddog.com', 10 | syncURL: 'https://test123.wilddogio.com' 11 | }) 12 | var sync = wilddogApp.sync() 13 | 14 | describe('WildVue', function () { 15 | var wilddogRef 16 | // network can be very slow 17 | this.timeout(0) 18 | beforeEach(function (done) { 19 | wilddogRef = sync 20 | wilddogRef.remove(function (error) { 21 | if (error && error.code !== 'OP_REPEAT') { 22 | done(error) 23 | } else { 24 | wilddogRef = wilddogRef.child(helpers.generateRandomString()) 25 | done() 26 | } 27 | }) 28 | }) 29 | 30 | describe('bind as Array', function () { 31 | /* it('throws error for invalid wilddog ref', function () { 32 | helpers.invalidWilddogRefs.forEach(function (ref) { 33 | expect(function () { 34 | new Vue({ 35 | wilddog: { 36 | items: ref 37 | } 38 | }) 39 | }).to.throw('WildVue: invalid Wilddog binding source.') 40 | }) 41 | }) 42 | */ 43 | it('binds array records which are objects', function (done) { 44 | var vm = new Vue({ 45 | wilddog: { 46 | items: wilddogRef 47 | }, 48 | template: '
{{ item[".key"] }} {{ item.index }}
' 49 | }).$mount() 50 | wilddogRef.set({ 51 | first: { index: 0 }, 52 | second: { index: 1 }, 53 | third: { index: 2 } 54 | }, function () { 55 | expect(vm.items).to.deep.equal([ 56 | { '.key': 'first', index: 0 }, 57 | { '.key': 'second', index: 1 }, 58 | { '.key': 'third', index: 2 } 59 | ]) 60 | Vue.nextTick(function () { 61 | expect(vm.$el.textContent).to.contain('first 0 second 1 third 2') 62 | done() 63 | }) 64 | }) 65 | }) 66 | 67 | it('bind using $bindAsArray', function (done) { 68 | var vm = new Vue({ 69 | template: '
{{ item[".key"] }} {{ item.index }}
', 70 | created: function () { 71 | this.$bindAsArray('items', wilddogRef) 72 | } 73 | }).$mount() 74 | wilddogRef.set({ 75 | first: { index: 0 }, 76 | second: { index: 1 }, 77 | third: { index: 2 } 78 | }, function () { 79 | expect(vm.items).to.deep.equal([ 80 | { '.key': 'first', index: 0 }, 81 | { '.key': 'second', index: 1 }, 82 | { '.key': 'third', index: 2 } 83 | ]) 84 | Vue.nextTick(function () { 85 | expect(vm.$el.textContent).to.contain('first 0 second 1 third 2') 86 | done() 87 | }) 88 | }) 89 | }) 90 | 91 | it('binds array records which are primitives', function (done) { 92 | var vm = new Vue({ 93 | wilddog: { 94 | items: wilddogRef 95 | }, 96 | template: '
{{ item[".key"] }} {{ item[".value"] }}
' 97 | }).$mount() 98 | wilddogRef.set(['first', 'second', 'third'], function () { 99 | expect(vm.items).to.deep.equal([ 100 | { '.key': '0', '.value': 'first' }, 101 | { '.key': '1', '.value': 'second' }, 102 | { '.key': '2', '.value': 'third' } 103 | ]) 104 | Vue.nextTick(function () { 105 | expect(vm.$el.textContent).to.contain('0 first 1 second 2 third') 106 | done() 107 | }) 108 | }) 109 | }) 110 | 111 | it('binds array records which are a mix of objects and primitives', function (done) { 112 | var vm = new Vue({ 113 | wilddog: { 114 | items: wilddogRef 115 | }, 116 | template: '
{{ item[".key"] }} {{ item[".value"] }} {{ item.index}}
' 117 | }).$mount() 118 | wilddogRef.set({ 119 | 0: 'first', 120 | 1: 'second', 121 | third: { index: 2 } 122 | }, function () { 123 | expect(vm.items).to.deep.equal([ 124 | { '.key': '0', '.value': 'first' }, 125 | { '.key': '1', '.value': 'second' }, 126 | { '.key': 'third', index: 2 } 127 | ]) 128 | Vue.nextTick(function () { 129 | expect(vm.$el.textContent).to.contain('0 first 1 second third 2') 130 | done() 131 | }) 132 | }) 133 | }) 134 | 135 | it('binds array records which are a mix of objects and primitives', function (done) { 136 | var vm = new Vue({ 137 | wilddog: { 138 | items: wilddogRef 139 | } 140 | }) 141 | wilddogRef.set(null, function () { 142 | expect(vm.items).to.deep.equal([]) 143 | done() 144 | }) 145 | }) 146 | 147 | it('binds sparse arrays', function (done) { 148 | var vm = new Vue({ 149 | wilddog: { 150 | items: wilddogRef 151 | } 152 | }) 153 | wilddogRef.set({ 0: 'a', 2: 'b', 5: 'c' }, function () { 154 | expect(vm.items).to.deep.equal([ 155 | { '.key': '0', '.value': 'a' }, 156 | { '.key': '2', '.value': 'b' }, 157 | { '.key': '5', '.value': 'c' } 158 | ]) 159 | done() 160 | }) 161 | }) 162 | 163 | it('binds only a subset of records when using limit queries', function (done) { 164 | var vm = new Vue({ 165 | wilddog: { 166 | items: wilddogRef.limitToLast(2) 167 | }, 168 | template: '
{{ item[".key"] }} {{ item[".value"] }}
' 169 | }).$mount() 170 | wilddogRef.set({ a: 1, b: 2, c: 3 }, function () { 171 | expect(vm.items).to.deep.equal([ 172 | { '.key': 'b', '.value': 2 }, 173 | { '.key': 'c', '.value': 3 } 174 | ]) 175 | Vue.nextTick(function () { 176 | expect(vm.$el.textContent).to.contain('b 2 c 3') 177 | done() 178 | }) 179 | }) 180 | }) 181 | 182 | it('removes records when they fall outside of a limit query', function (done) { 183 | var vm = new Vue({ 184 | wilddog: { 185 | items: wilddogRef.limitToLast(2) 186 | }, 187 | template: '
{{ item[".key"] }} {{ item[".value"] }}
' 188 | }).$mount() 189 | wilddogRef.set({ a: 1, b: 2, c: 3 }, function () { 190 | wilddogRef.child('d').set(4, function () { 191 | expect(vm.items).to.deep.equal([ 192 | { '.key': 'c', '.value': 3 }, 193 | { '.key': 'd', '.value': 4 } 194 | ]) 195 | Vue.nextTick(function () { 196 | expect(vm.$el.textContent).to.contain('c 3 d 4') 197 | done() 198 | }) 199 | }) 200 | }) 201 | }) 202 | 203 | it('adds a new record when an existing record in the limit query is removed', function (done) { 204 | var vm = new Vue({ 205 | wilddog: { 206 | items: wilddogRef.limitToLast(2) 207 | }, 208 | template: '
{{ item[".key"] }} {{ item[".value"] }}
' 209 | }).$mount() 210 | wilddogRef.set({ a: 1, b: 2, c: 3 }, function () { 211 | wilddogRef.child('b').remove(function () { 212 | expect(vm.items).to.deep.equal([ 213 | { '.key': 'a', '.value': 1 }, 214 | { '.key': 'c', '.value': 3 } 215 | ]) 216 | Vue.nextTick(function () { 217 | expect(vm.$el.textContent).to.contain('a 1 c 3') 218 | done() 219 | }) 220 | }) 221 | }) 222 | }) 223 | 224 | it('binds records in the correct order when using ordered queries', function (done) { 225 | var vm = new Vue({ 226 | wilddog: { 227 | items: wilddogRef.orderByValue() 228 | }, 229 | template: '
{{ item[".key"] }} {{ item[".value"] }}
' 230 | }).$mount() 231 | wilddogRef.set({ a: 2, b: 1, c: 3 }, function () { 232 | expect(vm.items).to.deep.equal([ 233 | { '.key': 'b', '.value': 1 }, 234 | { '.key': 'a', '.value': 2 }, 235 | { '.key': 'c', '.value': 3 } 236 | ]) 237 | Vue.nextTick(function () { 238 | expect(vm.$el.textContent).to.contain('b 1 a 2 c 3') 239 | done() 240 | }) 241 | }) 242 | }) 243 | 244 | it('binds multiple Wilddog references to state variables at the same time', function (done) { 245 | var vm = new Vue({ 246 | wilddog: { 247 | bindVar0: wilddogRef.child('items0'), 248 | bindVar1: wilddogRef.child('items1') 249 | }, 250 | template: 251 | '
' + 252 | '
{{ item[".key"] }} {{ item.index }}
' + 253 | '
{{ item[".key"] }} {{ item[".value"] }}
' + 254 | '
' 255 | }).$mount() 256 | wilddogRef.set({ 257 | items0: { 258 | first: { index: 0 }, 259 | second: { index: 1 }, 260 | third: { index: 2 } 261 | }, 262 | items1: ['first', 'second', 'third'] 263 | }, function () { 264 | expect(vm.bindVar0).to.deep.equal([ 265 | { '.key': 'first', index: 0 }, 266 | { '.key': 'second', index: 1 }, 267 | { '.key': 'third', index: 2 } 268 | ]) 269 | 270 | expect(vm.bindVar1).to.deep.equal([ 271 | { '.key': '0', '.value': 'first' }, 272 | { '.key': '1', '.value': 'second' }, 273 | { '.key': '2', '.value': 'third' } 274 | ]) 275 | Vue.nextTick(function () { 276 | expect(vm.$el.textContent).to.contain('first 0 second 1 third 2') 277 | expect(vm.$el.textContent).to.contain('0 first 1 second 2 third') 278 | done() 279 | }) 280 | }) 281 | }) 282 | 283 | it('updates an array record when its value changes', function (done) { 284 | var vm = new Vue({ 285 | wilddog: { 286 | items: wilddogRef 287 | }, 288 | template: '
{{ item[".key"] }} {{ item[".value"] || item.foo }}
' 289 | }).$mount() 290 | wilddogRef.set({ a: 1, b: 2, c: 3 }, function () { 291 | wilddogRef.child('b').set({ foo: 'bar' }, function () { 292 | expect(vm.items).to.deep.equal([ 293 | { '.key': 'a', '.value': 1 }, 294 | { '.key': 'b', foo: 'bar' }, 295 | { '.key': 'c', '.value': 3 } 296 | ]) 297 | Vue.nextTick(function () { 298 | expect(vm.$el.textContent).to.contain('a 1 b bar c 3') 299 | done() 300 | }) 301 | }) 302 | }) 303 | }) 304 | 305 | it('removes an array record when it is deleted', function (done) { 306 | var vm = new Vue({ 307 | wilddog: { 308 | items: wilddogRef 309 | }, 310 | template: '
{{ item[".key"] }} {{ item[".value"] }}
' 311 | }).$mount() 312 | wilddogRef.set({ a: 1, b: 2, c: 3 }, function () { 313 | wilddogRef.child('b').remove(function () { 314 | expect(vm.items).to.deep.equal([ 315 | { '.key': 'a', '.value': 1 }, 316 | { '.key': 'c', '.value': 3 } 317 | ]) 318 | Vue.nextTick(function () { 319 | expect(vm.$el.textContent).to.contain('a 1 c 3') 320 | done() 321 | }) 322 | }) 323 | }) 324 | }) 325 | 326 | it('moves an array record when it\'s order changes (moved to start of array) [orderByValue()]', function (done) { 327 | var vm = new Vue({ 328 | wilddog: { 329 | items: wilddogRef.orderByValue() 330 | }, 331 | template: '
{{ item[".key"] }} {{ item[".value"] }}
' 332 | }).$mount() 333 | wilddogRef.set({ a: 2, b: 3, c: 2 }, function () { 334 | wilddogRef.child('b').set(1, function () { 335 | expect(vm.items).to.deep.equal([ 336 | { '.key': 'b', '.value': 1 }, 337 | { '.key': 'a', '.value': 2 }, 338 | { '.key': 'c', '.value': 2 } 339 | ]) 340 | Vue.nextTick(function () { 341 | expect(vm.$el.textContent).to.contain('b 1 a 2 c 2') 342 | done() 343 | }) 344 | }) 345 | }) 346 | }) 347 | 348 | it('moves an array record when it\'s order changes (moved to middle of array) [orderByValue()]', function (done) { 349 | var vm = new Vue({ 350 | wilddog: { 351 | items: wilddogRef.orderByValue() 352 | }, 353 | template: '
{{ item[".key"] }} {{ item[".value"] }}
' 354 | }).$mount() 355 | wilddogRef.set({ a: 2, b: 1, c: 4 }, function () { 356 | wilddogRef.child('b').set(3, function () { 357 | expect(vm.items).to.deep.equal([ 358 | { '.key': 'a', '.value': 2 }, 359 | { '.key': 'b', '.value': 3 }, 360 | { '.key': 'c', '.value': 4 } 361 | ]) 362 | Vue.nextTick(function () { 363 | expect(vm.$el.textContent).to.contain('a 2 b 3 c 4') 364 | done() 365 | }) 366 | }) 367 | }) 368 | }) 369 | 370 | it('moves an array record when it\'s order changes (moved to end of array) [orderByValue()]', function (done) { 371 | var vm = new Vue({ 372 | wilddog: { 373 | items: wilddogRef.orderByValue() 374 | }, 375 | template: '
{{ item[".key"] }} {{ item[".value"] }}
' 376 | }).$mount() 377 | wilddogRef.set({ a: 2, b: 1, c: 3 }, function () { 378 | wilddogRef.child('b').set(4, function () { 379 | expect(vm.items).to.deep.equal([ 380 | { '.key': 'a', '.value': 2 }, 381 | { '.key': 'c', '.value': 3 }, 382 | { '.key': 'b', '.value': 4 } 383 | ]) 384 | Vue.nextTick(function () { 385 | expect(vm.$el.textContent).to.contain('a 2 c 3 b 4') 386 | done() 387 | }) 388 | }) 389 | }) 390 | }) 391 | 392 | it('moves an array record when it\'s order changes (moved to start of array) [orderByChild()]', function (done) { 393 | var vm = new Vue({ 394 | wilddog: { 395 | items: wilddogRef.orderByChild('value') 396 | }, 397 | template: '
{{ item[".key"] }} {{ item.value }}
' 398 | }).$mount() 399 | wilddogRef.set({ 400 | a: { value: 2 }, 401 | b: { value: 3 }, 402 | c: { value: 2 } 403 | }, function () { 404 | wilddogRef.child('b').set({ value: 1 }, function () { 405 | expect(vm.items).to.deep.equal([ 406 | { '.key': 'b', value: 1 }, 407 | { '.key': 'a', value: 2 }, 408 | { '.key': 'c', value: 2 } 409 | ]) 410 | Vue.nextTick(function () { 411 | expect(vm.$el.textContent).to.contain('b 1 a 2 c 2') 412 | done() 413 | }) 414 | }) 415 | }) 416 | }) 417 | 418 | it('moves an array record when it\'s order changes (moved to middle of array) [orderByChild()]', function (done) { 419 | var vm = new Vue({ 420 | wilddog: { 421 | items: wilddogRef.orderByChild('value') 422 | }, 423 | template: '
{{ item[".key"] }} {{ item.value }}
' 424 | }).$mount() 425 | wilddogRef.set({ 426 | a: { value: 2 }, 427 | b: { value: 1 }, 428 | c: { value: 4 } 429 | }, function () { 430 | wilddogRef.child('b').set({ value: 3 }, function () { 431 | expect(vm.items).to.deep.equal([ 432 | { '.key': 'a', value: 2 }, 433 | { '.key': 'b', value: 3 }, 434 | { '.key': 'c', value: 4 } 435 | ]) 436 | Vue.nextTick(function () { 437 | expect(vm.$el.textContent).to.contain('a 2 b 3 c 4') 438 | done() 439 | }) 440 | }) 441 | }) 442 | }) 443 | 444 | it('moves an array record when it\'s order changes (moved to end of array) [orderByChild()]', function (done) { 445 | var vm = new Vue({ 446 | wilddog: { 447 | items: wilddogRef.orderByChild('value') 448 | }, 449 | template: '
{{ item[".key"] }} {{ item.value }}
' 450 | }).$mount() 451 | wilddogRef.set({ 452 | a: { value: 2 }, 453 | b: { value: 1 }, 454 | c: { value: 3 } 455 | }, function () { 456 | wilddogRef.child('b').set({ value: 4 }, function () { 457 | expect(vm.items).to.deep.equal([ 458 | { '.key': 'a', value: 2 }, 459 | { '.key': 'c', value: 3 }, 460 | { '.key': 'b', value: 4 } 461 | ]) 462 | Vue.nextTick(function () { 463 | expect(vm.$el.textContent).to.contain('a 2 c 3 b 4') 464 | done() 465 | }) 466 | }) 467 | }) 468 | }) 469 | 470 | it('works with orderByKey() queries', function (done) { 471 | var vm = new Vue({ 472 | wilddog: { 473 | items: wilddogRef.orderByKey() 474 | }, 475 | template: '
{{ item[".key"] }} {{ item[".value"] }}
' 476 | }).$mount() 477 | wilddogRef.set({ b: 2, c: 1, d: 3 }, function () { 478 | wilddogRef.update({ a: 4, d: 4, e: 0 }, function () { 479 | expect(vm.items).to.deep.equal([ 480 | { '.key': 'a', '.value': 4 }, 481 | { '.key': 'b', '.value': 2 }, 482 | { '.key': 'c', '.value': 1 }, 483 | { '.key': 'd', '.value': 4 }, 484 | { '.key': 'e', '.value': 0 } 485 | ]) 486 | Vue.nextTick(function () { 487 | expect(vm.$el.textContent).to.contain('a 4 b 2 c 1 d 4 e 0') 488 | done() 489 | }) 490 | }) 491 | }) 492 | }) 493 | }) 494 | 495 | describe('bind as Object', function () { 496 | /* it('throws error for invalid wilddog ref', function () { 497 | helpers.invalidWilddogRefs.forEach(function (ref) { 498 | expect(function () { 499 | new Vue({ 500 | wilddog: { 501 | items: { 502 | source: ref, 503 | asObject: true 504 | } 505 | } 506 | }) 507 | }).to.throw('WildVue: invalid Wilddog binding source.') 508 | }) 509 | }) 510 | */ 511 | it('binds to an Object', function (done) { 512 | var obj = { 513 | first: { index: 0 }, 514 | second: { index: 1 }, 515 | third: { index: 2 } 516 | } 517 | var vm = new Vue({ 518 | wilddog: { 519 | items: { 520 | source: wilddogRef.child('items'), 521 | asObject: true 522 | } 523 | }, 524 | template: '
{{ items | json }}
' 525 | }).$mount() 526 | wilddogRef.child('items').set(obj, function () { 527 | obj['.key'] = 'items' 528 | expect(vm.items).to.deep.equal(obj) 529 | Vue.nextTick(function () { 530 | expect(vm.$el.textContent).to.contain(JSON.stringify(obj, null, 2)) 531 | done() 532 | }) 533 | }) 534 | }) 535 | 536 | it('binds with $bindAsObject', function (done) { 537 | var obj = { 538 | first: { index: 0 }, 539 | second: { index: 1 }, 540 | third: { index: 2 } 541 | } 542 | var vm = new Vue({ 543 | template: '
{{ items | json }}
', 544 | created: function () { 545 | this.$bindAsObject('items', wilddogRef.child('items')) 546 | } 547 | }).$mount() 548 | wilddogRef.child('items').set(obj, function () { 549 | obj['.key'] = 'items' 550 | expect(vm.items).to.deep.equal(obj) 551 | Vue.nextTick(function () { 552 | expect(vm.$el.textContent).to.contain(JSON.stringify(obj, null, 2)) 553 | done() 554 | }) 555 | }) 556 | }) 557 | 558 | it('binds to a primitive', function (done) { 559 | var vm = new Vue({ 560 | wilddog: { 561 | items: { 562 | source: wilddogRef.child('items'), 563 | asObject: true 564 | } 565 | }, 566 | template: '
{{ items | json }}
' 567 | }).$mount() 568 | wilddogRef.child('items').set('foo', function () { 569 | expect(vm.items).to.deep.equal({ 570 | '.key': 'items', 571 | '.value': 'foo' 572 | }) 573 | Vue.nextTick(function () { 574 | expect(vm.$el.textContent).to.contain(JSON.stringify(vm.items, null, 2)) 575 | done() 576 | }) 577 | }) 578 | }) 579 | 580 | it('binds to Wilddog reference with no data', function (done) { 581 | var vm = new Vue({ 582 | wilddog: { 583 | items: { 584 | source: wilddogRef.child('items'), 585 | asObject: true 586 | } 587 | }, 588 | template: '
{{ items | json }}
' 589 | }).$mount() 590 | wilddogRef.child('items').set(null, function () { 591 | expect(vm.items).to.deep.equal({ 592 | '.key': 'items', 593 | '.value': null 594 | }) 595 | Vue.nextTick(function () { 596 | expect(vm.$el.textContent).to.contain(JSON.stringify(vm.items, null, 2)) 597 | done() 598 | }) 599 | }) 600 | }) 601 | 602 | it('sets the key as null when bound to the root of the database', function (done) { 603 | var rootRef = wilddogRef.root() 604 | var vm = new Vue({ 605 | wilddog: { 606 | items: { 607 | source: rootRef, 608 | asObject: true 609 | } 610 | }, 611 | template: '
{{ items | json }}
' 612 | }).$mount() 613 | rootRef.set('foo', function () { 614 | expect(vm.items).to.deep.equal({ 615 | '.key': null, 616 | '.value': 'foo' 617 | }) 618 | Vue.nextTick(function () { 619 | expect(vm.$el.textContent).to.contain(JSON.stringify(vm.items, null, 2)) 620 | done() 621 | }) 622 | }) 623 | }) 624 | 625 | it('binds with limit queries', function (done) { 626 | var vm = new Vue({ 627 | wilddog: { 628 | items: { 629 | source: wilddogRef.child('items').limitToLast(2), 630 | asObject: true 631 | } 632 | }, 633 | template: '
{{ items | json }}
' 634 | }).$mount() 635 | wilddogRef.child('items').set({ 636 | first: { index: 0 }, 637 | second: { index: 1 }, 638 | third: { index: 2 } 639 | }, function () { 640 | expect(vm.items).to.deep.equal({ 641 | '.key': 'items', 642 | second: { index: 1 }, 643 | third: { index: 2 } 644 | }) 645 | Vue.nextTick(function () { 646 | expect(vm.$el.textContent).to.contain(JSON.stringify(vm.items, null, 2)) 647 | done() 648 | }) 649 | }) 650 | }) 651 | 652 | it('binds multiple Wilddog references to state variables at the same time', function (done) { 653 | var vm = new Vue({ 654 | wilddog: { 655 | bindVar0: { 656 | source: wilddogRef.child('items0'), 657 | asObject: true 658 | }, 659 | bindVar1: { 660 | source: wilddogRef.child('items1'), 661 | asObject: true 662 | } 663 | }, 664 | template: '
{{ bindVar0 | json }} {{ bindVar1 | json }}
' 665 | }).$mount() 666 | 667 | var items0 = { 668 | first: { index: 0 }, 669 | second: { index: 1 }, 670 | third: { index: 2 } 671 | } 672 | 673 | var items1 = { 674 | bar: { 675 | foo: 'baz' 676 | }, 677 | baz: true, 678 | foo: 100 679 | } 680 | 681 | wilddogRef.set({ 682 | items0: items0, 683 | items1: items1 684 | }, function () { 685 | items0['.key'] = 'items0' 686 | expect(vm.bindVar0).to.deep.equal(items0) 687 | items1['.key'] = 'items1' 688 | expect(vm.bindVar1).to.deep.equal(items1) 689 | Vue.nextTick(function () { 690 | expect(vm.$el.textContent).to.contain(JSON.stringify(vm.bindVar0, null, 2)) 691 | expect(vm.$el.textContent).to.contain(JSON.stringify(vm.bindVar1, null, 2)) 692 | done() 693 | }) 694 | }) 695 | }) 696 | 697 | it('binds a mixture of arrays and objects to state variables at the same time', function (done) { 698 | var vm = new Vue({ 699 | wilddog: { 700 | bindVar0: { 701 | source: wilddogRef.child('items0'), 702 | asObject: true 703 | }, 704 | bindVar1: { 705 | source: wilddogRef.child('items1'), 706 | asObject: false 707 | } 708 | }, 709 | template: '
{{ bindVar0 | json }} {{ bindVar1 | json }}
' 710 | }).$mount() 711 | 712 | var items0 = { 713 | first: { index: 0 }, 714 | second: { index: 1 }, 715 | third: { index: 2 } 716 | } 717 | 718 | var items1 = { 719 | bar: { 720 | foo: 'baz' 721 | }, 722 | baz: true, 723 | foo: 100 724 | } 725 | 726 | wilddogRef.set({ 727 | items0: items0, 728 | items1: items1 729 | }, function () { 730 | items0['.key'] = 'items0' 731 | expect(vm.bindVar0).to.deep.equal(items0) 732 | expect(vm.bindVar1).to.deep.equal([ 733 | { '.key': 'bar', foo: 'baz' }, 734 | { '.key': 'baz', '.value': true }, 735 | { '.key': 'foo', '.value': 100 } 736 | ]) 737 | Vue.nextTick(function () { 738 | expect(vm.$el.textContent).to.contain(JSON.stringify(vm.bindVar0, null, 2)) 739 | expect(vm.$el.textContent).to.contain(JSON.stringify(vm.bindVar1, null, 2)) 740 | done() 741 | }) 742 | }) 743 | }) 744 | }) 745 | 746 | describe('unbind', function () { 747 | it('throws error given unbound key', function () { 748 | var vm = new Vue() 749 | expect(function () { 750 | vm.$unbind('items') 751 | }).to.throw(/not bound to a Wilddog reference/) 752 | }) 753 | 754 | it('should work properly for instances with no wilddog bindings', function () { 755 | expect(function () { 756 | var vm = new Vue() 757 | vm.$destroy() 758 | }).not.to.throw() 759 | }) 760 | 761 | it('unbinds the state bound to Wilddog as an array', function (done) { 762 | var vm = new Vue({ 763 | wilddog: { 764 | items: wilddogRef 765 | } 766 | }) 767 | wilddogRef.set({ 768 | first: { index: 0 }, 769 | second: { index: 1 }, 770 | third: { index: 2 } 771 | }, function () { 772 | vm.$unbind('items') 773 | expect(vm.items).to.be.null 774 | expect(vm.$wilddogRefs.items).to.be.null 775 | expect(vm._wilddogSources.items).to.be.null 776 | expect(vm._wilddogListeners.items).to.be.null 777 | done() 778 | }) 779 | }) 780 | 781 | it('unbinds the state bound to Wilddog as an object', function (done) { 782 | var vm = new Vue({ 783 | wilddog: { 784 | items: { 785 | source: wilddogRef, 786 | asObject: true 787 | } 788 | } 789 | }) 790 | wilddogRef.set({ 791 | first: { index: 0 }, 792 | second: { index: 1 }, 793 | third: { index: 2 } 794 | }, function () { 795 | vm.$unbind('items') 796 | expect(vm.items).to.be.null 797 | expect(vm.$wilddogRefs.items).to.be.null 798 | expect(vm._wilddogSources.items).to.be.null 799 | expect(vm._wilddogListeners.items).to.be.null 800 | done() 801 | }) 802 | }) 803 | 804 | it('unbinds all bound state when the component unmounts', function (done) { 805 | var vm = new Vue({ 806 | wilddog: { 807 | item0: wilddogRef, 808 | item1: { 809 | source: wilddogRef, 810 | asObject: true 811 | } 812 | } 813 | }) 814 | sinon.spy(vm, '$unbind') 815 | wilddogRef.set({ 816 | first: { index: 0 }, 817 | second: { index: 1 }, 818 | third: { index: 2 } 819 | }, function () { 820 | vm.$destroy() 821 | expect(vm.$unbind).to.have.been.calledTwice 822 | expect(vm.item0).to.be.null 823 | expect(vm.item1).to.be.null 824 | done() 825 | }) 826 | }) 827 | 828 | it('handles already unbound state when the component unmounts', function (done) { 829 | var vm = new Vue({ 830 | wilddog: { 831 | item0: wilddogRef, 832 | item1: { 833 | source: wilddogRef, 834 | asObject: true 835 | } 836 | } 837 | }) 838 | sinon.spy(vm, '$unbind') 839 | wilddogRef.set({ 840 | first: { index: 0 }, 841 | second: { index: 1 }, 842 | third: { index: 2 } 843 | }, function () { 844 | vm.$unbind('item0') 845 | vm.$destroy() 846 | expect(vm.$unbind).to.have.been.calledTwice 847 | expect(vm.item0).to.be.null 848 | expect(vm.item1).to.be.null 849 | done() 850 | }) 851 | }) 852 | }) 853 | }) 854 | --------------------------------------------------------------------------------