├── .babelrc ├── .travis.yml ├── .gitignore ├── docs ├── advconfig.md ├── assets │ ├── architecture.png │ ├── reactivity.png │ ├── vuebackbone.png │ ├── architecture.xml │ ├── reactivity.xml │ └── vuebackbone.xml ├── SUMMARY.md ├── README.md ├── concept.md ├── migration.md └── guidelines.md ├── test ├── suite.js ├── no-mapping-model-spec.js ├── no-mapping-collection-spec.js ├── initialization-spec.js ├── model-spec.js └── collection-spec.js ├── .eslintrc ├── book.json ├── webpack.config.js ├── karma.conf.js ├── LICENSE ├── examples ├── comparison │ ├── data.js │ ├── index.htm │ ├── components.js │ ├── setup.js │ ├── parents.js │ └── run.js ├── todomvc │ ├── todomvc-backbone.js │ ├── index.htm │ └── todomvc-vue.js ├── todomvc-with-component │ ├── todomvc-backbone.js │ ├── index.htm │ └── todomvc-vue-with-component.js └── github-commits.htm ├── package.json ├── README.md ├── src ├── model-proxy.js ├── collection-proxy.js └── vue-backbone.js └── dist ├── vue-backbone.min.js └── vue-backbone.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | npm-debug.log -------------------------------------------------------------------------------- /docs/advconfig.md: -------------------------------------------------------------------------------- 1 | # Advanced Configuration 2 | 3 | > **Work in Progress!** -------------------------------------------------------------------------------- /docs/assets/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeapr4/vue-backbone/HEAD/docs/assets/architecture.png -------------------------------------------------------------------------------- /docs/assets/reactivity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeapr4/vue-backbone/HEAD/docs/assets/reactivity.png -------------------------------------------------------------------------------- /docs/assets/vuebackbone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeapr4/vue-backbone/HEAD/docs/assets/vuebackbone.png -------------------------------------------------------------------------------- /test/suite.js: -------------------------------------------------------------------------------- 1 | require('jasmine-jquery'); 2 | 3 | var testsContext = require.context(".", true, /-spec\.js$/); 4 | testsContext.keys().forEach(testsContext); -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Introduction](README.md) 4 | * [Gradual Migration](migration.md) 5 | * [Concept](concept.md) 6 | * [Usage Guidelines](guidelines.md) 7 | * [Advanced Configuration](advconfig.md) -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "amd": true, 11 | "jasmine": true 12 | }, 13 | "rules": { 14 | "no-console": 0 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": ">3.0.0", 3 | "root": "./docs", 4 | "plugins": ["edit-link", "github", "-fontsettings", "jsfiddle"], 5 | "pluginsConfig": { 6 | "edit-link": { 7 | "base": "https://github.com/mikeapr4/vue-backbone/tree/master/docs", 8 | "label": "Edit This Page" 9 | }, 10 | "github": { 11 | "url": "https://github.com/mikeapr4/vue-backbone/" 12 | }, 13 | "jsfiddle": { 14 | "type": "script", 15 | "tabs": ["js", "html", "result"], 16 | "height": "500", 17 | "width": "500" 18 | } 19 | }, 20 | "links": { 21 | "sharing": { 22 | "facebook": false, 23 | "twitter": false 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), webpack = require('webpack'), version = require("./package.json").version; 2 | 3 | module.exports = { 4 | resolve: { 5 | modules: [path.resolve(__dirname, 'src'), 'node_modules'] 6 | }, 7 | entry: 'vue-backbone.js', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'vue-backbone.js', 11 | library: 'VueBackbone', 12 | libraryTarget: 'umd' 13 | }, 14 | plugins: [ 15 | new webpack.BannerPlugin({banner: 16 | `Vue-Backbone v${version} 17 | https://github.com/mikeapr4/vue-backbone 18 | @license MIT` 19 | }) 20 | ], 21 | module: { 22 | rules: [{ 23 | test: /\.js$/, 24 | exclude: [/node_modules/], 25 | loader: 'babel-loader' 26 | }] 27 | } 28 | }; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | var reporters = ['progress', 'coverage']; 3 | if (process.env.TRAVIS) { 4 | reporters.push('coveralls'); 5 | } 6 | 7 | config.set({ 8 | frameworks: ['jasmine'], 9 | browsers: ['PhantomJS'], 10 | singleRun: true, 11 | files: ['test/suite.js'], 12 | preprocessors: { 13 | 'test/suite.js': ['webpack', 'sourcemap'] 14 | }, 15 | reporters: reporters, 16 | coverageReporter: { 17 | dir: 'coverage', 18 | reporters: [ 19 | { type: 'html', subdir: 'html' }, 20 | { type: 'lcov', subdir: '.' }, 21 | { type: 'text-summary' } 22 | ] 23 | }, 24 | webpack: { 25 | module: { 26 | rules: [{ 27 | test: /\.js$/, 28 | exclude: [/node_modules/], 29 | loader: 'babel-loader', 30 | options: { 31 | plugins: ['istanbul'] 32 | } 33 | }] 34 | }, 35 | devtool: 'inline-source-map' 36 | } 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michael Gallagher 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 | -------------------------------------------------------------------------------- /docs/assets/architecture.xml: -------------------------------------------------------------------------------- 1 | 7VfBcpswEP0aXzuADI2PgZj00HY69aHpUQEZ1AhEhbBNv74rJAGKndSZup1Mp/bB2qfVsuy+3ZUXKKkOtwI35QeeE7YIvPywQDeLIPB9L4IfhfQaCYOVBgpBc6M0ARv6gxjQM2hHc9I6ipJzJmnjghmva5JJB8NC8L2rtuXMfWqDC3IEbDLMjtEvNJelRq9Cb8LfEVqUcnxhs3OPs4dC8K42z1sEaDt89HaFrS2j35Y45/sZhNYLlAjOpV5Vh4QwFVsbNn0ufWJ39FuQWp5zAOkDO8w6Yj2OGByNc7qDZaGWFtpyMKqCzrgYlKPvHdcK41tOkD77meBM0h2xNsAVbcY1DfCJB17Ih3VFpSTihS4M6ZG9pQTEDdgHQrwvqSSbBmdqZw8FAFgpKwaSD0vcNpqSW3oguXKZMpaM/qI4iKIU8hG3UvAHYndqXhPzfimuKFOVk/BOUOW495HszebGuORbWRePvxydnmffEGJHhCSHGWTYcEt4RaToQcXshv6VPtLbijRM3U914NsyLd0aMPVnaq8YbU/8g4Wh4Gk62jYxj3sOlWlELmTJC15jtp7QOOvETgV6CMlQe4PkuWmBkIj+zuCD8FUJb0LFM9yWowVS59eqhYB4z3j2oKGUMmvpG5GyN2HHneQATY6957wxem5+IfOr4TPu2M4SnJX2WZqVPDObpqsVQs+lvwWDmYknsv0Ui4IYtWCpMRXrZ0kiCMNDNTsN90TCzdFPnA7lasgFJHHJFYauCe2oOTXRBvKB+5laoxTaI2KNjp7FNeT9Oa4dqLybrSem/Wehy7g5C9GlSXh211keTcEY5vi96siPKTLl3P/1KHjU+ZMkTYfO/6I4PzkmZpaXN+p7NCF+fxwge1GxFRuEx+MgODEOogtMA9uq/u3bScIoGR75Gi4n0dvrePVqLyfjZeRvXE5AnO7her5Mf3bQ+ic= -------------------------------------------------------------------------------- /examples/comparison/data.js: -------------------------------------------------------------------------------- 1 | /********************************************************************************** 2 | * Backbone Classes 3 | **********************************************************************************/ 4 | 5 | function genVal() { 6 | return Math.floor(Math.random() * 100000); 7 | } 8 | 9 | var TestModel = Backbone.Model.extend({ 10 | defaults: { value: undefined }, 11 | getPrecision: function() { 12 | return this.has("value") ? String(this.get("value")).length : 0; 13 | } 14 | }); 15 | 16 | var TestCollection = Backbone.Collection.extend({ 17 | model: TestModel, 18 | totalValue: function() { 19 | return this.reduce(function(memo, model) { 20 | return memo + model.get("value"); 21 | }, 0); 22 | }, 23 | oddModels: function() { 24 | return this.filter(function(model) { 25 | return model.get("value") % 2; 26 | }); // within the collection this won't be proxy mapped 27 | }, 28 | comparator: 'value' // necessary for Reactive Backbone 29 | }); 30 | 31 | var collectionJson, collection2Json; 32 | 33 | function populateCollections() { 34 | return new Promise(function(resolve) { 35 | var testCollection = new TestCollection(); 36 | for (var i = 0; i < 1000; i++) { 37 | testCollection.add({ value: genVal() }); 38 | } 39 | collectionJson = JSON.stringify(testCollection.toJSON()); 40 | 41 | testCollection = new TestCollection(); 42 | for (var i = 0; i < 1000; i++) { 43 | testCollection.add({ value: genVal() }); 44 | } 45 | collection2Json = JSON.stringify(testCollection.toJSON()); 46 | 47 | completeTask(resolve); 48 | }); 49 | } 50 | 51 | -------------------------------------------------------------------------------- /examples/todomvc/todomvc-backbone.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Model and Collection taken from the Backbone TodoMVC example 3 | * https://github.com/tastejs/todomvc/tree/master/examples/backbone 4 | */ 5 | 6 | var Todo = Backbone.Model.extend({ 7 | // Default attributes for the todo 8 | // and ensure that each todo created has `title` and `completed` keys. 9 | defaults: { 10 | title: "", 11 | completed: false 12 | }, 13 | 14 | initialize: function() { 15 | this.on("change", function() { 16 | this.save(); 17 | }); 18 | }, 19 | 20 | // Toggle the `completed` state of this todo item. 21 | toggle: function() { 22 | this.save({ 23 | completed: !this.get("completed") 24 | }); 25 | } 26 | }); 27 | 28 | var Todos = Backbone.Collection.extend({ 29 | // Reference to this collection's model. 30 | model: Todo, 31 | 32 | // Save all of the todo items under this example's namespace. 33 | localStorage: new Backbone.LocalStorage("todos-backbone"), 34 | 35 | // Filter down the list of all todo items that are finished. 36 | completed: function() { 37 | return this.where({ completed: true }); 38 | }, 39 | 40 | // Filter down the list to only todo items that are still not finished. 41 | active: function() { 42 | return this.where({ completed: false }); 43 | }, 44 | 45 | all: function() { 46 | return this.models; 47 | }, 48 | 49 | // We keep the Todos in sequential order, despite being saved by unordered 50 | // GUID in the database. This generates the next order number for new items. 51 | nextOrder: function() { 52 | return this.length ? this.last().get("order") + 1 : 1; 53 | }, 54 | 55 | // Todos are sorted by their original insertion order. 56 | comparator: "order" 57 | }); 58 | 59 | // Create our global collection of **Todos**. 60 | var todos = new Todos(); 61 | todos.fetch(); 62 | -------------------------------------------------------------------------------- /examples/todomvc-with-component/todomvc-backbone.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Model and Collection taken from the Backbone TodoMVC example 3 | * https://github.com/tastejs/todomvc/tree/master/examples/backbone 4 | */ 5 | 6 | var Todo = Backbone.Model.extend({ 7 | // Default attributes for the todo 8 | // and ensure that each todo created has `title` and `completed` keys. 9 | defaults: { 10 | title: "", 11 | completed: false 12 | }, 13 | 14 | initialize: function() { 15 | this.on("change", function() { 16 | this.save(); 17 | }); 18 | }, 19 | 20 | // Toggle the `completed` state of this todo item. 21 | toggle: function() { 22 | this.save({ 23 | completed: !this.get("completed") 24 | }); 25 | } 26 | }); 27 | 28 | var Todos = Backbone.Collection.extend({ 29 | // Reference to this collection's model. 30 | model: Todo, 31 | 32 | // Save all of the todo items under this example's namespace. 33 | localStorage: new Backbone.LocalStorage("todos-backbone"), 34 | 35 | // Filter down the list of all todo items that are finished. 36 | completed: function() { 37 | return this.where({ completed: true }); 38 | }, 39 | 40 | // Filter down the list to only todo items that are still not finished. 41 | active: function() { 42 | return this.where({ completed: false }); 43 | }, 44 | 45 | all: function() { 46 | return this.models; 47 | }, 48 | 49 | // We keep the Todos in sequential order, despite being saved by unordered 50 | // GUID in the database. This generates the next order number for new items. 51 | nextOrder: function() { 52 | return this.length ? this.last().get("order") + 1 : 1; 53 | }, 54 | 55 | // Todos are sorted by their original insertion order. 56 | comparator: "order" 57 | }); 58 | 59 | // Create our global collection of **Todos**. 60 | var todos = new Todos(); 61 | todos.fetch(); 62 | -------------------------------------------------------------------------------- /docs/assets/reactivity.xml: -------------------------------------------------------------------------------- 1 | 7VrZkps4FP0aVyUP7gJksHn00k4eJjNT6dRk5lEGGTSNkUfIbTtfPxISi1hsaC+V7or7odHlouXq3HOPsAdgvjl8onAbfiE+igaW4R8GYDGwLNM0HP5PWI7SYluuNAQU+8qpMDzhH0gZDWXdYR8lmiMjJGJ4qxs9EsfIY5oNUkr2utuaRPqoWxigmuHJg1Hd+h37LJTWiW0U9s8IByHLF6zurKD3HFCyi9V4Awus04+8vYFZX8o/CaFP9iUTeByAOSWEyavNYY4iEdssbPK5ZcvdfN4UxazLAxM1DXbMlo58HgnVJJSFJCAxjB4L6yxdHhIdGLwVsk3EL01+ycekx7+VPW38oxr/IsaOapvhjhFuKvr+jZCt6mFNYraEGxwJ3MzJjmJE+fR+R3t1U/VhjlR7TiJC04mD0UL8cXvCKHlGpTtu+uF3fJiE6cTlbP2pgApvriLiPUvTEkfZcmQ/2f7zrZl5O/qSPy8jJ8LVGvsMupAGSJlAvsk8eRDZIB4l7kJRBBl+0buCCsVB7pc/+ifBfBDLUBmX40nlmzVx9S4SHksPqacKPPAAwGPJbSsckh7jjF0dXhV/4Dqn/PmFnEHWKsWkMKWQbYavWuULjHYqdB+mHwdiks5/O5FDM0Z2Xlg0a3Bn6MB0FFOU4B9wlToI7KqYcG97NrAFwGCEg5gbPL7RHJ9g9oIow5w9purGBvt+mioRXKFolnNCCZOKFZpRfTIFmoCddqIWlUJ3Cz0cBwq2Kkh8kuigAUzRp1pswUplDE+aAas6GhoPhs3nXd7koSWbF2J6OLK0bs2J3gNZrxPEKnDqCyCrBiCOlYipkKYhyqJdQCin9MJUglRmFB0Mk3Rnp9zBHG0P5SecQPz/C1O2g2JGiz++ZEPzScvRpU9m9vFL1XSfSX6jCJ2ZGzdr06sWlSjipVtkxD7EDD1xeIo7e64e9OSDyVbW8zU+CKadrTkflxAPOOaXyxrJxyRGncpHKUeqideaJTVGb80G07Y1yAKgMLsvRISZaZxQFxDt6aHB+wSWTbPOhrOPaSIGOBFM1QgkuBF7EK+SrQQJ9DyUJF039qxa0GpmB+nwYJfFg9m7aneVGu0qoV73L5ElrRyNDpiVVsxbmVrqrixkWS8pC11sOCfRajxwnW5WqNu+jLuvSs6mfTu8qeCbldDnODTLKJRbdCccLpeuC8DtcHhrdPWDTl8FOhrrYsNyTytQe2xc5G8qcXMtxWo6dZJeCJKOCcPrYzeKbjB5IYwD1JG137TozfNDJ9RLNK4kmhNEyT3GemX/iWgStGrYixWjfPYrgl66yn4a8PUDLiCDrYPdSlrOLMf5eaUlmOi8dGdpab1n5Xfritus9Pq9QyrXYtOtF+PbvFdyRvqBZjSqHMKv9F7JsXV0jyaV15ZVFQDsU/6XV+k6p36Yiyq9IT5e86rHMIl/ldVzZdU6V1bHAFyzrGbo0ME0zAa57uFkfHdKNN4hJZbPX+lqX0eJTp0Rx9dgxL5EVi3TpnH6uJF/49XsfzmRTepE9iiI7BvFQZBuHEVDHlpfXP/is9N81gKpnM+AY+l8lr2Su/TrHb3aDUGlh6sQWv1g+taPEd8h88LitefdThKjhesaxls5SdhKTd7nIGHUw/5+quatXyG/rjaO67WxRZh1pqmu2z2uccpXVWqM9S72GlV0scHm+VSsZN5yKWi/b7hb07Tp1wXdykmf74wqCsBpyEerIR+da6Rj/Qv06S12pVSKr7ory/Rzg12x3crrFgfcbFd4s/jBjyzcxa+qwOP/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-backbone", 3 | "version": "0.1.3", 4 | "description": "Vue.js Plugin to facilitate Backbone integration", 5 | "main": "dist/vue-backbone.js", 6 | "module": "src/vue-backbone.js", 7 | "scripts": { 8 | "pretest": "eslint src/*.js test/*.js", 9 | "test": "karma start", 10 | "debug": "karma start --no-single-run --reporters kjhtml --browsers Chrome", 11 | "build": "webpack", 12 | "postbuild": "uglifyjs dist/vue-backbone.js -cmo dist/vue-backbone.min.js --comments", 13 | "prettier": "prettier --print-width 100 --write src/*.js test/*.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mikeapr4/vue-backbone.git" 18 | }, 19 | "keywords": [ 20 | "vue", 21 | "backbone" 22 | ], 23 | "author": "Michael Gallagher", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/mikeapr4/vue-backbone/issues" 27 | }, 28 | "homepage": "https://github.com/mikeapr4/vue-backbone#readme", 29 | "devDependencies": { 30 | "babel-cli": "^6.24.1", 31 | "babel-core": "^6.24.1", 32 | "babel-loader": "^7.0.0", 33 | "babel-plugin-istanbul": "^4.1.3", 34 | "babel-preset-es2015": "^6.24.1", 35 | "backbone": "^1.3.3", 36 | "eslint": "^3.19.0", 37 | "jasmine-core": "^2.6.2", 38 | "jasmine-jquery": "^2.1.1", 39 | "jquery": "^3.2.1", 40 | "karma": "^1.7.0", 41 | "karma-chrome-launcher": "^2.1.1", 42 | "karma-coverage": "^1.1.1", 43 | "karma-coveralls": "^1.1.2", 44 | "karma-jasmine": "^1.1.0", 45 | "karma-jasmine-html-reporter": "^0.2.2", 46 | "karma-phantomjs-launcher": "^1.0.4", 47 | "karma-sourcemap-loader": "^0.3.7", 48 | "karma-webpack": "^2.0.3", 49 | "prettier": "^1.4.2", 50 | "uglify-js": "^3.0.9", 51 | "vue": "^2.3.3", 52 | "webpack": "^2.5.1" 53 | }, 54 | "peerDependencies": { 55 | "backbone": ">= 1.0.0", 56 | "vue": ">= 2.0.0" 57 | }, 58 | "files": [ 59 | "dist/*.js" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-backbone 2 | 3 | [![Build Status](https://travis-ci.org/mikeapr4/vue-backbone.png?branch=master)](https://travis-ci.org/mikeapr4/vue-backbone) 4 | [![Coverage Status](https://coveralls.io/repos/github/mikeapr4/vue-backbone/badge.svg?branch=master)](https://coveralls.io/github/mikeapr4/vue-backbone) 5 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 6 | 7 | Vue.js Plugin to facilitate gradual migration from Backbone. Backbone Collections and Models can be safely integrated with Vue instances and components, with focus on clean code and future adoption of a Flux library (e.g. Vuex/Redux/Flux). 8 | 9 | ## Features 10 | 11 | * Reactive data ensures Vue correctly and efficiently updates. 12 | * Safe direct data access (`model.prop` vs `model.get('prop')`). 13 | * Backbone-encapsulated logic made available. 14 | * No syncing required, single source of truth. 15 | * Step-by-step incremental migration path. 16 | 17 | ## Documentation 18 | 19 | Usage and guidelines documentation available [here](https://mikeapr4.gitbooks.io/vue-backbone) 20 | 21 | ## Installation 22 | 23 | Via NPM 24 | 25 | npm install vue-backbone 26 | 27 | Via Yarn 28 | 29 | yarn add vue-backbone 30 | 31 | Script include (see [dist](https://github.com/mikeapr4/vue-backbone/tree/master/dist) folder) 32 | 33 | ```html 34 | 35 | ``` 36 | 37 | ## Examples 38 | 39 | Clone or download the repo to run the examples. 40 | 41 | * [*GitHub Commits*](https://github.com/mikeapr4/vue-backbone/tree/master/examples/github-commits.htm) - Backbone version of Vue.js example 42 | * [*TodoMVC*](https://github.com/mikeapr4/vue-backbone/tree/master/examples/todomvc) - combined Backbone and Vue TodoMVC examples 43 | * [*TodoMVC with Component*](https://github.com/mikeapr4/vue-backbone/tree/master/examples/todomvc-with-component) - more complex version of above 44 | * [*Comparison*](https://github.com/mikeapr4/vue-backbone/tree/master/examples/comparison) - in-browser performance test and comparison 45 | 46 | ## License 47 | 48 | [MIT](http://opensource.org/licenses/MIT) 49 | -------------------------------------------------------------------------------- /docs/assets/vuebackbone.xml: -------------------------------------------------------------------------------- 1 | 7Vvfc6M2EP5rPJN7cAYQP8Jj7MTXh17budzctY8KyFgNRi7IjnN/fSWQACFw7BgcJzlnJjErsUjab79dLcoITJfbzylcLb6QEMUjywi3I3AzsizTNFz2h0ueConjXRWCKMWh6FQJ7vBPJISGkK5xiDKlIyUkpnilCgOSJCigigymKXlUu81JrD51BSOkCe4CGOvSHziki0J65RiV/DeEowUtJyxa7mHwEKVknYjnjSwwzz9F8xJKXaJ/toAheayJwO0ITFNCaPFtuZ2imK+tXLbivllHaznuFCV0nxuEWTL6JKeOQrYS4pKkdEEiksD4tpJO8ukhrsBgVwu6jNlXk31lz0yf/hby/OIfcfEvovRJmBmuKWGiSvfvhKyEhjlJ6AwuccxxMyXrFKOUDe8P9CgahQ7TFtdTEpM0Hziwb/gPk2c0JQ+o1uLnH9YSwmyRD7wYbXjNocIu72MSPBSiGY7ldAo90v7MNJNgnW7K+/WllkiFaYSEyPILGV/VWidhjM+ILBFbJtYhRTGkeKPiDwoYR2W/8ta/CGaPtQzhciWgpMPZhqoiY4sZIHFXHRANRcC3FEWu5aiKitlpitiX2nwqUY63duyJpdnAeC2mfXH9acQn4v635g4woWQdLKpLDasUbakKwRRl+Ce8zztw4K34OPORO5ORw9EBYxwlTBAwszFwgckGpRQz178WDUschjnOY3iP4knp0DVACZduh+RO/LahMlciJpXjbgUDnEQCczrMhM/ycaOtghdBh2L+CqUoYBN3jY1Lw2Hjrtt7LMx/JB7HtgojOWipgcznGToWQJYGIIaVmIolzddDrnYFoZKPK1ENUlLIFYyz3LLXrINpr7b1O9yI//2OU7qGfEQ3f36Rj2aDLp5e9JHiEG+aotMM8luK0DNjY2JleM2IEMcs7nKPeFxgiu4YPHnLIwv9qvPBbFUE4znecpqczBmZ1hAPGOZnM42hE5Kgvbi/5iNNx+skY81LOr3BdByVQn1BoY9VBmDKBGWhRv9u91DgvQPLpqmB+WLyKXfECGecqVqBBJfcBsl9tipAAoMAZdm+hn021CsBb4+4f+nUI795cMjdN0/oDvF60D4mp+jkaLTFtDZjdiVTnU4kFhFYRFk9U3D3TBQEWo1LlmSbDep2juPuXsnZdIbDm1h8s770FfTKpjIDzdtOhMTZzPcBGA6JQ+PrMPCwRYRPtQ4i3+rMC0pSFZgFwNmZjjb7myJdqXBZjODFKHV12r3htJsQiudP+5FuiyhYwCRCe/Lwm05jS7yrFKmj1OkrbTUumTJPhdEZER/ozEqPzgGLe78iGOSzPCyr63UMt0tMa0nJKyeWE8t1zzexBFcqh1lXJ00srfec9w0dbdvzvJ3ln3ocNn09EJ+qJOTa6n7Gtht78I6S0KEh3XVUeHt2o+TY6G8DZ1d/NaS33O0qd7tXDR/prz5l6kx+MeW5wZKEeM5iLcUk+aDBXJZe+gjmHgB9BnMJFBWVY/mQfjc53snJ1XiH5Krt4/YiV1fnVm8Qaj2UEZsB3zT8nYxYvvZq73/8JkdWjGtEdsuJ7FuKoyg3XIrGbK1D/v1D8pnXH58B11L5TJb2jn3Fo4bNMWho6IXQ9O3wOW1eXvLAH5AGix07laH2JPaN7xvGW9mTOCIpPc2WxNCX/f1EzaFL0XvFRk+PjVbfsXFfc0turTjlqwg1xnydBK1ZdGVg83lXbHjebMZp/9Dl7nTTtiMGz4WTQ93RbOyhTLfFH60Wf3T7cEf9Rfz1EFapheJerTLLPwNYpVl8dmUN8xRWkf76Pus2A5FkCYUj389ZbXUbe5DNxbMVDu0oT0eF43lFnnHpWleOZ9rFb1VtRznoJecyTvzqT4W0985qkaXa80eu56t82Tw71iPEwC92fD12tHWMgdchRxucitPcwTlNUlcbrZmvhMTz4LRT4G3vvGzAkm/nuZaPaO3B3481snvbH+xNktVSgL1RjzqXB0aq8lWz7IY2KC87/zpTItFj9Vi2tfxG2ba3k9DNU3qqhn5OQtsawLh17vl+uc9N/HRa22q/jdIK8Bqvwd2W0xeDbeL10soJiuc9Vc6nMUa58nM44uN61xN/yCM+LRyi4e6AcvqAR3zYZfUPYgVZVP+FB27/Bw== -------------------------------------------------------------------------------- /src/model-proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a proxy of the Backbone Model which maps direct access of attributes 3 | * to the get/set interface that Backbone normally provides. This allows for 4 | * in-template binding to Backbone model attributes easily. 5 | * 6 | * In the case of ambiguity, say an attribute called "completed" and a method 7 | * called "completed". The method takes priority (so as not to break existing functionality), 8 | * however as a backup the attribute can be accessed with a prefix (conflictPrefix option), 9 | * e.g. model.$completed 10 | * 11 | * To avoid interference, it's stored under the Model property `_vuebackbone_proxy`. This proxy 12 | * is only really intended to be used by Vue templates. 13 | */ 14 | 15 | /** 16 | * Attach proxy getter/setter for a model attribute 17 | */ 18 | function proxyModelAttribute(proxy, model, attr, conflictPrefix) { 19 | let getter = model.get.bind(model, attr); 20 | let setter = model.set.bind(model, attr); 21 | 22 | // If there's a conflict with a function from the model, add the attribute with the prefix 23 | let safeAttr = proxy[attr] ? conflictPrefix + attr : attr; 24 | 25 | Object.defineProperty(proxy, safeAttr, { 26 | enumerable: true, 27 | get: getter, 28 | set: setter 29 | }); 30 | } 31 | 32 | export default function(model, conflictPrefix) { 33 | let proxy = {}; 34 | 35 | // Attach bound version of all the model functions to the proxy 36 | // these functions are readonly on the proxy and any future changes 37 | // to the model functions won't be reflected in the Vue proxy 38 | for (let key in model) { 39 | if (typeof model[key] === "function" && key !== "constructor") { 40 | var bndFunc = model[key].bind(model); 41 | Object.defineProperty(proxy, key, { value: bndFunc }); 42 | } 43 | } 44 | 45 | // Attach getter/setters for the model attributes. 46 | Object.keys(model.attributes).forEach(attr => { 47 | proxyModelAttribute(proxy, model, attr, conflictPrefix); 48 | }); 49 | if (!proxy.id) { 50 | // sometimes ID is a field in the model (in which case it'll be proxied already) 51 | Object.defineProperty(proxy, "id", { 52 | get: function() { 53 | return model.id; 54 | } 55 | }); 56 | } 57 | 58 | // Attach link back to original model 59 | Object.defineProperty(proxy, "_vuebackbone_original", { value: model }); 60 | 61 | return proxy; 62 | } 63 | -------------------------------------------------------------------------------- /examples/comparison/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |

Performance Comparison

29 |

30 | There are 4 configurations being compared here. 31 |

    32 |
  1. 2-way Sync (vue/issues/316)
  2. 33 |
  3. Reactive Backbone (codepen.io/niexin/pen/XmYdVa) 34 |
  4. Vue-Backbone without Proxies enabled
  5. 35 |
  6. Vue-Backbone with Proxies (default)
  7. 36 |
37 | Click Start to begin, 20 tests run per cycle, Collections contain 1000 models, it may take several minutes. 38 | The actions being measured here may not represent every project and situation, but should serve as a rough measure. 39 |

40 |
41 |
42 |

Status

43 |
44 |
45 |

Results

46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
Action(A) Avg/Min/Max (ms)(B) Avg/Min/Max (ms)(C) Avg/Min/Max (ms)(D) Avg/Min/Max (ms)
55 |
56 | 57 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Vue Backbone 2 | 3 | Vue.js Plugin to facilitate gradual migration from Backbone. Backbone Collections and Models can be safely integrated with Vue instances and components, with focus on clean code and future adoption of a Flux library (e.g. Vuex/Redux/Flux). 4 | 5 | ## Features 6 | 7 | * Reactive data ensures Vue correctly and efficiently updates. 8 | * Safe direct data access (`model.prop` vs `model.get('prop')`). 9 | * Backbone-encapsulated logic made available. 10 | * No syncing required, single source of truth. 11 | * Step-by-step incremental migration path. 12 | 13 | ## Objective 14 | 15 | Vue `data` (or Vuex `state`) works with simple objects (POJOs), with data processing defined in any of the following ways: 16 | 17 | * Template expressions 18 | * Computed properties 19 | * Methods 20 | * Filters 21 | * Watchers 22 | * Getters (Vuex) 23 | * Mutations (Vuex) 24 | 25 | Ultimately when the Backbone migration is finished, all the encapsulated logic within Backbone should have been transferred to the above patterns. Backbone's in-built REST API binding could still be leveraged, or replaced with an alternative (e.g. Axios). 26 | 27 | ### First steps 28 | 29 | Considering migration from Backbone to Vue, but don't know where to start? Want to understand what a gradual migration might look like? See [Gradual Migration](/migration.md). 30 | 31 | ### Concept 32 | 33 | Some understanding of how Vue Backbone works under the hood is preferable before using it, see [Concept](/concept.md). 34 | 35 | ## Usage 36 | 37 | Before looking at the examples in the project, the [Usage Guidelines](/guidelines.md) will clarify how the library works. 38 | 39 | ## Alternatives 40 | 41 | ### Backbone Objects as Data 42 | 43 | A perfectly functional demonstration of directly using Backbone objects as data (without Vue Backbone) can be seen [here](https://codepen.io/niexin/pen/XmYdVa). This is a viable approach to integrating Backbone objects with Vue, however it relies completely on the Backbone interface, migrating from Backbone to Vuex will require a complete rewrite. 44 | 45 | ### 2-way Sync 46 | 47 | Another approach detailed in a Vue [ticket](https://github.com/vuejs/vue/issues/316), defines a basic pattern for a 2-way sync between Vue data and a Backbone model. This approach does offer a big improvement over the previous alternative, in that it does allow for easier decoupling in future from Backbone. Obviously the example shown is just a starting point (see the [Comparison](https://github.com/mikeapr4/vue-backbone/tree/master/examples/comparison) example for a more detailed implementation). However one obstacle does remain, any Backbone-encapsulated logic is not reactive, meaning direct access to Backbone Model/Collection functions in, say a Vue template, may cause refresh glitches when the underlying data changes. -------------------------------------------------------------------------------- /examples/todomvc/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 |
20 |
21 |

todos

22 | 27 |
28 |
29 | 30 | 48 |
49 | 62 |
63 | 66 | 67 | -------------------------------------------------------------------------------- /src/collection-proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a proxy of the Backbone Collection which maps direct access of underlying 3 | * array of Model proxies to the functional interface that Backbone normally provides. 4 | * This allows for in-template looping/access to Backbone model proxies. 5 | * 6 | * In the case of ambiguity, say a function exists on both the Array.prototype and in 7 | * Backbone. In general Backbone functionality is favoured, but there is a list of Array 8 | * functions which will be kept. 9 | * 10 | * In addition, common Backbone collection functions which return an array of models, or a single 11 | * one, have the returned Model(s) mapped to their proxies. Just a convenience. 12 | * 13 | * To avoid interference, it's stored under the Model property `_vuebackbone_proxy`. This proxy 14 | * is only really intended to be used by Vue templates. 15 | * 16 | * Generate documentation using: https://jsfiddle.net/fLcn09eb/3/ 17 | */ 18 | 19 | const arrayPriority = [ 20 | "slice", 21 | "forEach", 22 | "map", 23 | "reduce", 24 | "reduceRight", 25 | "find", 26 | "filter", 27 | "every", 28 | "some", 29 | "indexOf", 30 | "lastIndexOf", 31 | "findIndex" 32 | ], 33 | returnsModels = [ 34 | "pop", 35 | "shift", 36 | "remove", 37 | "get", 38 | "at", 39 | "where", 40 | "findWhere", 41 | "reject", 42 | "sortBy", 43 | "shuffle", 44 | "toArray", 45 | "detect", 46 | "select", 47 | "first", 48 | "head", 49 | "take", 50 | "rest", 51 | "tail", 52 | "drop", 53 | "initial", 54 | "last", 55 | "without" 56 | ]; 57 | 58 | function checkForReturnsModels(func, key) { 59 | if (returnsModels.indexOf(key) > -1) { 60 | return function() { 61 | let models = func.apply(this, arguments); 62 | return ( 63 | (models && 64 | (models._vuebackbone_proxy || 65 | models.map(m => m._vuebackbone_proxy))) || 66 | models 67 | ); 68 | }; 69 | } 70 | return func; 71 | } 72 | 73 | export default function(collection, simple) { 74 | let proxy = collection.map(model => model._vuebackbone_proxy); 75 | 76 | // Attach bound version of all the collection functions to the proxy 77 | // these functions are readonly on the proxy and any future changes 78 | // to the collection functions won't be reflected in the Vue proxy 79 | for (let key in collection) { 80 | if ( 81 | typeof collection[key] === "function" && 82 | key !== "constructor" && 83 | (simple || arrayPriority.indexOf(key) === -1) 84 | ) { 85 | let bndFunc = collection[key].bind(collection); 86 | if (!simple) { 87 | bndFunc = checkForReturnsModels(bndFunc, key); 88 | } 89 | Object.defineProperty(proxy, key, { value: bndFunc }); 90 | } 91 | } 92 | 93 | // Attach link back to original model 94 | Object.defineProperty(proxy, "_vuebackbone_original", { 95 | value: collection 96 | }); 97 | 98 | return proxy; 99 | } 100 | -------------------------------------------------------------------------------- /examples/todomvc-with-component/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 30 | 31 | 34 | 35 | 36 |
37 |
38 |

todos

39 | 44 |
45 |
46 | 47 | 50 |
51 | 64 |
65 | 70 | 71 | -------------------------------------------------------------------------------- /examples/comparison/components.js: -------------------------------------------------------------------------------- 1 | /********************************************************************************** 2 | * Vue Components 3 | **********************************************************************************/ 4 | 5 | var componentTemplates = [ 6 | "{{ value }}, {{ model.getPrecision() }}, ", 7 | "{{ model.get('value') }}, {{ model.getPrecision() }}, ", 8 | "{{ model.get('value') }}, {{ getPrecision }}, ", 9 | "{{ model.value }}, {{ model.getPrecision() }}, " 10 | ]; 11 | 12 | var changeValueMethods = [ 13 | function() { 14 | this.value = genVal(); 15 | }, 16 | function() { 17 | this.model.set("value", genVal()); 18 | }, 19 | function() { 20 | this.model.set("value", genVal()); 21 | }, 22 | function() { 23 | this.model.value = genVal(); 24 | } 25 | ]; 26 | 27 | Vue.component("test-comp-a", { 28 | props: ["model"], 29 | 30 | // Model data mirrored in Vue data 31 | data: function() { 32 | return { value: undefined }; 33 | }, 34 | 35 | template: componentTemplates[0], 36 | methods: { changeValue: changeValueMethods[0] }, 37 | created: function() { 38 | var vm = this; 39 | 40 | function sync() { 41 | vm.unsync && vm.unsync(); 42 | vm.unsync = function() {}; 43 | 44 | Object.keys(vm.$data).forEach(function(key) { 45 | vm[key] = vm.model.get(key); 46 | // setup two-way sync between 47 | // the vm and the Backbone model 48 | var unwatch = vm.$watch(key, function(val) { 49 | vm.model.set(key, val); 50 | }); 51 | var syncedModel = vm.model, onchange = function(model, value) { 52 | vm[key] = value; 53 | } 54 | syncedModel.on("change:" + key, onchange); 55 | 56 | // Unfortunate unsync complexity :( 57 | vm.unsync = _.compose(vm.unsync, function() { 58 | unwatch(); 59 | syncedModel.off("change:" + key, onchange); 60 | }); 61 | }); 62 | } 63 | 64 | sync(); 65 | vm.$watch('model', sync); // resync if the model changes 66 | }, 67 | updated: function() { 68 | this.$emit('updated'); 69 | } 70 | }); 71 | 72 | Vue.component("test-comp-b", { 73 | props: ["model"], 74 | 75 | template: componentTemplates[1], 76 | methods: { changeValue: changeValueMethods[1] }, 77 | updated: function() { 78 | this.$emit('updated'); 79 | } 80 | }); 81 | 82 | Vue.component("test-comp-c", { 83 | props: ["model"], 84 | bb: function() { 85 | return { model: { prop: true } }; 86 | }, 87 | template: componentTemplates[2], 88 | methods: { changeValue: changeValueMethods[2] }, 89 | computed: { 90 | getPrecision: function() { 91 | return this.model.getPrecision(); 92 | } 93 | }, 94 | updated: function() { 95 | this.$emit('updated'); 96 | } 97 | }); 98 | 99 | Vue.component("test-comp-d", { 100 | props: ["model"], 101 | bb: function() { 102 | return { model: { prop: true } }; 103 | }, 104 | template: componentTemplates[3], 105 | methods: { changeValue: changeValueMethods[3] }, 106 | updated: function() { 107 | this.$emit('updated'); 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /docs/concept.md: -------------------------------------------------------------------------------- 1 | # Concept 2 | 3 | As mentioned in the introduction, Vue data is made of simple objects (POJOs) and the logic applied to the data is separated from the data itself. Any developer familiar with Backbone will know the core storage of a Model is also made up of simple object(s) or types. See [`model.attributes`](http://backbonejs.org/#Model-attributes) and [`collection.models`](http://backbonejs.org/#Collection-models). 4 | 5 | A normal Vue instance has **reactive** data, which is accessed by various logical constructs (e.g. Computed, Watchers, Template expressions etc), which once modified will trigger Vue to re-render automatically (using its convenient Virtual DOM). 6 | 7 | This has many similarities with Backbone, which provides `change` events to allow Views re-render/update when necessary. 8 | 9 | ## Reactivity Overview 10 | 11 | Vue Backbone is possible due to Vue's [**reactivity**](https://vuejs.org/v2/guide/reactivity.html), which is where raw access to simple objects (POJOs) can be intercepted, a full explanation can be found in Vue documentation, but here is a modified version of Vue's diagram: 12 | 13 | ![](assets/reactivity.png) 14 | 15 | ## Vue Backbone Architecture 16 | 17 | ![](assets/architecture.png) 18 | 19 | What was a single node in the diagram, _Reactive Data_, is now replaced with 3 nodes. The read/write access to the data will now enter the _Reactive Client_ node, and the watcher output from _Reactive Data_ will now come from _Reactive Emitter_. 20 | 21 | ### What does this mean? 22 | 23 | * _Reactive Client_ can provide an interface with simple objects (POJOs). 24 | * _Reactive Client_ will interact with the _Backbone_ interface, meaning Backbone's own behaviour is used and maintained (events etc). 25 | * Backbone-originating access or changes will also trigger registrations/notifications from the _Reactive Emitter_. 26 | * While Logic is still being moved to Vue logical constructs, Backbone's own functions will be accessible via the _Reactive Client_ (more detail in [Usage Guidelines](/guidelines.md)). Which will in turn trigger reactive registration where necessary. 27 | * Once all Backbone-originating logic has been migrated to Vue, these 3 nodes can be replaced by a Flux library (or even Vue's own reactive data). 28 | 29 | ![](assets/vuebackbone.png) 30 | 31 | ### Tightly Coupled? 32 | 33 | If Backbone is going to emit reactive registrations/notifications, how tight is the coupling? Thankfully **Data Reactivity** can be achieved without the consumer of the data being aware, it is an excellent example of the [Proxy Pattern](https://en.wikipedia.org/wiki/Proxy_pattern). 34 | 35 | When a Backbone object is used with Vue, it's underlying data structure is made reactive unbeknownst to Backbone. 36 | 37 | ### Limitations 38 | 39 | Just as with Vue's own reactivity, the **addition/deletion** of data within a Backbone Model after its initialization, will not be detected by Vue Backbone. 40 | 41 | Vue’s recommended workaround is to initialize all properties upfront, which is a good practice anyway. 42 |   43 | For a Backbone Model, the same could be done. Though it doesn’t mean the model `defaults` need to describe each attribute, in the case of a model populated by a RESTful API response, at construction time the data will be available. Though, watch out for absent attributes that could be later added, they might be best described in `defaults`. -------------------------------------------------------------------------------- /examples/todomvc/todomvc-vue.js: -------------------------------------------------------------------------------- 1 | // Install Plugin 2 | Vue.use(VueBackbone); 3 | 4 | window.onload = function() { 5 | /** 6 | * Vue instance basic logic taken from the Vue.js TodoMVC example 7 | * https://github.com/tastejs/todomvc/tree/master/examples/vue 8 | */ 9 | 10 | var app = new Vue({ 11 | // Vue-Backbone initial options 12 | bb: function() { 13 | return { 14 | todos: todos 15 | }; 16 | }, 17 | 18 | // app initial state 19 | data: { 20 | newTodo: "", 21 | editedTodo: null, 22 | visibility: "all" 23 | }, 24 | 25 | // computed properties 26 | // http://vuejs.org/guide/computed.html 27 | computed: { 28 | // Runs a function on the collection based on 29 | // visibility, as this function returns an array 30 | // of models, mapBBModels is used to provide 31 | // enhanced model objects 32 | filteredTodos: VueBackbone.mapBBModels(function() { 33 | return this.todos[this.visibility](); 34 | }), 35 | 36 | remaining: function() { 37 | return this.todos.active().length; 38 | }, 39 | 40 | allDone: { 41 | get: function() { 42 | return this.remaining === 0; 43 | }, 44 | set: function(value) { 45 | // Making use of built-in Backbone Collection/Model functionality 46 | this.todos.each(function(todo) { 47 | todo.set("completed", value); 48 | }); 49 | } 50 | } 51 | }, 52 | 53 | filters: { 54 | pluralize: function(n) { 55 | return n === 1 ? "item" : "items"; 56 | } 57 | }, 58 | 59 | methods: { 60 | addTodo: function() { 61 | var value = this.newTodo && this.newTodo.trim(); 62 | if (!value) { 63 | return; 64 | } 65 | this.todos.create({ 66 | title: value, 67 | completed: false, 68 | order: this.todos.nextOrder() 69 | }); 70 | this.newTodo = ""; 71 | }, 72 | 73 | removeTodo: function(todo) { 74 | todo.destroy(); 75 | }, 76 | 77 | editTodo: function(todo) { 78 | this.beforeEditCache = todo.title; 79 | this.editedTodo = todo; 80 | }, 81 | 82 | doneEdit: function(todo) { 83 | if (!this.editedTodo) { 84 | return; 85 | } 86 | this.editedTodo = null; 87 | todo.title = todo.title.trim(); 88 | if (!todo.title) { 89 | this.removeTodo(todo); 90 | } 91 | }, 92 | 93 | cancelEdit: function(todo) { 94 | this.editedTodo = null; 95 | todo.title = this.beforeEditCache; 96 | }, 97 | 98 | removeCompleted: function() { 99 | _.invoke(this.todos.completed(), "destroy"); 100 | } 101 | }, 102 | 103 | // a custom directive to wait for the DOM to be updated 104 | // before focusing on the input field. 105 | // http://vuejs.org/guide/custom-directive.html 106 | directives: { 107 | "todo-focus": function(el, binding) { 108 | if (binding.value) { 109 | el.focus(); 110 | } 111 | } 112 | } 113 | }); 114 | 115 | // handle routing 116 | function onHashChange() { 117 | var visibility = window.location.hash.replace(/#\/?/, ""); 118 | if (todos[visibility]) { 119 | app.visibility = visibility; 120 | } else { 121 | window.location.hash = ""; 122 | app.visibility = "all"; 123 | } 124 | } 125 | 126 | window.addEventListener("hashchange", onHashChange); 127 | onHashChange(); 128 | 129 | // mount 130 | app.$mount(".todoapp"); 131 | }; 132 | -------------------------------------------------------------------------------- /docs/migration.md: -------------------------------------------------------------------------------- 1 | # Gradual Migration 2 | 3 | Vue (as the name suggests), is focused firstly on the _V_ in an MV* framework. So the first step in any migration would be to replace Backbone Views with Vue instances, while maintaining Backbone Models and Collections. 4 | 5 | In order to make this gradual, both Vue and Backbone would need to run side-by-side and interface with each other. In a project with 100 Backbone View classes (normally arranged hierarchically), the most obvious process is to replace one view at a time. 6 | 7 | ## Vue Interface vs Backbone View Interface 8 | 9 | If a Backbone developer has decided to move forward with Vue, it may well be due to the similarities in the interfaces of the 2 frameworks. 10 | 11 | [Backbone Interface](https://jsfiddle.net/gaes86xq/) 12 | 13 | Becomes: 14 | 15 | [Vue Interface](https://jsfiddle.net/2o8x0aas/) 16 | 17 | Even the `render` operation can be separated in Vue, if the instance doesn't have an `el` defined, then [`$mount`](https://vuejs.org/v2/api/#vm-mount) can act as a render operation. 18 | 19 | ### Interacting with a Rendered Instance 20 | 21 | Backbone Views have at their core, a DOM `el` (also accessible via `$el`). Vue also provides its root element via `$el`. 22 | 23 | > Important Note: In Backbone a `$` prefix indicates a jQuery element, however Vue uses `$` to distinguish internal Vue methods and properties from User-defined properties or functions. 24 | 25 | Backbone Views fire events (`trigger`) and provides listener registration (`on`). Vue provides the same (`$emit` and `$on`). 26 | 27 | ## Hybrid Hierarchy 28 | 29 | Take the following example of a hierarchy of Backbone Views: 30 | 31 | [Backbone Hierarchy](https://jsfiddle.net/1qea2kdb/) 32 | 33 | There are 3 views here, `AppView` which renders `HeaderView` which renders `LinkView`. To demonstrate a hybrid hierarchy, `HeaderView` can be converted to a Vue instance, this will demonstrate a Backbone View with a Vue descendant, and Vue instance with a Backbone descendant. 34 | 35 | [Hybrid Hierarchy](https://jsfiddle.net/aavj5he1/) 36 | 37 | A successful hybrid where the lifecycle hook `mounted` is used to instantiate a child Backbone view within Vue, and in the case of a Vue child, `render()` isn't called. 38 | 39 | Other points to note are that Vue doesn't use jQuery by default, so the `el` passed to Vue needs to be a DOM element, and the `$el` property on the Vue needs to be cast to jQuery for any jQuery functionality. Lastly, note that a Vue instance will replace the `el` it is mounted on, so the template for the HeaderView needed the surrounding element included in it. 40 | 41 | ### SubViews and Composition Management 42 | 43 | Vanilla Backbone doesn't provide **SubView** functionality, but many projects employ Composition Lifecycle management using extensions (e.g. Marionette). If this is the case, it might be worth considering a generic Backbone-wrapper for Vue instances. 44 | 45 | Alternatively Vue children could simply be managed (manually) outside of the SubView mechanism. Equally, a Vue-wrapper could be created for Backbone instances to allow them be instantiated via component tags inside a Vue template. 46 | 47 | ## Next Step 48 | 49 | Now that Backbone Views can be gradually converted to Vue instances, the next step is to allow those new Vue instances to use Backbone Collections/Models in a way which in future can be easily swapped for a Flux implementation... 50 | 51 | ...this is where Vue Backbone can help. -------------------------------------------------------------------------------- /examples/github-commits.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | 23 | 24 | 25 | 26 | 107 | 108 | 109 |
110 |

Latest Vue.js Commits

111 | 119 |

vuejs/vue@{{ currentBranch }}

120 | 129 |
130 | -------------------------------------------------------------------------------- /examples/comparison/setup.js: -------------------------------------------------------------------------------- 1 | // Install Plugin 2 | Vue.use(VueBackbone, { proxies: false }); 3 | 4 | // Handy trick to modify the options - not intended for general use 5 | function toggleProxies(setting) { 6 | VueBackbone.install({ mixin: function() {} }, { proxies: setting }); 7 | } 8 | 9 | var $msgs, iterations = 20, timings = {}, currentMark = 0; 10 | 11 | function addMsg(msg) { 12 | $msgs.append("
" + msg + "
"); 13 | } 14 | 15 | // Returns a function that updates msg status and returns a new Promise 16 | // Note: setTimeout use, to allow for the UI to update 17 | function addTask(task) { 18 | return function() { 19 | return new Promise(function(resolve) { 20 | addMsg(task + "..."); 21 | setTimeout(resolve); 22 | }); 23 | }; 24 | } 25 | 26 | // Update msg status before resolving promise 27 | // Note: setTimeout use, to allow for the UI to update 28 | function completeTask(resolve) { 29 | $msgs.children().last().append("Complete"); 30 | setTimeout(resolve); 31 | } 32 | 33 | // Preset all the timings to zero for the actions and cycles 34 | function initTimings(actions, cycles) { 35 | actions.forEach(function(action) { 36 | timings[action] = {}; 37 | cycles.forEach(function(cycle) { 38 | timings[action][cycle] = [0,0,0]; 39 | }); 40 | }); 41 | } 42 | 43 | // Convience for measuring mark to mark 44 | function mark(action) { 45 | if (!action) { 46 | currentMark = 0; 47 | window.performance.mark("checkpoint" + currentMark); 48 | } 49 | else { 50 | currentMark++; 51 | window.performance.mark("checkpoint" + currentMark); 52 | window.performance.measure(action, "checkpoint" + (currentMark - 1), "checkpoint" + currentMark); 53 | } 54 | } 55 | 56 | // Take in a single test run function and key for this run, then 57 | // it executes that single run for all the iterations required, then 58 | // at the end it moves the performance measures into the "timings" variable 59 | // that was previously initialized. 60 | function cycle(singleRun, key) { 61 | return function() { 62 | window.performance.clearMeasures(); 63 | 64 | var promise = new Promise(singleRun); 65 | for (var i = 1; i < iterations; i++) { 66 | promise = promise.then(function() { 67 | return new Promise(singleRun); 68 | }); 69 | } 70 | 71 | return promise.then(function() { 72 | return new Promise(function(resolve) { 73 | window.performance.getEntriesByType("measure").forEach(function(item) { 74 | var t = timings[item.name][key]; 75 | t[0] += item.duration; 76 | t[1] = t[1] ? Math.min(t[1], item.duration) : item.duration; 77 | t[2] = t[2] ? Math.max(t[2], item.duration) : item.duration; 78 | }); 79 | 80 | completeTask(resolve); 81 | }); 82 | }); 83 | }; 84 | } 85 | 86 | // Take the "timings" results and add them to the table. 87 | function populateResults() { 88 | var $table = $("table"); 89 | Object.keys(timings).forEach(function(key) { 90 | var $row = $("").appendTo($table); 91 | $row.append("" + key + ""); 92 | Object.keys(timings[key]).forEach(function(cycle) { 93 | var nums = timings[key][cycle], 94 | avg = Math.floor(nums[0] / iterations * 10) / 10, 95 | min = Math.floor(nums[1] * 10) / 10, 96 | max = Math.floor(nums[2] * 10) / 10; 97 | 98 | if ((avg + min + max) === 0) { 99 | $row.append('X'); 100 | } 101 | else { 102 | $row.append('' + avg + "" + min + "" + max + ""); 103 | } 104 | }); 105 | }); 106 | } 107 | 108 | window.addEventListener("load", function() { 109 | $msgs = $("#msgs"); 110 | addMsg("Page loaded."); 111 | }); 112 | -------------------------------------------------------------------------------- /examples/todomvc-with-component/todomvc-vue-with-component.js: -------------------------------------------------------------------------------- 1 | // Install Plugin 2 | Vue.use(VueBackbone); 3 | 4 | window.onload = function() { 5 | /** 6 | * Vue instance based on the Vue.js TodoMVC example 7 | * https://github.com/tastejs/todomvc/tree/master/examples/vue 8 | */ 9 | 10 | Vue.component("todo", { 11 | template: "#todo-template", 12 | 13 | props: ["model"], 14 | 15 | bb: function() { 16 | return { 17 | model: { prop: true } 18 | }; 19 | }, 20 | 21 | data: function() { 22 | return { 23 | editing: false 24 | }; 25 | }, 26 | 27 | methods: { 28 | removeTodo: function() { 29 | this.model.destroy(); 30 | }, 31 | 32 | editTodo: function() { 33 | this.beforeEditCache = this.model.title; 34 | this.editing = true; 35 | }, 36 | 37 | doneEdit: function() { 38 | if (!this.editing) { 39 | return; 40 | } 41 | this.editing = false; 42 | this.model.title = this.model.title.trim(); 43 | if (!this.model.title) { 44 | this.removeTodo(); 45 | } 46 | }, 47 | 48 | cancelEdit: function() { 49 | this.editing = false; 50 | this.model.title = this.beforeEditCache; 51 | } 52 | }, 53 | 54 | // a custom directive to wait for the DOM to be updated 55 | // before focusing on the input field. 56 | // http://vuejs.org/guide/custom-directive.html 57 | directives: { 58 | "todo-focus": function(el, binding) { 59 | if (binding.value) { 60 | el.focus(); 61 | } 62 | } 63 | } 64 | }); 65 | 66 | var app = new Vue({ 67 | // Vue-Backbone initial options 68 | bb: function() { 69 | return { 70 | todos: todos 71 | }; 72 | }, 73 | 74 | // app initial state 75 | data: { 76 | newTodo: "", 77 | visibility: "all" 78 | }, 79 | 80 | // computed properties 81 | // http://vuejs.org/guide/computed.html 82 | computed: { 83 | // Runs a function on the collection based on 84 | // visibility, as this function returns an array 85 | // of models, mapBBModels is used to provide 86 | // enhanced model objects 87 | filteredTodos: VueBackbone.mapBBModels(function() { 88 | return this.todos[this.visibility](); 89 | }), 90 | 91 | remaining: function() { 92 | return this.todos.active().length; 93 | }, 94 | 95 | allDone: { 96 | get: function() { 97 | return this.remaining === 0; 98 | }, 99 | set: function(value) { 100 | // Making use of built-in Backbone Collection/Model functionality 101 | this.todos.each(function(todo) { 102 | todo.set("completed", value); 103 | }); 104 | } 105 | } 106 | }, 107 | 108 | filters: { 109 | pluralize: function(n) { 110 | return n === 1 ? "item" : "items"; 111 | } 112 | }, 113 | 114 | methods: { 115 | addTodo: function() { 116 | var value = this.newTodo && this.newTodo.trim(); 117 | if (!value) { 118 | return; 119 | } 120 | this.todos.create({ 121 | title: value, 122 | completed: false, 123 | order: this.todos.nextOrder() 124 | }); 125 | this.newTodo = ""; 126 | }, 127 | 128 | removeCompleted: function() { 129 | _.invoke(this.todos.completed(), "destroy"); 130 | } 131 | } 132 | }); 133 | 134 | // handle routing 135 | function onHashChange() { 136 | var visibility = window.location.hash.replace(/#\/?/, ""); 137 | if (todos[visibility]) { 138 | app.visibility = visibility; 139 | } else { 140 | window.location.hash = ""; 141 | app.visibility = "all"; 142 | } 143 | } 144 | 145 | window.addEventListener("hashchange", onHashChange); 146 | onHashChange(); 147 | 148 | // mount 149 | app.$mount(".todoapp"); 150 | }; 151 | -------------------------------------------------------------------------------- /examples/comparison/parents.js: -------------------------------------------------------------------------------- 1 | /********************************************************************************** 2 | * Vue Classes 3 | **********************************************************************************/ 4 | 5 | var vueTemplates = [ 6 | '
' + 7 | "{{ collection.totalValue() }}, {{ evenModels.length }}, {{ oddModels.length }}
", 8 | 9 | '
' + 10 | "{{ collection.totalValue() }}, {{ evenModels.length }}, {{ oddModels.length }}
", 11 | 12 | '
' + 13 | "{{ collectionObj.totalValue() }}, {{ evenModels.length }}, {{ oddModels.length }}
", 14 | 15 | '
' + 16 | "{{ collection.totalValue() }}, {{ evenModels.length }}, {{ oddModels.length }}
" 17 | ]; 18 | 19 | var computed = [ 20 | { 21 | collection: function() { 22 | return this.$options.collection; 23 | }, 24 | evenModels: function() { 25 | return this.$options.collection.filter(function(model) { 26 | return !(model.get("value") % 2); 27 | }); 28 | }, 29 | oddModels: function() { 30 | return this.$options.collection.oddModels().map(function(m) { 31 | return m.attributes; 32 | }); 33 | } 34 | }, 35 | { 36 | list: function() { 37 | return this.collection.models; 38 | }, 39 | evenModels: function() { 40 | return this.collection.filter(function(model) { 41 | return !(model.get("value") % 2); 42 | }); 43 | }, 44 | oddModels: function() { 45 | return this.collection.oddModels(); 46 | } 47 | }, 48 | { 49 | list: function() { 50 | return this.$bb.collection.models; 51 | }, 52 | collectionObj: function() { 53 | return this.$bb.collection; 54 | }, 55 | evenModels: VueBackbone.mapBBModels(function() { 56 | // explicit mapping to attributes 57 | return this.$bb.collection.filter(function(model) { 58 | return !(model.get("value") % 2); 59 | }); 60 | }), 61 | oddModels: VueBackbone.mapBBModels(function() { 62 | // explicit mapping to attributes 63 | return this.$bb.collection.oddModels(); 64 | }) 65 | }, 66 | { 67 | evenModels: function() { 68 | return this.collection.filter(function(model) { 69 | return !(model.get("value") % 2); 70 | }); // native array (no proxy mapping necessary) 71 | }, 72 | oddModels: VueBackbone.mapBBModels(function() { 73 | // explicit proxy mapping needed 74 | return this.collection.oddModels(); 75 | }) 76 | } 77 | ]; 78 | 79 | var TestVueA = Vue.extend({ 80 | template: vueTemplates[0], 81 | data: function() { 82 | return { list: [] }; 83 | }, 84 | computed: computed[0], 85 | methods: { 86 | changeCollection: function(coll) { 87 | var vm = this; 88 | 89 | vm.unsync && vm.unsync(); 90 | 91 | vm.$options.collection = coll; 92 | vm.list = coll.toArray(); 93 | var onadd = function(model) { 94 | vm.list.push(model); 95 | }; 96 | var onremove = function(model) { 97 | var pos = vm.list.indexOf(model); 98 | vm.list.splice(pos, 1); 99 | }; 100 | var onreset = function() { 101 | vm.list = coll.toArray(); 102 | }; 103 | coll.on("add", onadd); 104 | coll.on("remove", onremove); 105 | coll.on("reset sort", onreset); 106 | 107 | vm.unsync = function() { 108 | coll.off("add", onadd); 109 | coll.off("remove", onremove); 110 | coll.off("reset sort", onreset); 111 | }; 112 | } 113 | }, 114 | created: function() { 115 | this.changeCollection(this.$options.collection); 116 | } 117 | }); 118 | 119 | var TestVueB = Vue.extend({ 120 | template: vueTemplates[1], 121 | computed: computed[1], 122 | methods: { 123 | changeCollection: function(coll) { 124 | this.collection = coll; 125 | } 126 | } 127 | }); 128 | 129 | var TestVueC = Vue.extend({ 130 | template: vueTemplates[2], 131 | computed: computed[2], 132 | methods: { 133 | changeCollection: function(coll) { 134 | this.$bb.collection = coll; 135 | } 136 | } 137 | }); 138 | 139 | var TestVueD = Vue.extend({ 140 | template: vueTemplates[3], 141 | computed: computed[3], 142 | methods: { 143 | changeCollection: function(coll) { 144 | this.collection = coll; 145 | } 146 | } 147 | }); 148 | -------------------------------------------------------------------------------- /test/no-mapping-model-spec.js: -------------------------------------------------------------------------------- 1 | import Backbone from 'backbone' 2 | import Vue from 'vue/dist/vue' 3 | import VueBackbone from '../src/vue-backbone.js' 4 | import $ from 'jquery' 5 | 6 | Vue.use(VueBackbone); 7 | 8 | describe('Unmapped Model', () => { 9 | 10 | let $sandbox, el, template; 11 | 12 | beforeAll(() => { 13 | // Hack to reset the options inside VueBackbone after installation 14 | VueBackbone.install( 15 | {mixin: () => {}}, 16 | {proxies: false} 17 | ); 18 | }); 19 | 20 | afterAll(() => { 21 | VueBackbone.install( 22 | {mixin: () => {}}, 23 | {proxies: true} 24 | ); 25 | }); 26 | 27 | beforeEach(() => { 28 | $sandbox = $('
'); 29 | el = $sandbox.children()[0]; 30 | }); 31 | 32 | it('should provide raw Backbone data access in Vue data object', (done) => { 33 | 34 | let model = new Backbone.Model({name: 'itemA'}); 35 | 36 | new Vue({ 37 | el, 38 | bb: () => ({item: model}), 39 | template: '
{{ JSON.stringify(item) }}
', 40 | mounted() { 41 | expect($sandbox.html()).toBe('
{"name":"itemA"}
'); 42 | done(); 43 | } 44 | }); 45 | }); 46 | 47 | it('should not provide access to Model Proxy via Vue computed hash', (done) => { 48 | 49 | let model = new Backbone.Model({name: 'itemA'}); 50 | spyOn(console, 'error'); 51 | console.error.calls.reset(); 52 | 53 | new Vue({ 54 | el, 55 | bb: () => ({item: model}), 56 | template: '
{{ JSON.stringify(item.has(\'name\')) }}
', 57 | mounted() { 58 | var errmsg = console.error.calls.mostRecent().args[0].message 59 | , expectation1 = 'item.has is not a function' // Chrome 60 | , expectation2 = 'undefined is not a function (evaluating \'item.has(\'name\')\')'; // PhantomJS 61 | 62 | expect(errmsg === expectation1 || errmsg === expectation2).toBeTruthy(); 63 | done(); 64 | } 65 | }); 66 | }); 67 | 68 | describe('reactions for data access directly from template', () => { 69 | 70 | beforeEach(() => { 71 | template = '
{{ item.name }}
'; 72 | }); 73 | 74 | it('doesnt make new attributes reactive, therefore wont recognise them', (done) => { 75 | 76 | let model = new Backbone.Model(); 77 | spyOn(console, 'warn'); 78 | 79 | new Vue({ 80 | el, 81 | template, 82 | bb: () => ({item: model}), 83 | mounted() { 84 | expect($sandbox.html()).toBe('
'); 85 | model.set({name: 'itemA', value: 1}); 86 | expect(console.warn).toHaveBeenCalledWith('VueBackbone: Adding new Model attributes after binding is not supported, provide defaults for all properties'); 87 | setTimeout(done); // defer 88 | }, 89 | updated() { 90 | fail('Vue should not have updated.'); 91 | } 92 | }); 93 | }); 94 | 95 | it('accepts a model with defaults and reacts to changes', (done) => { 96 | 97 | let model = new (Backbone.Model.extend({ 98 | defaults: { 99 | name: undefined, 100 | value: undefined 101 | } 102 | }))(); 103 | 104 | new Vue({ 105 | el, 106 | template, 107 | bb: () => ({item: model}), 108 | mounted() { 109 | expect($sandbox.html()).toBe('
'); 110 | model.set({name: 'itemA', value: 1}); 111 | }, 112 | updated() { 113 | expect($sandbox.html()).toBe('
itemA
'); 114 | done(); 115 | } 116 | }); 117 | }); 118 | 119 | it('reacts to model replacement', (done) => { 120 | 121 | let model = new Backbone.Model({name: 'itemA', value: undefined}); 122 | 123 | new Vue({ 124 | el, 125 | template, 126 | bb: () => ({item: model}), 127 | mounted() { 128 | expect($sandbox.html()).toBe('
itemA
'); 129 | this.$bb.item = new Backbone.Model({name: 'allNew', value: 1}); 130 | }, 131 | updated() { 132 | expect($sandbox.html()).toBe('
allNew
'); 133 | done(); 134 | } 135 | }); 136 | }); 137 | 138 | }); 139 | 140 | 141 | it('components and properties wont work with unmapped models', (done) => { 142 | 143 | spyOn(console, 'error'); 144 | 145 | let model = new Backbone.Model({name: 'itemA', value: undefined}); 146 | 147 | Vue.component('item', { 148 | props: ['model'], 149 | bb: () => ({model: {prop: true}}), 150 | template: '
{{ model.name }}
' 151 | }); 152 | 153 | new Vue({ 154 | el, 155 | template: '

', 156 | bb: () => ({item: model}), 157 | mounted() { 158 | expect(console.error).toHaveBeenCalledWith('VueBackbone: Unrecognized Backbone object in Vue instantiation (model), must be a Collection or Model'); 159 | done(); 160 | } 161 | }); 162 | 163 | }); 164 | 165 | }); -------------------------------------------------------------------------------- /test/no-mapping-collection-spec.js: -------------------------------------------------------------------------------- 1 | import Backbone from 'backbone' 2 | import Vue from 'vue/dist/vue' 3 | import VueBackbone, { mapBBModels } from '../src/vue-backbone.js' 4 | import $ from 'jquery' 5 | 6 | Vue.use(VueBackbone); 7 | 8 | describe('No Model-mapping Collection', () => { 9 | 10 | let $sandbox, el, template; 11 | 12 | beforeAll(() => { 13 | // Hack to reset the options inside VueBackbone after installation 14 | VueBackbone.install( 15 | {mixin: () => {}}, 16 | {proxies: false} 17 | ); 18 | }); 19 | 20 | afterAll(() => { 21 | VueBackbone.install( 22 | {mixin: () => {}}, 23 | {proxies: true} 24 | ); 25 | }); 26 | 27 | beforeEach(() => { 28 | $sandbox = $('
'); 29 | el = $sandbox.children()[0]; 30 | }); 31 | 32 | it('should provide raw Backbone data access in Vue data object', (done) => { 33 | 34 | let collection = new Backbone.Collection([{name: 'itemA'}]); 35 | 36 | new Vue({ 37 | el, 38 | bb: () => ({list: collection}), 39 | template: '
{{ JSON.stringify(list) }}
', 40 | mounted() { 41 | expect($sandbox.html()).toBe('
[{"name":"itemA"}]
'); 42 | done(); 43 | } 44 | }); 45 | }); 46 | 47 | it('should not provide access to array of Model Proxies via Vue computed hash', (done) => { 48 | 49 | let collection = new Backbone.Collection([{name: 'itemA'}]); 50 | spyOn(console, 'error'); 51 | console.error.calls.reset(); 52 | 53 | new Vue({ 54 | el, 55 | bb: () => ({list: collection}), 56 | template: '
{{ list[0].has(\'name\') }}
', 57 | mounted() { 58 | var errmsg = console.error.calls.mostRecent().args[0].message 59 | , expectation1 = 'list[0].has is not a function' // Chrome 60 | , expectation2 = 'undefined is not a function (evaluating \'list[0].has(\'name\')\')'; // PhantomJS 61 | 62 | expect(errmsg === expectation1 || errmsg === expectation2).toBeTruthy(); 63 | done(); 64 | } 65 | }); 66 | }); 67 | 68 | describe('reactions for data access directly from template', () => { 69 | 70 | beforeEach(() => { 71 | template = '
{{ list.length ? list[list.length - 1].name : "empty!" }}
'; 72 | }); 73 | 74 | it('accepts empty collection, and reacts to additional row', (done) => { 75 | 76 | let collection = new Backbone.Collection(); 77 | 78 | new Vue({ 79 | el, 80 | template, 81 | bb: () => ({list: collection}), 82 | mounted() { 83 | expect($sandbox.html()).toBe('
empty!
'); 84 | collection.add({name: 'itemA'}); 85 | }, 86 | updated() { 87 | expect($sandbox.html()).toBe('
itemA
'); 88 | done(); 89 | } 90 | }); 91 | }); 92 | 93 | it('accepts a collection with row, and reacts to removed row', (done) => { 94 | 95 | let collection = new Backbone.Collection([{name: 'itemA'}]); 96 | 97 | new Vue({ 98 | el, 99 | data: {a: 1}, 100 | template, 101 | bb: () => ({list: collection}), 102 | mounted() { 103 | expect($sandbox.html()).toBe('
itemA
'); 104 | collection.remove(collection.first()); 105 | }, 106 | updated() { 107 | expect($sandbox.html()).toBe('
empty!
'); 108 | done(); 109 | } 110 | }); 111 | }); 112 | 113 | it('reacts to collection replacement', (done) => { 114 | 115 | let collection = new Backbone.Collection([{name: 'itemA'}]); 116 | 117 | new Vue({ 118 | el, 119 | data: {a: 1}, 120 | template, 121 | bb: () => ({list: collection}), 122 | mounted() { 123 | expect($sandbox.html()).toBe('
itemA
'); 124 | this.$bb.list = new Backbone.Collection([{name: 'allNew'}]); 125 | }, 126 | updated() { 127 | expect($sandbox.html()).toBe('
allNew
'); 128 | done(); 129 | } 130 | }); 131 | }); 132 | 133 | }); 134 | 135 | 136 | it('mapped responses in computed values', (done) => { 137 | 138 | let collection = new Backbone.Collection([{name: 'itemA'}]); 139 | 140 | new Vue({ 141 | el, 142 | template: '
{{ firstItem && firstItem.name }}, {{ validItems[0] && validItems[0].name }}
', 143 | bb: () => ({list: collection}), 144 | computed: { 145 | firstItem: mapBBModels(function() { // manual mapping required still (model instances converted to model data) 146 | return this.$bb.list.first(); 147 | }), 148 | validItems: mapBBModels(function() { 149 | return this.$bb.list.reject(model => !model.has('name')); 150 | }) 151 | }, 152 | mounted() { 153 | expect($sandbox.html()).toBe('
itemA, itemA
'); 154 | collection.remove(collection.first()); 155 | }, 156 | updated() { 157 | expect($sandbox.html()).toBe('
,
'); 158 | done(); 159 | } 160 | }); 161 | 162 | }); 163 | 164 | 165 | }); -------------------------------------------------------------------------------- /dist/vue-backbone.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Vue-Backbone v0.1.3 3 | * https://github.com/mikeapr4/vue-backbone 4 | * @license MIT 5 | */ 6 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.VueBackbone=t():e.VueBackbone=t()}(this,function(){return r={},o.m=n=[function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){var n=e.map(function(e){return e._vuebackbone_proxy});for(var o in e)if("function"==typeof e[o]&&"constructor"!==o&&(t||-1===i.indexOf(o))){var r=e[o].bind(e);t||(r=u(r,o)),Object.defineProperty(n,o,{value:r})}return Object.defineProperty(n,"_vuebackbone_original",{value:e}),n};var i=["slice","forEach","map","reduce","reduceRight","find","filter","every","some","indexOf","lastIndexOf","findIndex"],o=["pop","shift","remove","get","at","where","findWhere","reject","sortBy","shuffle","toArray","detect","select","first","head","take","rest","tail","drop","initial","last","without"];function u(t,e){return-1Object.keys(t._previousAttributes).length&&console.warn("VueBackbone: Adding new Model attributes after binding is not supported, provide defaults for all properties")},t.on("change",e.onchange)}(n,o)}function d(e,t){var n=e._vuebackbone[t];n&&(n.bb.off(null,n.onchange),n.onreset&&n.bb.off(null,n.onreset))}function l(e,t,n,o){e._vuebackbone[t]={bb:n},a(n),o||(function(e,o){var r=e.$options.data,i=b(e._vuebackbone[o].bb),u=s(o);e.$options.data=function(){var t={},n=r?r.apply(this,arguments):{};if(n.hasOwnProperty(o))throw"VueBackbone: Property '"+o+"' mustn't exist within the Vue data already";if(n.hasOwnProperty(u))throw"VueBackbone: Property '"+u+"' mustn't exist within the Vue data already";return Object.keys(n).forEach(function(e){return t[e]=n[e]}),t[u]=i,t}}(e,t),u.addComputed&&u.proxies?function(t,n){var o=t._vuebackbone[n],r=s(n),e=t.$options;e.computed=e.computed||{},e.computed[n]?console.warn("VueBackbone: Generated computed function '"+n+"' already exists within the Vue computed functions"):e.computed[n]={get:function(){return t.$data[r],o.bb._vuebackbone_proxy},set:function(e){d(t,n),a(e),o.bb=e,t.$data[r]=b(e),p(t,n)}}}(e,t):function(t,n){var o=t._vuebackbone[n],r=s(n);t.$bb=t.$bb||{},Object.defineProperty(t.$bb,n,{get:function(){return t.$data[r],o.bb},set:function(e){d(t,n),o.bb=e,t.$data[r]=b(e),p(t,n)}})}(e,t)),p(e,t)}var v={beforeCreate:function(){var o=this,r=o.$options.bb;if(r){if("function"!=typeof r)throw"VueBackbone: 'bb' initialization option must be a function";r=r(),o._vuebackbone={},Object.keys(r).forEach(function(e){var t=r[e],n=!1;if(!0===t.prop){if(!o.$options.propsData||!o.$options.propsData[e])throw"VueBackbone: Missing Backbone object in Vue prop '"+e+"'";t=o.$options.propsData[e],n=!0}if(!(t=t._vuebackbone_original||t).on||!t.attributes&&!t.models)throw"VueBackbone: Unrecognized Backbone object in Vue instantiation ("+e+"), must be a Collection or Model";l(o,e,t,n)})}},destroyed:function(){var t=this,e=t._vuebackbone;e&&Object.keys(e).forEach(function(e){return d(t,e)})}};function y(t){return u.proxies?function(){var e=t.apply(this,arguments);return e&&(e._vuebackbone_proxy||e.map(function(e){return e._vuebackbone_proxy}))||e}:function(){var e=t.apply(this,arguments);return e&&(e.attributes||e.map(function(e){return e.attributes}))||e}}function _(e,t){for(var n in t)t.hasOwnProperty(n)&&(u[n]=t[n]);e.mixin(v)}function h(e){return e._vuebackbone_original||e}t.default={install:_,mapBBModels:y,original:h}}],o.c=r,o.i=function(e){return e},o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:n})},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=2);function o(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}var n,r}); -------------------------------------------------------------------------------- /test/initialization-spec.js: -------------------------------------------------------------------------------- 1 | import Backbone from 'backbone' 2 | import Vue from 'vue/dist/vue' 3 | import VueBackbone from '../src/vue-backbone.js' 4 | import $ from 'jquery' 5 | 6 | Vue.use(VueBackbone); 7 | 8 | describe('Initialization', () => { 9 | 10 | let $sandbox, el; 11 | 12 | beforeEach(() => { 13 | $sandbox = $('
'); 14 | el = $sandbox.children()[0]; 15 | }); 16 | 17 | it('should allow Vue renders without any VueBackbone integration', (done) => { 18 | new Vue({ 19 | el, 20 | data: {list: 1}, 21 | template: '
{{ list }}
', 22 | mounted() { 23 | expect($sandbox.html()).toBe('
1
'); 24 | done(); 25 | } 26 | }); 27 | }); 28 | 29 | it('should fail fatally where Vue data object contains attribute already for VueBackbone collection', (done) => { 30 | 31 | let collection = new Backbone.Collection(); 32 | spyOn(console, 'error'); 33 | 34 | new Vue({ 35 | el, 36 | data: {list: 1}, 37 | bb: () => ({list: collection}), 38 | template: '
{{ list }}
', 39 | mounted() { 40 | expect(console.error).toHaveBeenCalledWith('VueBackbone: Property \'list\' mustn\'t exist within the Vue data already'); 41 | done(); 42 | } 43 | }); 44 | }); 45 | 46 | it('should fail fatally where Vue data object contains secret attribute already for VueBackbone collection', (done) => { 47 | 48 | let collection = new Backbone.Collection(); 49 | spyOn(console, 'error'); 50 | 51 | new Vue({ 52 | el, 53 | data: {_list: 1}, 54 | bb: () => ({list: collection}), 55 | template: '
{{ list }}
', 56 | mounted() { 57 | expect(console.error).toHaveBeenCalledWith('VueBackbone: Property \'_list\' mustn\'t exist within the Vue data already'); 58 | done(); 59 | } 60 | }); 61 | }); 62 | 63 | it('should fail fatally where the bb options are not a function', (done) => { 64 | 65 | let collection = new Backbone.Collection(); 66 | spyOn(console, 'error'); 67 | 68 | new Vue({ 69 | el, 70 | bb: {list: collection}, 71 | template: '
{{ list }}
', 72 | mounted() { 73 | expect(console.error).toHaveBeenCalledWith('VueBackbone: \'bb\' initialization option must be a function'); 74 | done(); 75 | } 76 | }); 77 | }); 78 | 79 | it('should fail fatally where a non-Backbone object is passed in the bb options', (done) => { 80 | 81 | spyOn(console, 'error'); 82 | 83 | new Vue({ 84 | el, 85 | bb: () => ({list: []}), 86 | template: '
{{ list }}
', 87 | mounted() { 88 | expect(console.error).toHaveBeenCalledWith('VueBackbone: Unrecognized Backbone object in Vue instantiation (list), must be a Collection or Model'); 89 | done(); 90 | } 91 | }); 92 | }); 93 | 94 | it('should fail fatally where there is no Property for a bb option set to prop', (done) => { 95 | 96 | spyOn(console, 'error'); 97 | 98 | new Vue({ 99 | el, 100 | bb: () => ({list: {prop: true}}), 101 | template: '
{{ list }}
', 102 | mounted() { 103 | expect(console.error).toHaveBeenCalledWith('VueBackbone: Missing Backbone object in Vue prop \'list\''); 104 | done(); 105 | } 106 | }); 107 | }); 108 | 109 | it('should work when no Vue data object was specified', (done) => { 110 | 111 | let collection = new Backbone.Collection(); 112 | 113 | new Vue({ 114 | el, 115 | bb: () => ({list: collection}), 116 | template: '
{{ list }}
', 117 | mounted() { 118 | expect($sandbox.html()).toBe('
[]
'); 119 | done(); 120 | } 121 | }); 122 | }); 123 | 124 | it('should correctly merge data and bb options', (done) => { 125 | 126 | let collection = new Backbone.Collection(); 127 | 128 | new Vue({ 129 | el, 130 | data: {a: 1}, 131 | bb: () => ({list: collection}), 132 | template: '
{{ list }}{{ a }}
', 133 | mounted() { 134 | expect($sandbox.html()).toBe('
[]1
'); 135 | done(); 136 | } 137 | }); 138 | }); 139 | 140 | it('should respect the data function', (done) => { 141 | 142 | let collection = new Backbone.Collection(); 143 | 144 | const Child = { 145 | data() { 146 | return { uid: this._uid }; 147 | }, 148 | bb: () => ({list: collection}), 149 | template: '
{{ list }}{{ uid }}
', 150 | } 151 | 152 | new Vue({ 153 | el, 154 | components: { Child }, 155 | template: '', 156 | mounted() { 157 | expect($sandbox.html()).toBe('
[]' + this.$children[0]._uid + '
'); 158 | done(); 159 | } 160 | }); 161 | }); 162 | 163 | it('should trigger unbind when destroyed', (done) => { 164 | 165 | let collection = new Backbone.Collection(); 166 | 167 | new Vue({ 168 | el, 169 | data: {a: 1}, 170 | bb: () => ({list: collection}), 171 | template: '
{{ list }}{{ a }}
', 172 | mounted() { 173 | expect($sandbox.html()).toBe('
[]1
'); 174 | this.$destroy(); 175 | }, 176 | destroyed() { 177 | collection.add({a: 1}); 178 | expect(this.$data._list).toEqual([]); 179 | done(); 180 | } 181 | }); 182 | }); 183 | 184 | it('should accept original Backbone objects in VueBackbone.original', () => { 185 | const model = new Backbone.Model({id: 'A'}); 186 | expect(VueBackbone.original(model)).toBe(model); 187 | }); 188 | 189 | }); -------------------------------------------------------------------------------- /examples/comparison/run.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function() { 2 | /********************************************************************************** 3 | * Functionality under Test 4 | **********************************************************************************/ 5 | 6 | initTimings( 7 | [ 8 | "Creation", 9 | "Mount", 10 | "New Row", 11 | "Remove Row", 12 | "Replace Collection", 13 | "Reset Collection", 14 | "Model Change" 15 | ], 16 | ["A", "B", "C", "D"] 17 | ); 18 | 19 | function genericSingleRun( 20 | beforeOp, 21 | vueInstance, 22 | instanceOpts, 23 | addOp, 24 | removeOp, 25 | changeCollectionOp, 26 | replaceCollectionOp, 27 | changeValueOp, 28 | resolve 29 | ) { 30 | var collection = new TestCollection(JSON.parse(collectionJson)), 31 | collection2 = new TestCollection(JSON.parse(collection2Json)), 32 | child; 33 | 34 | // Run any setup 35 | beforeOp && beforeOp(); 36 | 37 | mark(); 38 | 39 | var step5 = function() { 40 | var vm = this; 41 | // var row = $(vm.$el).text(); 42 | // var model = vm.model.get('value') + ', ' + vm.model.getPrecision() + ', '; 43 | // console.assert(row === model); 44 | mark("Model Change"); 45 | vm.$parent.$destroy(); 46 | setTimeout(resolve); 47 | }, 48 | step4 = function() { 49 | var vm = this; 50 | // var firstRow = $(vm.$el).find('span').first().text(); 51 | // var firstModel = collection2.first().get('value') + ', ' + collection2.first().getPrecision() + ', '; 52 | // console.assert(firstRow === firstModel); 53 | mark("Reset Collection"); 54 | nextUpdate = function() {}; 55 | child = vm.$children[0]; 56 | child.$on("updated", step5); 57 | changeValueOp(child); 58 | }, 59 | step3 = function() { 60 | var vm = this; 61 | // var firstRow = vm.$children[0].$el.innerText; 62 | // var firstModel = collection2.first().get('value') + ', ' + collection2.first().getPrecision() + ', '; 63 | // console.assert(firstRow === firstModel); 64 | mark("Replace Collection"); 65 | nextUpdate = step4; 66 | replaceCollectionOp(vm, JSON.parse(collectionJson)); 67 | }, 68 | step2 = function() { 69 | var vm = this, 70 | rows = $(vm.$el).find("span").length; 71 | // Update (may) fires twice during this operation (2-way sync) 72 | if (rows === 1001) { 73 | return; 74 | } 75 | mark("Remove Row"); 76 | nextUpdate = step3; 77 | changeCollectionOp(vm, collection2); 78 | }, 79 | step1 = function() { 80 | var vm = this; 81 | // console.assert($(vm.$el).find('span').length == 1001); 82 | mark("New Row"); 83 | nextUpdate = step2; 84 | removeOp(vm); 85 | }, 86 | nextUpdate = step1; 87 | 88 | new vueInstance( 89 | _.extend( 90 | { 91 | el: $("
").children()[0], 92 | 93 | beforeMount: function() { 94 | mark("Creation"); 95 | }, 96 | mounted: function() { 97 | var vm = this; 98 | // console.assert($(vm.$el).find('span').length == 1000); 99 | mark("Mount"); 100 | addOp(vm); 101 | }, 102 | updated: function() { 103 | nextUpdate.apply(this); 104 | } 105 | }, 106 | instanceOpts(collection) 107 | ) 108 | ); 109 | } 110 | 111 | var singleRunA = genericSingleRun.bind( 112 | null, 113 | null, 114 | TestVueA, 115 | function(collection) { 116 | return { collection: collection }; 117 | }, 118 | function(vm) { 119 | vm.$options.collection.add({ value: genVal() }); 120 | }, 121 | function(vm) { 122 | vm.$options.collection.remove(vm.$options.collection.first()); 123 | }, 124 | function(vm, coll) { 125 | vm.changeCollection(coll); 126 | }, 127 | function(vm, data) { 128 | vm.$options.collection.reset(data); 129 | }, 130 | function(child) { 131 | child.changeValue(); 132 | } 133 | ); 134 | 135 | var singleRunB = genericSingleRun.bind( 136 | null, 137 | null, 138 | TestVueB, 139 | function(collection) { 140 | return { data: { collection: collection } }; 141 | }, 142 | function(vm) { 143 | vm.collection.add({ value: genVal() }); 144 | }, 145 | function(vm) { 146 | vm.collection.remove(vm.collection.first()); 147 | }, 148 | function(vm, coll) { 149 | vm.changeCollection(coll); 150 | }, 151 | function(vm, data) { 152 | vm.collection.reset(data); 153 | }, 154 | function(child) { 155 | child.changeValue(); 156 | } 157 | ); 158 | 159 | var singleRunC = genericSingleRun.bind( 160 | null, 161 | function() { 162 | toggleProxies(false); 163 | }, 164 | TestVueC, 165 | function(collection) { 166 | return { 167 | bb: function() { 168 | return { collection: collection }; 169 | } 170 | }; 171 | }, 172 | function(vm) { 173 | vm.$bb.collection.add({ value: genVal() }); 174 | }, 175 | function(vm) { 176 | vm.$bb.collection.remove(vm.$bb.collection.first()); 177 | }, 178 | function(vm, coll) { 179 | vm.changeCollection(coll); 180 | }, 181 | function(vm, data) { 182 | vm.$bb.collection.reset(data); 183 | }, 184 | function(child) { 185 | child.changeValue(); 186 | } 187 | ); 188 | 189 | var singleRunD = genericSingleRun.bind( 190 | null, 191 | function() { 192 | toggleProxies(true); 193 | }, 194 | TestVueD, 195 | function(collection) { 196 | return { 197 | bb: function() { 198 | return { collection: collection }; 199 | } 200 | }; 201 | }, 202 | function(vm) { 203 | vm.collection.add({ value: genVal() }); 204 | }, 205 | function(vm) { 206 | vm.collection.remove(VueBackbone.original(vm.collection.first())); 207 | }, 208 | function(vm, coll) { 209 | vm.changeCollection(coll); 210 | }, 211 | function(vm, data) { 212 | vm.collection.reset(data); 213 | }, 214 | function(child) { 215 | child.changeValue(); 216 | } 217 | ); 218 | 219 | $('
Start
') 220 | .click(function(e) { 221 | e.preventDefault(); 222 | $(e.target).remove(); 223 | 224 | addTask("Populating Collections")() 225 | .then(populateCollections) 226 | .then(addTask("Cycle A")) 227 | .then(cycle(singleRunA, "A")) 228 | .then(addTask("Cycle B")) 229 | .then(cycle(singleRunB, "B")) 230 | .then(addTask("Cycle C")) 231 | .then(cycle(singleRunC, "C")) 232 | .then(addTask("Cycle D")) 233 | .then(cycle(singleRunD, "D")) 234 | .then(populateResults); 235 | }) 236 | .appendTo($msgs); 237 | }); 238 | -------------------------------------------------------------------------------- /docs/guidelines.md: -------------------------------------------------------------------------------- 1 | # Usage Guidelines 2 | 3 | ## Initialization 4 | 5 | Vue Backbone is a Vue.js plugin, and as such it needs to be registered with Vue as follows: 6 | 7 | ```js 8 | // Install Plugin 9 | Vue.use(VueBackbone); 10 | ``` 11 | 12 | When creating/extending Vue instances or components, normally data is specified something like this: 13 | 14 | ```js 15 | new Vue({ 16 | template: '{{ message }}', 17 | el: '#app', 18 | data: { 19 | message: 'Hello Vue!' 20 | } 21 | }) 22 | ``` 23 | 24 | To use a Backbone Collection/Model within a Vue, instantiate as follows: 25 | 26 | ```js 27 | new Vue({ 28 | template: '{{ item.cost }}', 29 | el: '#app', 30 | bb: function() { 31 | return { 32 | item: model 33 | }; 34 | } 35 | }) 36 | ``` 37 | 38 | * The `bb` option is used 39 | * It must _always_ be a function that returns an object. 40 | * There can be multiple Backbone objects, each one given a unique key which will become accessible on the Vue instance. 41 | * `data` can be used independently also, it is not interfered with. 42 | * Backbone references on a Vue instances are mutable 43 | * e.g. `vm.item = collection[collection.indexOf(vm.item) + 1]` 44 | * See below for the interface these Backbone objects use once attached to the Vue instance. 45 | 46 | ### Components 47 | 48 | Vue Components are not constructed in Javascript, but rather from HTML tags and properties. When defining the component with `props`, the `bb` option should be included as above, but instead of a reference to a concrete Backbone object, it is flagged as a property, this way when the component is instantiated, it will find the Backbone object in the `props`. 49 | 50 | ```js 51 | Vue.component('item-view', { 52 | props: ['item'], 53 | template: '{{ item.cost }}', 54 | el: '#app', 55 | bb: function() { 56 | return { 57 | item: {prop: true} 58 | }; 59 | } 60 | }) 61 | ``` 62 | 63 | ## `Backbone.Model` Interface 64 | 65 | When a model is attached to a Vue, any access from the Vue goes through an enhanced interface \(the original model is not modified\). This interface is a **superset** of the `Backbone.Model` itself, all the standard Backbone methods \(including `get`/`set`\) along with any custom ones for this Model class, and the attributes of the model are exposed as read/write properties directly. 66 | 67 | ```js 68 | new Vue({ 69 | template: '{{ item.cost }}', 70 | el: '#app', 71 | bb: function() { 72 | return { 73 | item: new Backbone.Model({cost: 5}) 74 | }; 75 | } 76 | }) 77 | ``` 78 | 79 | ## `Backbone.Collection` Interface 80 | 81 | When a collection is attached to a Vue, any access from the Vue goes through an enhanced interface \(the original collection is not modified\). 82 | 83 | This interface behaves primarily like an array of Models \(with the enhanced interface above\). It can be used inside a `for...in` loop, and it supports direct entry access \(i.e. `collection[0]`\), and common array functions like `slice`, `forEach` and `map`. 84 | 85 | Added to this are the `Backbone.Collection` functions, e.g. `on`, `sortBy` or `first`. And any custom ones coming from this particular Collection class. 86 | 87 | > **Warning!** this interface allows mutation for internal use, however only use Backbone Collection functions for mutation of the array \(e.g. `add`, `push`, `pop`, `remove`\). Also ensure events fire for these mutations as otherwise Vue Backbone may not detect the change. 88 | 89 | Below is a full table of the supported functions, where by default they come from the `Array` prototype, but **\*** indicates a `Backbone.Collection` function, and **\*\*** indicates a `Backbone.Collection` function modified to return model\(s\) with the enhanced interface. 90 | 91 | | add\* | every | indexBy\* | modelId\* | set\* | 92 | | :--- | :--- | :--- | :--- | :--- | 93 | | all\* | fetch\* | indexOf | off\* | shift\*\* | 94 | | any\* | fill | initial\*\* | on\* | shuffle\*\* | 95 | | at\*\* | filter | initialize\* | once\* | size\* | 96 | | bind\* | find | inject\* | parse\* | slice | 97 | | chain\* | findIndex | invoke\* | partition\* | some | 98 | | clone\* | findLastIndex\* | isEmpty\* | pluck\* | sort\* | 99 | | collect\* | findWhere\*\* | join | pop\*\* | sortBy\*\* | 100 | | concat | first\*\* | keys | push\* | splice | 101 | | contains\* | foldl\* | last\*\* | reduce | stopListening\* | 102 | | copyWithin | foldr\* | lastIndexOf | reduceRight | sync\* | 103 | | countBy\* | forEach | length | reject\*\* | tail\*\* | 104 | | create\* | get\*\* | listenTo\* | remove\*\* | take\*\* | 105 | | detect\*\* | groupBy\* | listenToOnce\* | reset\* | toArray\*\* | 106 | | difference\* | has\* | map | rest\*\* | toJSON\* | 107 | | drop\*\* | head\*\* | max\* | reverse | toLocaleString | 108 | | each\* | include\* | min\* | sample\* | toString | 109 | | entries | includes\* | model\* | select\*\* | trigger\* | 110 | 111 | ## Converting to/from Enhanced Interfaces 112 | 113 | Sometimes it is convenient to convert the return of a `Backbone.Collection` function, from an array of models or a single model, into an array of _Vue Backbone_ models or a single one. Although this is automatically done for the in-built `Backbone.Collection` functions, a custom one may still return plain Backbone model\(s\). Here's an example: 114 | 115 | ```js 116 | new Vue({ 117 | bb: function() { 118 | return { 119 | items: collection 120 | }; 121 | }, 122 | computed: { 123 | validItems: function() { 124 | return this.items.getValidModels(); 125 | } 126 | } 127 | }) 128 | ``` 129 | 130 | the solution is `VueBackbone.mapBBModels()` which can be used as follows: 131 | 132 | ```js 133 | new Vue({ 134 | bb: function() { 135 | return { 136 | items: collection 137 | }; 138 | }, 139 | computed: { 140 | validItems: VueBackbone.mapBBModels(function() { 141 | return this.items.getValidModels(); 142 | }) 143 | } 144 | }) 145 | ``` 146 | 147 | Alternatively, it may be necessary to map a _Vue Backbone_ model/collection back to the original object. A good example of this is where the model is used as a parameter into a Backbone Collection function. 148 | 149 | ```js 150 | collection.remove(collection.first()) 151 | ``` 152 | 153 | The above code will not work with a _Vue Backbone_ collection, as the `first` function will return an enhanced interface, which is not strictly equivalent to any model in the collection. The `VueBackbone.original` function must be used: 154 | 155 | ```js 156 | collection.remove(VueBackbone.original(collection.first())) 157 | ``` 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /test/model-spec.js: -------------------------------------------------------------------------------- 1 | import Backbone from "backbone"; 2 | import Vue from "vue/dist/vue"; 3 | import VueBackbone from "../src/vue-backbone.js"; 4 | import $ from "jquery"; 5 | 6 | Vue.use(VueBackbone); 7 | 8 | describe("Model", () => { 9 | let $sandbox, el, template; 10 | 11 | beforeEach(() => { 12 | $sandbox = $("
"); 13 | el = $sandbox.children()[0]; 14 | }); 15 | 16 | it("should provide access to Model Proxy via Vue computed hash", done => { 17 | const model = new Backbone.Model({ name: "itemA" }); 18 | 19 | new Vue({ 20 | el, 21 | bb: () => ({ item: model }), 22 | template: 23 | "
{{ JSON.stringify(item.toJSON()) }}|{{ item.name }}
", 24 | mounted() { 25 | expect($sandbox.html()).toBe( 26 | '
{"name":"itemA"}|itemA
' 27 | ); 28 | done(); 29 | } 30 | }); 31 | }); 32 | 33 | it("should provide access to conflicted properties with prefix", done => { 34 | const model = new Backbone.Model({ set: "itemA" }); 35 | 36 | new Vue({ 37 | el, 38 | bb: () => ({ item: model }), 39 | template: "
{{ item.$set }}
", 40 | mounted() { 41 | expect($sandbox.html()).toBe("
itemA
"); 42 | done(); 43 | } 44 | }); 45 | }); 46 | 47 | it("should proxy the Model ID for specified idAttribute", done => { 48 | const Model = Backbone.Model.extend({ idAttribute: "name" }), 49 | model = new Model({ name: "itemA" }); 50 | 51 | new Vue({ 52 | el, 53 | bb: () => ({ item: model }), 54 | template: "
{{ item.id }}
", 55 | mounted() { 56 | expect($sandbox.html()).toBe("
itemA
"); 57 | done(); 58 | } 59 | }); 60 | }); 61 | 62 | it("should proxy the Model ID for default idAttribute", done => { 63 | const model = new Backbone.Model({ id: "A", name: "itemA" }); 64 | 65 | new Vue({ 66 | el, 67 | bb: () => ({ item: model }), 68 | template: "
{{ item.id }}
", 69 | mounted() { 70 | expect($sandbox.html()).toBe("
A
"); 71 | done(); 72 | } 73 | }); 74 | }); 75 | 76 | describe("reactions for data access directly from template", () => { 77 | beforeEach(() => { 78 | template = 79 | "
{{ item.name }}
"; 80 | }); 81 | 82 | it("doesnt make new attributes reactive, therefore wont recognise them", done => { 83 | const model = new Backbone.Model(); 84 | spyOn(console, "warn"); 85 | 86 | new Vue({ 87 | el, 88 | template, 89 | bb: () => ({ item: model }), 90 | mounted() { 91 | expect($sandbox.html()).toBe('
'); 92 | model.set({ name: "itemA", value: 1 }); 93 | expect(console.warn).toHaveBeenCalledWith( 94 | "VueBackbone: Adding new Model attributes after binding is not supported, provide defaults for all properties" 95 | ); 96 | setTimeout(done); // defer 97 | }, 98 | updated() { 99 | fail("Vue should not have updated."); 100 | } 101 | }); 102 | }); 103 | 104 | it("accepts a model with defaults and reacts to changes", done => { 105 | const model = new (Backbone.Model.extend({ 106 | defaults: { 107 | name: undefined, 108 | value: undefined 109 | } 110 | }))(); 111 | 112 | new Vue({ 113 | el, 114 | template, 115 | bb: () => ({ item: model }), 116 | mounted() { 117 | expect($sandbox.html()).toBe('
'); 118 | model.set({ name: "itemA", value: 1 }); 119 | }, 120 | updated() { 121 | expect($sandbox.html()).toBe( 122 | '
itemA
' 123 | ); 124 | done(); 125 | } 126 | }); 127 | }); 128 | 129 | it("will trigger Backbone events for Vue originating changes", done => { 130 | const model = new (Backbone.Model.extend({ 131 | defaults: { 132 | name: undefined, 133 | value: undefined 134 | } 135 | }))(); 136 | 137 | model.on("change:name", done); 138 | 139 | new Vue({ 140 | el, 141 | template, 142 | bb: () => ({ item: model }), 143 | mounted() { 144 | expect($sandbox.html()).toBe('
'); 145 | this.item.name = "abc"; 146 | } 147 | }); 148 | 149 | // Test times out if it fails 150 | }); 151 | 152 | it("reacts to model replacement", done => { 153 | const model = new Backbone.Model({ 154 | name: "itemA", 155 | value: undefined 156 | }); 157 | 158 | new Vue({ 159 | el, 160 | template, 161 | bb: () => ({ item: model }), 162 | mounted() { 163 | expect($sandbox.html()).toBe('
itemA
'); 164 | this.item = new Backbone.Model({ 165 | name: "allNew", 166 | value: 1 167 | }); 168 | }, 169 | updated() { 170 | expect($sandbox.html()).toBe( 171 | '
allNew
' 172 | ); 173 | done(); 174 | } 175 | }); 176 | }); 177 | }); 178 | 179 | describe("reactions for computed values derived from model logic", () => { 180 | let model, computed; 181 | 182 | beforeEach(() => { 183 | template = 184 | '
{{ itemName }}
'; 185 | model = new Backbone.Model({ name: "itemA", value: undefined }); 186 | computed = { 187 | computedHasValue() { 188 | return this.item.has("value"); 189 | }, 190 | itemName() { 191 | return this.item.get("name"); 192 | } 193 | }; 194 | }); 195 | 196 | it("accepts a model with defaults and reacts to changes", done => { 197 | model = new (Backbone.Model.extend({ 198 | defaults: { 199 | name: undefined, 200 | value: undefined 201 | } 202 | }))(); 203 | 204 | new Vue({ 205 | el, 206 | template, 207 | bb: () => ({ item: model }), 208 | computed, 209 | mounted() { 210 | expect($sandbox.html()).toBe('
'); 211 | model.set({ name: "itemA", value: 1 }); 212 | }, 213 | updated() { 214 | expect($sandbox.html()).toBe( 215 | '
itemA
' 216 | ); 217 | done(); 218 | } 219 | }); 220 | }); 221 | 222 | it("reacts to model replacement", done => { 223 | new Vue({ 224 | el, 225 | template, 226 | bb: () => ({ item: model }), 227 | computed, 228 | mounted() { 229 | expect($sandbox.html()).toBe('
itemA
'); 230 | this.item = new Backbone.Model({ 231 | name: "allNew", 232 | value: 1 233 | }); 234 | }, 235 | updated() { 236 | expect($sandbox.html()).toBe( 237 | '
allNew
' 238 | ); 239 | done(); 240 | } 241 | }); 242 | }); 243 | 244 | it("does not react to reset, when computed does not use model", function( 245 | done 246 | ) { 247 | new Vue({ 248 | el, 249 | template, 250 | bb: () => ({ item: model }), 251 | computed: { 252 | computedHasValue() { 253 | return true; 254 | }, 255 | itemName() { 256 | return "itemA"; 257 | } 258 | }, 259 | mounted() { 260 | expect($sandbox.html()).toBe( 261 | '
itemA
' 262 | ); 263 | this.item = new Backbone.Model({ 264 | name: "allNew", 265 | value: 1 266 | }); 267 | setTimeout(done); // defer 268 | }, 269 | updated() { 270 | fail("Vue should not have updated."); 271 | } 272 | }); 273 | }); 274 | }); 275 | 276 | describe("components and properties", () => { 277 | let model, component; 278 | 279 | beforeEach(() => { 280 | component = callback => ({ 281 | props: ["model"], 282 | bb: () => ({ model: { prop: true } }), 283 | template: 284 | "
{{ model.name }}
", 285 | updated: callback 286 | }); 287 | 288 | template = '

'; 289 | model = new Backbone.Model({ name: "itemA", value: undefined }); 290 | }); 291 | 292 | it("reacts to changes", done => { 293 | Vue.component( 294 | "item", 295 | component(() => { 296 | expect($sandbox.html()).toBe( 297 | '

itemA

' 298 | ); 299 | done(); 300 | }) 301 | ); 302 | 303 | new Vue({ 304 | el, 305 | template, 306 | bb: () => ({ item: model }), 307 | mounted() { 308 | expect($sandbox.html()).toBe( 309 | '

itemA

' 310 | ); 311 | model.set({ name: "itemA", value: 1 }); 312 | }, 313 | updated() { 314 | fail("Parent vue shouldnt update"); 315 | } 316 | }); 317 | }); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /src/vue-backbone.js: -------------------------------------------------------------------------------- 1 | import modelProxy from "./model-proxy.js"; 2 | import collectionProxy from "./collection-proxy.js"; 3 | 4 | /** 5 | * Default values for the possible options passed in Vue.use 6 | */ 7 | var opts = { 8 | proxies: true, 9 | conflictPrefix: "$", 10 | addComputed: true, 11 | dataPrefix: "_", 12 | simpleCollectionProxy: false 13 | }; 14 | 15 | /** 16 | * Created a VueBackbone Proxy object which can be 17 | * accessed from the original Backbone Object 18 | */ 19 | function vueBackboneProxy(bb) { 20 | if (opts.proxies && !bb._vuebackbone_proxy) { 21 | if (bb.models) { 22 | bb.each(vueBackboneProxy); 23 | bb._vuebackbone_proxy = collectionProxy( 24 | bb, 25 | opts.simpleCollectionProxy 26 | ); 27 | } else { 28 | bb._vuebackbone_proxy = modelProxy(bb, opts.conflictPrefix); 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * Functions to retrieve the underlying POJO 35 | * beneath the Backbone objects 36 | */ 37 | 38 | function rawSrcModel(model) { 39 | return model.attributes; 40 | } 41 | 42 | function rawSrcCollection(collection) { 43 | return collection.map(rawSrcModel); 44 | } 45 | 46 | function rawSrc(bb) { 47 | return bb.models ? rawSrcCollection(bb) : rawSrcModel(bb); 48 | } 49 | 50 | /** 51 | * When Proxies are enabled, the computed value is the most 52 | * practical way to access the proxy (functionality and data together). 53 | * However without proxies, the raw data should be accessible 54 | * via the `bb` options key directly, and the instance (functionality) 55 | * will be accessible via the vm.$bb[key] property. 56 | */ 57 | function getDataKey(key) { 58 | return opts.addComputed && opts.proxies ? opts.dataPrefix + key : key; 59 | } 60 | 61 | /** 62 | * Setup handlers for Backbone events, so that Vue keeps sync. 63 | * Also ensure Models are mapped. 64 | */ 65 | 66 | function bindCollectionToVue(vm, key, ctx, bb) { 67 | // Handle mapping of models for Vue proxy 68 | if (opts.proxies) { 69 | bb.on("add", vueBackboneProxy); // map new models 70 | ctx.onreset = () => bb.each(vueBackboneProxy); 71 | bb.on("reset", ctx.onreset); // map complete reset 72 | } 73 | 74 | // Changes to collection array will require a full reset (for reactivity) 75 | ctx.onchange = () => { 76 | vm.$data[getDataKey(key)] = rawSrcCollection(bb); 77 | // Proxy array isn't by reference, so it needs to be updated 78 | // (this is less costly than recreating it) 79 | if (opts.proxies) { 80 | var proxy = bb._vuebackbone_proxy; 81 | proxy.length = 0; // truncate first 82 | bb.forEach( 83 | (entry, index) => (proxy[index] = entry._vuebackbone_proxy) 84 | ); 85 | } 86 | }; 87 | bb.on("reset sort remove add", ctx.onchange); 88 | } 89 | 90 | /** 91 | * As VueBackbone can't support reactivity on new attributes added to a Backbone 92 | * Model, there's a safety with warning for it. 93 | */ 94 | function bindModelToVue(ctx, bb) { 95 | ctx.onchange = () => { 96 | // Test for new attribute 97 | if (bb.keys().length > Object.keys(bb._previousAttributes).length) { 98 | // Not an error, as it may be the case this attribute is not needed for Vue at all 99 | console.warn( 100 | "VueBackbone: Adding new Model attributes after binding is not supported, provide defaults for all properties" 101 | ); 102 | } 103 | }; 104 | 105 | bb.on("change", ctx.onchange); 106 | } 107 | 108 | function bindBBToVue(vm, key) { 109 | var ctx = vm._vuebackbone[key], 110 | bb = ctx.bb; 111 | 112 | bb.models ? bindCollectionToVue(vm, key, ctx, bb) : bindModelToVue(ctx, bb); 113 | } 114 | 115 | /** 116 | * Cleanup if the Backbone link is changed, or if the Vue is destroyed 117 | */ 118 | function unbindBBFromVue(vm, key) { 119 | var ctx = vm._vuebackbone[key]; 120 | 121 | if (ctx) { 122 | ctx.bb.off(null, ctx.onchange); 123 | ctx.onreset && ctx.bb.off(null, ctx.onreset); 124 | 125 | // The VueBackbone Proxy could be deleted at this 126 | // point, and the handler to proxy new models, but 127 | // this would cause problems if multiple 128 | // Vue objects used the same Backbone model/collection 129 | 130 | //ctx.bb.off(null, vueBackboneProxy); 131 | //delete ctx.bb._vuebackbone_proxy; 132 | } 133 | } 134 | 135 | /** 136 | * Update Vue data object, at this point it will already be a function (not a hash) 137 | * This will make the underlying source of the collection/model reactive. 138 | */ 139 | function extendData(vm, key) { 140 | var origDataFn = vm.$options.data, 141 | ctx = vm._vuebackbone[key], 142 | value = rawSrc(ctx.bb), 143 | dataKey = getDataKey(key); 144 | 145 | vm.$options.data = function() { 146 | let data = {}, 147 | origData = origDataFn ? origDataFn.apply(this, arguments) : {}; 148 | 149 | if (origData.hasOwnProperty(key)) { 150 | throw `VueBackbone: Property '${key}' mustn't exist within the Vue data already`; 151 | } 152 | if (origData.hasOwnProperty(dataKey)) { 153 | throw `VueBackbone: Property '${dataKey}' mustn't exist within the Vue data already`; 154 | } 155 | // shallow copy (just in case) 156 | Object.keys(origData).forEach(attr => (data[attr] = origData[attr])); 157 | data[dataKey] = value; 158 | return data; 159 | }; 160 | } 161 | 162 | /** 163 | * In the case proxies are disabled or computed accessor, 164 | * the Backbone instance is added to vm.$bb[key] 165 | * 166 | * Instance access will trigger, this._key (reactive) access, 167 | * which means any computed values recompute. 168 | * In the case of Collections, the reason this is needed is that calculations in the 169 | * collection can work off the internal models arrays, which isn't the same as the rawSrc one 170 | * For Models, this access is important in the case the full model object is replaced, 171 | * it will ensure the computed value recomputes. 172 | */ 173 | function extendVm(vm, key) { 174 | var ctx = vm._vuebackbone[key], 175 | dataKey = getDataKey(key); 176 | 177 | vm.$bb = vm.$bb || {}; 178 | Object.defineProperty(vm.$bb, key, { 179 | get() { 180 | let access = vm.$data[dataKey]; // eslint-disable-line no-unused-vars 181 | return ctx.bb; 182 | }, 183 | set(bb) { 184 | unbindBBFromVue(vm, key); 185 | ctx.bb = bb; 186 | vm.$data[dataKey] = rawSrc(bb); 187 | bindBBToVue(vm, key); 188 | } 189 | }); 190 | } 191 | 192 | /** 193 | * Update Vue computed functions, this will provide a handy accessor (key) 194 | * for mapped models of a collection, or the mapped model directly. 195 | * 196 | * Computed (this.key) access will trigger, this._key (reactive) access, 197 | * which means any computed values recompute. 198 | * In the case of Collections, the reason this is needed is that calculations in the 199 | * collection can work off the internal models arrays, which isn't the same as the rawSrc one 200 | * For Models, this access is important in the case the full model object is replaced, 201 | * it will ensure the computed value recomputes. 202 | */ 203 | function extendComputed(vm, key) { 204 | var ctx = vm._vuebackbone[key], 205 | dataKey = getDataKey(key), 206 | o = vm.$options; 207 | 208 | o.computed = o.computed || {}; 209 | 210 | // In the case of conflict, don't add it 211 | if (!o.computed[key]) { 212 | o.computed[key] = { 213 | get() { 214 | let access = vm.$data[dataKey]; // eslint-disable-line no-unused-vars 215 | return ctx.bb._vuebackbone_proxy; 216 | }, 217 | set(bb) { 218 | unbindBBFromVue(vm, key); 219 | vueBackboneProxy(bb); 220 | ctx.bb = bb; 221 | vm.$data[dataKey] = rawSrc(bb); 222 | bindBBToVue(vm, key); 223 | } 224 | }; 225 | } else { 226 | console.warn( 227 | `VueBackbone: Generated computed function '${key}' already exists within the Vue computed functions` 228 | ); 229 | } 230 | } 231 | 232 | /** 233 | * Setup Vue and BB instance during Vue creation. 234 | * At this point the validation/normalization has 235 | * occurred. 236 | */ 237 | function initBBAndVue(vm, key, bb, prop) { 238 | vm._vuebackbone[key] = { bb: bb }; 239 | 240 | vueBackboneProxy(bb); 241 | if (!prop) { 242 | extendData(vm, key); 243 | if (opts.addComputed && opts.proxies) { 244 | extendComputed(vm, key); 245 | } else { 246 | extendVm(vm, key); 247 | } 248 | } 249 | bindBBToVue(vm, key); 250 | } 251 | 252 | /** 253 | * Vue Mixin with Global Handlers 254 | */ 255 | let vueBackboneMixin = { 256 | beforeCreate() { 257 | var vm = this, 258 | bbopts = vm.$options.bb; 259 | if (bbopts) { 260 | if (typeof bbopts !== "function") { 261 | throw `VueBackbone: 'bb' initialization option must be a function`; 262 | } 263 | bbopts = bbopts(); // remember, it's a function 264 | vm._vuebackbone = {}; 265 | 266 | Object.keys(bbopts).forEach(key => { 267 | var bb = bbopts[key], 268 | prop = false; 269 | 270 | // Detect Property 271 | if (bb.prop === true) { 272 | if (!vm.$options.propsData || !vm.$options.propsData[key]) { 273 | throw `VueBackbone: Missing Backbone object in Vue prop '${key}'`; 274 | } 275 | bb = vm.$options.propsData[key]; 276 | prop = true; 277 | } 278 | 279 | // If Proxy, retrieve original instance 280 | bb = bb._vuebackbone_original || bb; 281 | 282 | // Detect Model or Collection 283 | if (bb.on && (bb.attributes || bb.models)) { 284 | initBBAndVue(vm, key, bb, prop); 285 | } else { 286 | throw `VueBackbone: Unrecognized Backbone object in Vue instantiation (${key}), must be a Collection or Model`; 287 | } 288 | }); 289 | } 290 | }, 291 | destroyed: function() { 292 | let vm = this, 293 | ctx = vm._vuebackbone; 294 | if (ctx) { 295 | Object.keys(ctx).forEach(key => unbindBBFromVue(vm, key)); 296 | } 297 | } 298 | }; 299 | 300 | /** 301 | * Maps an individual Backbone model, or an array of them, or an falsy value. 302 | * @returns either a hash of raw attributes, or the vue model proxy 303 | */ 304 | export function mapBBModels(func) { 305 | if (opts.proxies) { 306 | return function() { 307 | let models = func.apply(this, arguments); 308 | return ( 309 | (models && 310 | (models._vuebackbone_proxy || 311 | models.map(m => m._vuebackbone_proxy))) || 312 | models 313 | ); 314 | }; 315 | } else { 316 | return function() { 317 | let models = func.apply(this, arguments); 318 | return ( 319 | (models && 320 | (models.attributes || models.map(m => m.attributes))) || 321 | models 322 | ); 323 | }; 324 | } 325 | } 326 | 327 | export function install(Vue, options) { 328 | for (let key in options) { 329 | if (options.hasOwnProperty(key)) { 330 | opts[key] = options[key]; 331 | } 332 | } 333 | Vue.mixin(vueBackboneMixin); 334 | } 335 | 336 | export function original(bb) { 337 | return bb._vuebackbone_original || bb; 338 | } 339 | 340 | export default { 341 | install: install, 342 | mapBBModels: mapBBModels, 343 | original: original 344 | }; 345 | -------------------------------------------------------------------------------- /test/collection-spec.js: -------------------------------------------------------------------------------- 1 | import Backbone from 'backbone' 2 | import Vue from 'vue/dist/vue' 3 | import VueBackbone, { mapBBModels } from '../src/vue-backbone.js' 4 | import $ from 'jquery' 5 | 6 | Vue.use(VueBackbone); 7 | 8 | describe('Collection', () => { 9 | 10 | let $sandbox, el, template; 11 | 12 | beforeEach(() => { 13 | $sandbox = $('
'); 14 | el = $sandbox.children()[0]; 15 | }); 16 | 17 | it('should provide access to array of Model Proxies via Vue computed hash', (done) => { 18 | 19 | let collection = new Backbone.Collection([{name: 'itemA'}]); 20 | 21 | new Vue({ 22 | el, 23 | bb: () => ({list: collection}), 24 | template: '
{{ JSON.stringify(list[0].toJSON()) }}|{{ list[0].name }}
', 25 | mounted() { 26 | expect($sandbox.html()).toBe('
{"name":"itemA"}|itemA
'); 27 | done(); 28 | } 29 | }); 30 | }); 31 | 32 | describe('reactions for data access directly from template', () => { 33 | 34 | beforeEach(() => { 35 | template = '
{{ list.length ? list[list.length - 1].name : "empty!" }}
'; 36 | }); 37 | 38 | it('accepts empty collection, and reacts to additional row', (done) => { 39 | 40 | let collection = new Backbone.Collection(); 41 | 42 | new Vue({ 43 | el, 44 | template, 45 | bb: () => ({list: collection}), 46 | mounted() { 47 | expect($sandbox.html()).toBe('
empty!
'); 48 | collection.add({name: 'itemA'}); 49 | }, 50 | updated() { 51 | expect($sandbox.html()).toBe('
itemA
'); 52 | done(); 53 | } 54 | }); 55 | }); 56 | 57 | it('accepts a collection with row, and reacts to removed row', (done) => { 58 | 59 | let collection = new Backbone.Collection([{name: 'itemA'}]); 60 | 61 | new Vue({ 62 | el, 63 | data: {a: 1}, 64 | template, 65 | bb: () => ({list: collection}), 66 | mounted() { 67 | expect($sandbox.html()).toBe('
itemA
'); 68 | collection.remove(collection.first()); 69 | }, 70 | updated() { 71 | expect($sandbox.html()).toBe('
empty!
'); 72 | done(); 73 | } 74 | }); 75 | }); 76 | 77 | it('reacts to collection replacement', (done) => { 78 | 79 | let collection = new Backbone.Collection([{name: 'itemA'}]); 80 | 81 | new Vue({ 82 | el, 83 | data: {a: 1}, 84 | template, 85 | bb: () => ({list: collection}), 86 | mounted() { 87 | expect($sandbox.html()).toBe('
itemA
'); 88 | this.list = new Backbone.Collection([{name: 'allNew'}]); 89 | }, 90 | updated() { 91 | expect($sandbox.html()).toBe('
allNew
'); 92 | done(); 93 | } 94 | }); 95 | }); 96 | 97 | }); 98 | 99 | 100 | describe('reactions for computed values derived from collection logic', () => { 101 | 102 | let collection, computed; 103 | 104 | beforeEach(() => { 105 | // Also testing the auto-generated computed function $$list 106 | template = '
{{ firstName }}, {{ list.length }}
'; 107 | collection = new Backbone.Collection([{name: 'itemA'}]); 108 | computed = { 109 | computedHasItems() { 110 | return !this.list.isEmpty(); 111 | }, 112 | firstName() { 113 | return this.computedHasItems ? this.list.first().get('name') : 'empty!'; 114 | } 115 | } 116 | }); 117 | 118 | it('reacts to removal', function(done) { 119 | new Vue({ 120 | el, 121 | template, 122 | bb: () => ({list: collection}), 123 | computed, 124 | mounted() { 125 | expect($sandbox.html()).toBe('
itemA, 1
'); 126 | collection.remove(collection.first()); 127 | }, 128 | updated() { 129 | expect($sandbox.html()).toBe('
empty!, 0
'); 130 | done(); 131 | } 132 | }); 133 | }); 134 | 135 | it('reacts to reset', function(done) { 136 | new Vue({ 137 | el, 138 | template, 139 | bb: () => ({list: collection}), 140 | computed, 141 | mounted() { 142 | expect($sandbox.html()).toBe('
itemA, 1
'); 143 | collection.reset([{name: 'itemB'}]); 144 | }, 145 | updated() { 146 | expect($sandbox.html()).toBe('
itemB, 1
'); 147 | done(); 148 | } 149 | }); 150 | }); 151 | 152 | it('reacts to sort', function(done) { 153 | collection.reset([{name: 'itemB'}, {name: 'itemA'}]); 154 | collection.comparator = 'name'; 155 | 156 | new Vue({ 157 | el, 158 | template, 159 | bb: () => ({list: collection}), 160 | computed, 161 | mounted() { 162 | expect($sandbox.html()).toBe('
itemB, 2
'); 163 | collection.sort(); 164 | }, 165 | updated() { 166 | expect($sandbox.html()).toBe('
itemA, 2
'); 167 | done(); 168 | } 169 | }); 170 | }); 171 | 172 | it('reacts to replace', function(done) { 173 | new Vue({ 174 | el, 175 | template, 176 | bb: () => ({list: collection}), 177 | computed, 178 | mounted() { 179 | expect($sandbox.html()).toBe('
itemA, 1
'); 180 | this.list = new Backbone.Collection([{name: 'allNew'}]); 181 | }, 182 | updated() { 183 | expect($sandbox.html()).toBe('
allNew, 1
'); 184 | done(); 185 | } 186 | }); 187 | }); 188 | 189 | it('does not react to reset, when computed does not use collection', function(done) { 190 | 191 | spyOn(console, 'warn'); 192 | 193 | new Vue({ 194 | el, 195 | template, 196 | bb: () => ({list: collection}), 197 | computed: { 198 | computedHasItems() { return true; }, 199 | firstName() { return 'itemA'; }, 200 | list() { return [{}]; } 201 | }, 202 | mounted() { 203 | 204 | // Computed value exists prior to VueBackbone integration, so we assert 205 | // the warning too. 206 | expect(console.warn).toHaveBeenCalledWith('VueBackbone: Generated computed function \'list\' already exists within the Vue computed functions'); 207 | 208 | expect($sandbox.html()).toBe('
itemA, 1
'); 209 | collection.reset([{name: 'itemB'}]); 210 | setTimeout(done); // defer 211 | }, 212 | updated() { 213 | fail('Vue should not have updated.'); 214 | } 215 | }); 216 | }); 217 | 218 | }); 219 | 220 | it('mapped responses in computed values with auto mapped', (done) => { 221 | 222 | let collection = new Backbone.Collection([{name: 'itemA'}]); 223 | 224 | new Vue({ 225 | el, 226 | template: '
{{ firstItem && firstItem.has(\'name\') && firstItem.name }}, {{ validItems[0] && validItems[0].isEmpty() }}
', 227 | bb: () => ({list: collection}), 228 | computed: { 229 | firstItem() { 230 | return this.list.first(); // proxy automatically maps the models for this 231 | }, 232 | validItems() { 233 | return this.list.reject(model => !model.has('name')); // proxy automatically maps the models for this 234 | } 235 | }, 236 | mounted() { 237 | expect($sandbox.html()).toBe('
itemA, false
'); 238 | collection.remove(collection.first()); 239 | }, 240 | updated() { 241 | expect($sandbox.html()).toBe('
,
'); 242 | done(); 243 | } 244 | }); 245 | 246 | }); 247 | 248 | it('mapped responses in computed values with explicit mapped', (done) => { 249 | 250 | let collection = new Backbone.Collection([{name: 'itemA'}]); 251 | 252 | new Vue({ 253 | el, 254 | template: '
{{ firstItem && firstItem.has(\'name\') && firstItem.name }}, {{ validItems[0] && validItems[0].isEmpty() }}
', 255 | bb: () => ({list: collection}), 256 | computed: { 257 | firstItem: mapBBModels(function() { 258 | return VueBackbone.original(this.list).first(); // use original object to access function without auto mapping 259 | }), 260 | validItems: mapBBModels(function() { 261 | return VueBackbone.original(this.list).reject(model => !model.has('name')); 262 | }) 263 | }, 264 | mounted() { 265 | expect($sandbox.html()).toBe('
itemA, false
'); 266 | collection.remove(collection.first()); 267 | }, 268 | updated() { 269 | expect($sandbox.html()).toBe('
,
'); 270 | done(); 271 | } 272 | }); 273 | 274 | }); 275 | 276 | describe('proxied collection functions', function() { 277 | 278 | afterAll(() => { 279 | // Hack to reset the options inside VueBackbone after installation 280 | VueBackbone.install( 281 | {mixin: () => {}}, 282 | {simpleCollectionProxy: false} 283 | ); 284 | }); 285 | 286 | it('with auto-mapping of functions returning models', (done) => { 287 | 288 | let collection = new Backbone.Collection([{name: 'itemA'}]); 289 | 290 | new Vue({ 291 | el, 292 | template: '
{{ list[0].itemA }}
', 293 | bb: () => ({list: collection}), 294 | mounted() { 295 | 296 | // Test array prototype functions (they accept proxy models parameters, and return proxy models) 297 | expect(this.list.indexOf(this.list[0])).toBe(0); 298 | expect(this.list.filter(function() { return true })[0]).toBe(this.list[0]); 299 | 300 | // Test Collection auto-mapped functions 301 | expect(this.list.at(0)).toBe(this.list[0]); 302 | expect(this.list.first()).toBe(this.list[0]); 303 | expect(this.list.toArray()[0]).toBe(this.list[0]); 304 | 305 | done(); 306 | } 307 | }); 308 | 309 | }); 310 | 311 | it('with no auto-mapping of functions returning models', (done) => { 312 | 313 | VueBackbone.install( 314 | {mixin: () => {}}, 315 | {simpleCollectionProxy: true} 316 | ); 317 | 318 | let collection = new Backbone.Collection([{name: 'itemA'}]); 319 | 320 | new Vue({ 321 | el, 322 | template: '
{{ list[0].itemA }}
', 323 | bb: () => ({list: collection}), 324 | mounted() { 325 | 326 | // Test Collection functions (unaltered) 327 | const originalModel = VueBackbone.original(this.list[0]); 328 | 329 | expect(this.list.indexOf(this.list[0])).toBe(-1); 330 | expect(this.list.filter(function() { return true })[0]).toBe(originalModel); 331 | 332 | expect(this.list.at(0)).toBe(originalModel); 333 | expect(this.list.first()).toBe(originalModel); 334 | expect(this.list.toArray()[0]).toBe(originalModel); 335 | 336 | done(); 337 | } 338 | }); 339 | 340 | }); 341 | 342 | }); 343 | 344 | 345 | 346 | 347 | }); -------------------------------------------------------------------------------- /dist/vue-backbone.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Vue-Backbone v0.1.3 3 | * https://github.com/mikeapr4/vue-backbone 4 | * @license MIT 5 | */ 6 | (function webpackUniversalModuleDefinition(root, factory) { 7 | if(typeof exports === 'object' && typeof module === 'object') 8 | module.exports = factory(); 9 | else if(typeof define === 'function' && define.amd) 10 | define([], factory); 11 | else if(typeof exports === 'object') 12 | exports["VueBackbone"] = factory(); 13 | else 14 | root["VueBackbone"] = factory(); 15 | })(this, function() { 16 | return /******/ (function(modules) { // webpackBootstrap 17 | /******/ // The module cache 18 | /******/ var installedModules = {}; 19 | /******/ 20 | /******/ // The require function 21 | /******/ function __webpack_require__(moduleId) { 22 | /******/ 23 | /******/ // Check if module is in cache 24 | /******/ if(installedModules[moduleId]) { 25 | /******/ return installedModules[moduleId].exports; 26 | /******/ } 27 | /******/ // Create a new module (and put it into the cache) 28 | /******/ var module = installedModules[moduleId] = { 29 | /******/ i: moduleId, 30 | /******/ l: false, 31 | /******/ exports: {} 32 | /******/ }; 33 | /******/ 34 | /******/ // Execute the module function 35 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 36 | /******/ 37 | /******/ // Flag the module as loaded 38 | /******/ module.l = true; 39 | /******/ 40 | /******/ // Return the exports of the module 41 | /******/ return module.exports; 42 | /******/ } 43 | /******/ 44 | /******/ 45 | /******/ // expose the modules object (__webpack_modules__) 46 | /******/ __webpack_require__.m = modules; 47 | /******/ 48 | /******/ // expose the module cache 49 | /******/ __webpack_require__.c = installedModules; 50 | /******/ 51 | /******/ // identity function for calling harmony imports with the correct context 52 | /******/ __webpack_require__.i = function(value) { return value; }; 53 | /******/ 54 | /******/ // define getter function for harmony exports 55 | /******/ __webpack_require__.d = function(exports, name, getter) { 56 | /******/ if(!__webpack_require__.o(exports, name)) { 57 | /******/ Object.defineProperty(exports, name, { 58 | /******/ configurable: false, 59 | /******/ enumerable: true, 60 | /******/ get: getter 61 | /******/ }); 62 | /******/ } 63 | /******/ }; 64 | /******/ 65 | /******/ // getDefaultExport function for compatibility with non-harmony modules 66 | /******/ __webpack_require__.n = function(module) { 67 | /******/ var getter = module && module.__esModule ? 68 | /******/ function getDefault() { return module['default']; } : 69 | /******/ function getModuleExports() { return module; }; 70 | /******/ __webpack_require__.d(getter, 'a', getter); 71 | /******/ return getter; 72 | /******/ }; 73 | /******/ 74 | /******/ // Object.prototype.hasOwnProperty.call 75 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 76 | /******/ 77 | /******/ // __webpack_public_path__ 78 | /******/ __webpack_require__.p = ""; 79 | /******/ 80 | /******/ // Load entry module and return exports 81 | /******/ return __webpack_require__(__webpack_require__.s = 2); 82 | /******/ }) 83 | /************************************************************************/ 84 | /******/ ([ 85 | /* 0 */ 86 | /***/ (function(module, exports, __webpack_require__) { 87 | 88 | "use strict"; 89 | 90 | 91 | Object.defineProperty(exports, "__esModule", { 92 | value: true 93 | }); 94 | 95 | exports.default = function (collection, simple) { 96 | var proxy = collection.map(function (model) { 97 | return model._vuebackbone_proxy; 98 | }); 99 | 100 | // Attach bound version of all the collection functions to the proxy 101 | // these functions are readonly on the proxy and any future changes 102 | // to the collection functions won't be reflected in the Vue proxy 103 | for (var key in collection) { 104 | if (typeof collection[key] === "function" && key !== "constructor" && (simple || arrayPriority.indexOf(key) === -1)) { 105 | var bndFunc = collection[key].bind(collection); 106 | if (!simple) { 107 | bndFunc = checkForReturnsModels(bndFunc, key); 108 | } 109 | Object.defineProperty(proxy, key, { value: bndFunc }); 110 | } 111 | } 112 | 113 | // Attach link back to original model 114 | Object.defineProperty(proxy, "_vuebackbone_original", { 115 | value: collection 116 | }); 117 | 118 | return proxy; 119 | }; 120 | 121 | /** 122 | * Create a proxy of the Backbone Collection which maps direct access of underlying 123 | * array of Model proxies to the functional interface that Backbone normally provides. 124 | * This allows for in-template looping/access to Backbone model proxies. 125 | * 126 | * In the case of ambiguity, say a function exists on both the Array.prototype and in 127 | * Backbone. In general Backbone functionality is favoured, but there is a list of Array 128 | * functions which will be kept. 129 | * 130 | * In addition, common Backbone collection functions which return an array of models, or a single 131 | * one, have the returned Model(s) mapped to their proxies. Just a convenience. 132 | * 133 | * To avoid interference, it's stored under the Model property `_vuebackbone_proxy`. This proxy 134 | * is only really intended to be used by Vue templates. 135 | * 136 | * Generate documentation using: https://jsfiddle.net/fLcn09eb/3/ 137 | */ 138 | 139 | var arrayPriority = ["slice", "forEach", "map", "reduce", "reduceRight", "find", "filter", "every", "some", "indexOf", "lastIndexOf", "findIndex"], 140 | returnsModels = ["pop", "shift", "remove", "get", "at", "where", "findWhere", "reject", "sortBy", "shuffle", "toArray", "detect", "select", "first", "head", "take", "rest", "tail", "drop", "initial", "last", "without"]; 141 | 142 | function checkForReturnsModels(func, key) { 143 | if (returnsModels.indexOf(key) > -1) { 144 | return function () { 145 | var models = func.apply(this, arguments); 146 | return models && (models._vuebackbone_proxy || models.map(function (m) { 147 | return m._vuebackbone_proxy; 148 | })) || models; 149 | }; 150 | } 151 | return func; 152 | } 153 | 154 | /***/ }), 155 | /* 1 */ 156 | /***/ (function(module, exports, __webpack_require__) { 157 | 158 | "use strict"; 159 | 160 | 161 | Object.defineProperty(exports, "__esModule", { 162 | value: true 163 | }); 164 | 165 | exports.default = function (model, conflictPrefix) { 166 | var proxy = {}; 167 | 168 | // Attach bound version of all the model functions to the proxy 169 | // these functions are readonly on the proxy and any future changes 170 | // to the model functions won't be reflected in the Vue proxy 171 | for (var key in model) { 172 | if (typeof model[key] === "function" && key !== "constructor") { 173 | var bndFunc = model[key].bind(model); 174 | Object.defineProperty(proxy, key, { value: bndFunc }); 175 | } 176 | } 177 | 178 | // Attach getter/setters for the model attributes. 179 | Object.keys(model.attributes).forEach(function (attr) { 180 | proxyModelAttribute(proxy, model, attr, conflictPrefix); 181 | }); 182 | if (!proxy.id) { 183 | // sometimes ID is a field in the model (in which case it'll be proxied already) 184 | Object.defineProperty(proxy, "id", { 185 | get: function get() { 186 | return model.id; 187 | } 188 | }); 189 | } 190 | 191 | // Attach link back to original model 192 | Object.defineProperty(proxy, "_vuebackbone_original", { value: model }); 193 | 194 | return proxy; 195 | }; 196 | 197 | /** 198 | * Create a proxy of the Backbone Model which maps direct access of attributes 199 | * to the get/set interface that Backbone normally provides. This allows for 200 | * in-template binding to Backbone model attributes easily. 201 | * 202 | * In the case of ambiguity, say an attribute called "completed" and a method 203 | * called "completed". The method takes priority (so as not to break existing functionality), 204 | * however as a backup the attribute can be accessed with a prefix (conflictPrefix option), 205 | * e.g. model.$completed 206 | * 207 | * To avoid interference, it's stored under the Model property `_vuebackbone_proxy`. This proxy 208 | * is only really intended to be used by Vue templates. 209 | */ 210 | 211 | /** 212 | * Attach proxy getter/setter for a model attribute 213 | */ 214 | function proxyModelAttribute(proxy, model, attr, conflictPrefix) { 215 | var getter = model.get.bind(model, attr); 216 | var setter = model.set.bind(model, attr); 217 | 218 | // If there's a conflict with a function from the model, add the attribute with the prefix 219 | var safeAttr = proxy[attr] ? conflictPrefix + attr : attr; 220 | 221 | Object.defineProperty(proxy, safeAttr, { 222 | enumerable: true, 223 | get: getter, 224 | set: setter 225 | }); 226 | } 227 | 228 | /***/ }), 229 | /* 2 */ 230 | /***/ (function(module, exports, __webpack_require__) { 231 | 232 | "use strict"; 233 | 234 | 235 | Object.defineProperty(exports, "__esModule", { 236 | value: true 237 | }); 238 | exports.mapBBModels = mapBBModels; 239 | exports.install = install; 240 | exports.original = original; 241 | 242 | var _modelProxy = __webpack_require__(1); 243 | 244 | var _modelProxy2 = _interopRequireDefault(_modelProxy); 245 | 246 | var _collectionProxy = __webpack_require__(0); 247 | 248 | var _collectionProxy2 = _interopRequireDefault(_collectionProxy); 249 | 250 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 251 | 252 | /** 253 | * Default values for the possible options passed in Vue.use 254 | */ 255 | var opts = { 256 | proxies: true, 257 | conflictPrefix: "$", 258 | addComputed: true, 259 | dataPrefix: "_", 260 | simpleCollectionProxy: false 261 | }; 262 | 263 | /** 264 | * Created a VueBackbone Proxy object which can be 265 | * accessed from the original Backbone Object 266 | */ 267 | function vueBackboneProxy(bb) { 268 | if (opts.proxies && !bb._vuebackbone_proxy) { 269 | if (bb.models) { 270 | bb.each(vueBackboneProxy); 271 | bb._vuebackbone_proxy = (0, _collectionProxy2.default)(bb, opts.simpleCollectionProxy); 272 | } else { 273 | bb._vuebackbone_proxy = (0, _modelProxy2.default)(bb, opts.conflictPrefix); 274 | } 275 | } 276 | } 277 | 278 | /** 279 | * Functions to retrieve the underlying POJO 280 | * beneath the Backbone objects 281 | */ 282 | 283 | function rawSrcModel(model) { 284 | return model.attributes; 285 | } 286 | 287 | function rawSrcCollection(collection) { 288 | return collection.map(rawSrcModel); 289 | } 290 | 291 | function rawSrc(bb) { 292 | return bb.models ? rawSrcCollection(bb) : rawSrcModel(bb); 293 | } 294 | 295 | /** 296 | * When Proxies are enabled, the computed value is the most 297 | * practical way to access the proxy (functionality and data together). 298 | * However without proxies, the raw data should be accessible 299 | * via the `bb` options key directly, and the instance (functionality) 300 | * will be accessible via the vm.$bb[key] property. 301 | */ 302 | function getDataKey(key) { 303 | return opts.addComputed && opts.proxies ? opts.dataPrefix + key : key; 304 | } 305 | 306 | /** 307 | * Setup handlers for Backbone events, so that Vue keeps sync. 308 | * Also ensure Models are mapped. 309 | */ 310 | 311 | function bindCollectionToVue(vm, key, ctx, bb) { 312 | // Handle mapping of models for Vue proxy 313 | if (opts.proxies) { 314 | bb.on("add", vueBackboneProxy); // map new models 315 | ctx.onreset = function () { 316 | return bb.each(vueBackboneProxy); 317 | }; 318 | bb.on("reset", ctx.onreset); // map complete reset 319 | } 320 | 321 | // Changes to collection array will require a full reset (for reactivity) 322 | ctx.onchange = function () { 323 | vm.$data[getDataKey(key)] = rawSrcCollection(bb); 324 | // Proxy array isn't by reference, so it needs to be updated 325 | // (this is less costly than recreating it) 326 | if (opts.proxies) { 327 | var proxy = bb._vuebackbone_proxy; 328 | proxy.length = 0; // truncate first 329 | bb.forEach(function (entry, index) { 330 | return proxy[index] = entry._vuebackbone_proxy; 331 | }); 332 | } 333 | }; 334 | bb.on("reset sort remove add", ctx.onchange); 335 | } 336 | 337 | /** 338 | * As VueBackbone can't support reactivity on new attributes added to a Backbone 339 | * Model, there's a safety with warning for it. 340 | */ 341 | function bindModelToVue(ctx, bb) { 342 | ctx.onchange = function () { 343 | // Test for new attribute 344 | if (bb.keys().length > Object.keys(bb._previousAttributes).length) { 345 | // Not an error, as it may be the case this attribute is not needed for Vue at all 346 | console.warn("VueBackbone: Adding new Model attributes after binding is not supported, provide defaults for all properties"); 347 | } 348 | }; 349 | 350 | bb.on("change", ctx.onchange); 351 | } 352 | 353 | function bindBBToVue(vm, key) { 354 | var ctx = vm._vuebackbone[key], 355 | bb = ctx.bb; 356 | 357 | bb.models ? bindCollectionToVue(vm, key, ctx, bb) : bindModelToVue(ctx, bb); 358 | } 359 | 360 | /** 361 | * Cleanup if the Backbone link is changed, or if the Vue is destroyed 362 | */ 363 | function unbindBBFromVue(vm, key) { 364 | var ctx = vm._vuebackbone[key]; 365 | 366 | if (ctx) { 367 | ctx.bb.off(null, ctx.onchange); 368 | ctx.onreset && ctx.bb.off(null, ctx.onreset); 369 | 370 | // The VueBackbone Proxy could be deleted at this 371 | // point, and the handler to proxy new models, but 372 | // this would cause problems if multiple 373 | // Vue objects used the same Backbone model/collection 374 | 375 | //ctx.bb.off(null, vueBackboneProxy); 376 | //delete ctx.bb._vuebackbone_proxy; 377 | } 378 | } 379 | 380 | /** 381 | * Update Vue data object, at this point it will already be a function (not a hash) 382 | * This will make the underlying source of the collection/model reactive. 383 | */ 384 | function extendData(vm, key) { 385 | var origDataFn = vm.$options.data, 386 | ctx = vm._vuebackbone[key], 387 | value = rawSrc(ctx.bb), 388 | dataKey = getDataKey(key); 389 | 390 | vm.$options.data = function () { 391 | var data = {}, 392 | origData = origDataFn ? origDataFn.apply(this, arguments) : {}; 393 | 394 | if (origData.hasOwnProperty(key)) { 395 | throw "VueBackbone: Property '" + key + "' mustn't exist within the Vue data already"; 396 | } 397 | if (origData.hasOwnProperty(dataKey)) { 398 | throw "VueBackbone: Property '" + dataKey + "' mustn't exist within the Vue data already"; 399 | } 400 | // shallow copy (just in case) 401 | Object.keys(origData).forEach(function (attr) { 402 | return data[attr] = origData[attr]; 403 | }); 404 | data[dataKey] = value; 405 | return data; 406 | }; 407 | } 408 | 409 | /** 410 | * In the case proxies are disabled or computed accessor, 411 | * the Backbone instance is added to vm.$bb[key] 412 | * 413 | * Instance access will trigger, this._key (reactive) access, 414 | * which means any computed values recompute. 415 | * In the case of Collections, the reason this is needed is that calculations in the 416 | * collection can work off the internal models arrays, which isn't the same as the rawSrc one 417 | * For Models, this access is important in the case the full model object is replaced, 418 | * it will ensure the computed value recomputes. 419 | */ 420 | function extendVm(vm, key) { 421 | var ctx = vm._vuebackbone[key], 422 | dataKey = getDataKey(key); 423 | 424 | vm.$bb = vm.$bb || {}; 425 | Object.defineProperty(vm.$bb, key, { 426 | get: function get() { 427 | var access = vm.$data[dataKey]; // eslint-disable-line no-unused-vars 428 | return ctx.bb; 429 | }, 430 | set: function set(bb) { 431 | unbindBBFromVue(vm, key); 432 | ctx.bb = bb; 433 | vm.$data[dataKey] = rawSrc(bb); 434 | bindBBToVue(vm, key); 435 | } 436 | }); 437 | } 438 | 439 | /** 440 | * Update Vue computed functions, this will provide a handy accessor (key) 441 | * for mapped models of a collection, or the mapped model directly. 442 | * 443 | * Computed (this.key) access will trigger, this._key (reactive) access, 444 | * which means any computed values recompute. 445 | * In the case of Collections, the reason this is needed is that calculations in the 446 | * collection can work off the internal models arrays, which isn't the same as the rawSrc one 447 | * For Models, this access is important in the case the full model object is replaced, 448 | * it will ensure the computed value recomputes. 449 | */ 450 | function extendComputed(vm, key) { 451 | var ctx = vm._vuebackbone[key], 452 | dataKey = getDataKey(key), 453 | o = vm.$options; 454 | 455 | o.computed = o.computed || {}; 456 | 457 | // In the case of conflict, don't add it 458 | if (!o.computed[key]) { 459 | o.computed[key] = { 460 | get: function get() { 461 | var access = vm.$data[dataKey]; // eslint-disable-line no-unused-vars 462 | return ctx.bb._vuebackbone_proxy; 463 | }, 464 | set: function set(bb) { 465 | unbindBBFromVue(vm, key); 466 | vueBackboneProxy(bb); 467 | ctx.bb = bb; 468 | vm.$data[dataKey] = rawSrc(bb); 469 | bindBBToVue(vm, key); 470 | } 471 | }; 472 | } else { 473 | console.warn("VueBackbone: Generated computed function '" + key + "' already exists within the Vue computed functions"); 474 | } 475 | } 476 | 477 | /** 478 | * Setup Vue and BB instance during Vue creation. 479 | * At this point the validation/normalization has 480 | * occurred. 481 | */ 482 | function initBBAndVue(vm, key, bb, prop) { 483 | vm._vuebackbone[key] = { bb: bb }; 484 | 485 | vueBackboneProxy(bb); 486 | if (!prop) { 487 | extendData(vm, key); 488 | if (opts.addComputed && opts.proxies) { 489 | extendComputed(vm, key); 490 | } else { 491 | extendVm(vm, key); 492 | } 493 | } 494 | bindBBToVue(vm, key); 495 | } 496 | 497 | /** 498 | * Vue Mixin with Global Handlers 499 | */ 500 | var vueBackboneMixin = { 501 | beforeCreate: function beforeCreate() { 502 | var vm = this, 503 | bbopts = vm.$options.bb; 504 | if (bbopts) { 505 | if (typeof bbopts !== "function") { 506 | throw "VueBackbone: 'bb' initialization option must be a function"; 507 | } 508 | bbopts = bbopts(); // remember, it's a function 509 | vm._vuebackbone = {}; 510 | 511 | Object.keys(bbopts).forEach(function (key) { 512 | var bb = bbopts[key], 513 | prop = false; 514 | 515 | // Detect Property 516 | if (bb.prop === true) { 517 | if (!vm.$options.propsData || !vm.$options.propsData[key]) { 518 | throw "VueBackbone: Missing Backbone object in Vue prop '" + key + "'"; 519 | } 520 | bb = vm.$options.propsData[key]; 521 | prop = true; 522 | } 523 | 524 | // If Proxy, retrieve original instance 525 | bb = bb._vuebackbone_original || bb; 526 | 527 | // Detect Model or Collection 528 | if (bb.on && (bb.attributes || bb.models)) { 529 | initBBAndVue(vm, key, bb, prop); 530 | } else { 531 | throw "VueBackbone: Unrecognized Backbone object in Vue instantiation (" + key + "), must be a Collection or Model"; 532 | } 533 | }); 534 | } 535 | }, 536 | 537 | destroyed: function destroyed() { 538 | var vm = this, 539 | ctx = vm._vuebackbone; 540 | if (ctx) { 541 | Object.keys(ctx).forEach(function (key) { 542 | return unbindBBFromVue(vm, key); 543 | }); 544 | } 545 | } 546 | }; 547 | 548 | /** 549 | * Maps an individual Backbone model, or an array of them, or an falsy value. 550 | * @returns either a hash of raw attributes, or the vue model proxy 551 | */ 552 | function mapBBModels(func) { 553 | if (opts.proxies) { 554 | return function () { 555 | var models = func.apply(this, arguments); 556 | return models && (models._vuebackbone_proxy || models.map(function (m) { 557 | return m._vuebackbone_proxy; 558 | })) || models; 559 | }; 560 | } else { 561 | return function () { 562 | var models = func.apply(this, arguments); 563 | return models && (models.attributes || models.map(function (m) { 564 | return m.attributes; 565 | })) || models; 566 | }; 567 | } 568 | } 569 | 570 | function install(Vue, options) { 571 | for (var key in options) { 572 | if (options.hasOwnProperty(key)) { 573 | opts[key] = options[key]; 574 | } 575 | } 576 | Vue.mixin(vueBackboneMixin); 577 | } 578 | 579 | function original(bb) { 580 | return bb._vuebackbone_original || bb; 581 | } 582 | 583 | exports.default = { 584 | install: install, 585 | mapBBModels: mapBBModels, 586 | original: original 587 | }; 588 | 589 | /***/ }) 590 | /******/ ]); 591 | }); --------------------------------------------------------------------------------