├── .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 | [](https://travis-ci.org/mikeapr4/vue-backbone)
4 | [](https://coveralls.io/github/mikeapr4/vue-backbone)
5 | [](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 | 2-way Sync (vue/issues/316 )
33 | Reactive Backbone (codepen.io/niexin/pen/XmYdVa )
34 | Vue-Backbone without Proxies enabled
35 | Vue-Backbone with Proxies (default)
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 | Action
49 | (A) Avg/Min/Max (ms)
50 | (B) Avg/Min/Max (ms)
51 | (C) Avg/Min/Max (ms)
52 | (D) Avg/Min/Max (ms)
53 |
54 |
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 |
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 |
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 | 
14 |
15 | ## Vue Backbone Architecture
16 |
17 | 
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 | 
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 |
112 |
117 | {{ branch }}
118 |
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 | $('')
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 | });
--------------------------------------------------------------------------------