├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── scripts └── travis │ └── angular-versions.js ├── karma.conf.js ├── bower.json ├── LICENSE.md ├── package.json ├── Gruntfile.js ├── ng-backbone.min.js ├── ng-backbone.map ├── README.md ├── test ├── backbone.spec.js ├── ng-backbone-model.spec.js └── ng-backbone-collection.spec.js └── ng-backbone.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | scripts/ 2 | test/ 3 | 4 | .travis.yml 5 | Gruntfile.js 6 | karma.conf.js 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 6 5 | env: 6 | - ANGULAR_VERSION="1.3" 7 | - ANGULAR_VERSION="1.4" 8 | - ANGULAR_VERSION="1.5" 9 | - ANGULAR_VERSION="^1.6.0-rc" 10 | before_install: 11 | - node scripts/travis/angular-versions 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 (2016/12/4) 2 | ## Optimizations 3 | - **ngBackbone:** Update sync layer to use standard `then` method but fallback if necessary 4 | (0286477a) 5 | 6 | 7 | # v0.1.1 (2015/01/29) 8 | ## Bug Fixes 9 | - **sync:** add $http promise arguments to options 10 | ([4df0c828](https://github.com/adrianlee44/ng-backbone/commit/4df0c82807fe094d5262053c5bd9fa729d308543)) 11 | 12 | # v0.1.0 (2014/09/21) 13 | ## Features 14 | - **ngBackbone:** Initial release 15 | -------------------------------------------------------------------------------- /scripts/travis/angular-versions.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | pkg = require('../../package.json'); 4 | 5 | var angularVersion = process.env['ANGULAR_VERSION']; 6 | 7 | pkg.dependencies['angular'] = angularVersion; 8 | pkg.devDependencies['angular-mocks'] = angularVersion; 9 | 10 | var writeData = JSON.stringify(pkg, null, ' '); 11 | 12 | var pkgPath = path.join(process.cwd(), 'package.json'); 13 | fs.writeFile(pkgPath, writeData); 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '', 4 | frameworks: ['jasmine'], 5 | files: [ 6 | 'node_modules/angular/angular.js', 7 | 'node_modules/underscore/underscore.js', 8 | 'node_modules/backbone/backbone.js', 9 | 'ng-backbone.js', 10 | 'node_modules/angular-mocks/angular-mocks.js', 11 | 'test/*.spec.js' 12 | ], 13 | reporters: ['dots', 'coverage'], 14 | preprocessors: { 15 | 'ng-backbone.js': ['coverage'] 16 | }, 17 | browsers: ['PhantomJS'], 18 | coverageReporter: { 19 | reporters: [ 20 | { type: 'lcovonly', subdir: '.'} 21 | ] 22 | } 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-backbone", 3 | "version": "1.0.0", 4 | "authors": [ 5 | "Adrian Lee " 6 | ], 7 | "description": "Backbone data model and collection for AngularJS", 8 | "main": "ng-backbone.js", 9 | "keywords": [ 10 | "Backbone", 11 | "AngularJS", 12 | "data", 13 | "model", 14 | "collection" 15 | ], 16 | "license": "MIT", 17 | "homepage": "https://github.com/adrianlee44/ng-backbone", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test" 23 | ], 24 | "dependencies": { 25 | "angular": ">1.2.0", 26 | "backbone": ">1.1.2" 27 | }, 28 | "devDependencies": { 29 | "angular-mocks": ">1.2.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Adrian Lee 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-backbone", 3 | "version": "1.0.0", 4 | "description": "Backbone data model and collection for AngularJS", 5 | "main": "ng-backbone.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/grunt test && cat ./coverage/lcov.info | coveralls" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:adrianlee44/ng-backbone.git" 12 | }, 13 | "author": "Adrian Lee", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/adrianlee44/ng-backbone/issues" 17 | }, 18 | "homepage": "https://github.com/adrianlee44/ng-backbone", 19 | "dependencies": { 20 | "angular": ">1.2.0", 21 | "backbone": ">1.1.2" 22 | }, 23 | "devDependencies": { 24 | "angular-mocks": ">1.2.0", 25 | "coveralls": "^2.11.15", 26 | "grunt": "^1.0.1", 27 | "grunt-contrib-jshint": "^1.1.0", 28 | "grunt-contrib-uglify": "^2.0.0", 29 | "grunt-karma": "^2.0.0", 30 | "jasmine-core": "^2.5.2", 31 | "karma": "^1.3.0", 32 | "karma-coverage": "^1.1.1", 33 | "karma-jasmine": "^1.0.2", 34 | "karma-phantomjs-launcher": "^1.0.2", 35 | "load-grunt-tasks": "^3.5.2", 36 | "phantomjs": "^2.1.7", 37 | "time-grunt": "^1.4.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | require('load-grunt-tasks')(grunt); 3 | 4 | require('time-grunt')(grunt); 5 | 6 | grunt.initConfig({ 7 | pkg: grunt.file.readJSON('package.json'), 8 | bower: grunt.file.readJSON('bower.json'), 9 | karma: { 10 | options: { 11 | configFile: 'karma.conf.js' 12 | }, 13 | unit: { 14 | singleRun: false, 15 | autoWatch: true 16 | }, 17 | ci: { 18 | singleRun: true 19 | } 20 | }, 21 | jshint: { 22 | options: { 23 | curly: true, 24 | eqeqeq: true, 25 | quotmark: 'single', 26 | undef: true, 27 | unused: true, 28 | strict: true, 29 | browser: true, 30 | eqnull: true, 31 | globals: { 32 | angular: true, 33 | Backbone: true, 34 | _: true 35 | } 36 | }, 37 | ngBackbone: ['ng-backbone.js'], 38 | test: { 39 | options: { 40 | strict: false, 41 | globals: { 42 | afterEach: true, 43 | beforeEach: true, 44 | describe: true, 45 | expect: true, 46 | inject: true, 47 | it: true, 48 | jasmine: true, 49 | module: true, 50 | angular: true 51 | } 52 | }, 53 | src: ['test/*.spec.js'] 54 | } 55 | }, 56 | uglify: { 57 | ngBackbone: { 58 | options: { 59 | sourceMap: true, 60 | sourceMapName: 'ng-backbone.map' 61 | }, 62 | files: { 63 | 'ng-backbone.min.js': ['ng-backbone.js'] 64 | } 65 | } 66 | } 67 | }); 68 | 69 | grunt.registerTask('test', [ 70 | "jshint", 71 | "karma:ci" 72 | ]); 73 | grunt.registerTask('default', [ 74 | 'karma:ci', 75 | 'uglify' 76 | ]); 77 | }; 78 | -------------------------------------------------------------------------------- /ng-backbone.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b,c){"use strict";angular.module("ngBackbone",[]).factory("Backbone",["$http",function(a){var b,c,d,e=_.isUndefined;return b={create:"POST",update:"PUT",patch:"PATCH",delete:"DELETE",read:"GET"},c=function(a,c,f){e(f)&&(f={});var g=f.method||b[a],h={method:g};f.url||(h.url=_.result(c,"url")),e(f.data)&&c&&("POST"===g||"PUT"===g||"PATCH"===g)&&(h.data=JSON.stringify(f.attrs||c.toJSON(f))),"GET"!==g||e(f.data)||(h.params=f.data);var i=function(a){f.xhr={status:a.status,headers:a.headers,config:a.config},!e(f.success)&&_.isFunction(f.success)&&f.success(a.data)},j=function(a){f.xhr={status:a.status,headers:a.headers,config:a.config},!e(f.error)&&_.isFunction(f.error)&&f.error(a.data)},k=d(_.extend(h,f));return!e(k.success)&&_.isFunction(k.success)?k.success(function(a,b,c,d){return i({data:a,status:b,headers:c,config:d})}).error(function(a,b,c,d){return j({data:a,status:b,headers:c,config:d})}):k.then(i,j),c.trigger("request",c,k,_.extend(h,f)),k},d=function(){return a.apply(a,arguments)},_.extend(Backbone,{sync:c,ajax:d})}]).factory("NgBackboneModel",["$rootScope","Backbone",function(a,b){var c;return c=function(a){var b=this;Object.defineProperty(this.$attributes,a,{enumerable:!0,configurable:!0,get:function(){return b.get(a)},set:function(c){b.set(a,c)}})},b.Model.extend({constructor:function(){return this.$status={deleting:!1,loading:!1,saving:!1,syncing:!1},this.on("request",function(a,b,c){this.$setStatus({deleting:"DELETE"===c.method,loading:"GET"===c.method,saving:"POST"===c.method||"PUT"===c.method,syncing:!0})}),this.on("sync error",this.$resetStatus),b.Model.apply(this,arguments)},set:function(a,c,d){var e=b.Model.prototype.set.apply(this,arguments);return e&&this.$setBinding(a,c,d),e},$resetStatus:function(){return this.$setStatus({deleting:!1,loading:!1,saving:!1,syncing:!1})},$setBinding:function(a,b,d){var e,f,g;if(_.isUndefined(a))return this;_.isObject(a)?(f=a,d=b):(f={})[a]=b,d=d||{},_.isUndefined(this.$attributes)&&(this.$attributes={}),g=d.unset;for(e in f)g&&this.$attributes.hasOwnProperty(e)?delete this.$attributes[e]:g||this.$attributes[e]||c.call(this,e);return this},$setStatus:function(a,b,c){var d,e;if(_.isUndefined(a))return this;_.isObject(a)?(e=a,c=b):(e={})[a]=b,c=c||{};for(d in this.$status)e.hasOwnProperty(d)&&_.isBoolean(e[d])&&(this.$status[d]=e[d])},$removeBinding:function(a,b){return this.$setBinding(a,void 0,_.extend({},b,{unset:!0}))}})}]).factory("NgBackboneCollection",["Backbone","NgBackboneModel",function(a,b){return a.Collection.extend({model:b,constructor:function(){var b=this;this.$status={deleting:!1,loading:!1,saving:!1,syncing:!1},this.on("request",function(a,b,c){this.$setStatus({deleting:"DELETE"===c.method,loading:"GET"===c.method,saving:"POST"===c.method||"PUT"===c.method,syncing:!0})}),this.on("sync error",this.$resetStatus),this.on("destroy",this.$resetStatus),Object.defineProperty(this,"$models",{enumerable:!0,get:function(){return b.models}}),a.Collection.apply(this,arguments)},$setStatus:function(a,b,c){var d,e;if(_.isUndefined(a))return this;_.isObject(a)?(e=a,c=b):(e={})[a]=b,c=c||{};for(d in this.$status)e.hasOwnProperty(d)&&_.isBoolean(e[d])&&(this.$status[d]=e[d])},$resetStatus:function(){return this.$setStatus({deleting:!1,loading:!1,saving:!1,syncing:!1})}})}])}(window,document); 2 | //# sourceMappingURL=ng-backbone.map -------------------------------------------------------------------------------- /ng-backbone.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["ng-backbone.js"],"names":["window","document","undefined","angular","module","factory","$http","methodMap","sync","ajax","isUndefined","_","create","update","patch","delete","read","method","model","options","httpMethod","params","url","result","data","JSON","stringify","attrs","toJSON","successFn","response","xhr","status","headers","config","success","isFunction","errorFn","error","extend","then","trigger","apply","arguments","Backbone","$rootScope","defineProperty","key","self","this","Object","$attributes","enumerable","configurable","get","set","newValue","Model","constructor","$status","deleting","loading","saving","syncing","on","$setStatus","$resetStatus","val","output","prototype","$setBinding","attr","unset","isObject","hasOwnProperty","call","value","isBoolean","$removeBinding","NgBackboneModel","Collection","models"],"mappings":"CAaA,SAAUA,EAAQC,EAAUC,GAC1B,YAEAC,SAAQC,OAAO,iBAMbC,QAAQ,YAAa,QAAS,SAASC,GACrC,GAAIC,GAAWC,EAAMC,EAAMC,EAAcC,EAAED,WAkG3C,OAhGAH,IACEK,OAAQ,OACRC,OAAQ,MACRC,MAAO,QACPC,OAAQ,SACRC,KAAM,OAGRR,EAAO,SAASS,EAAQC,EAAOC,GAEzBT,EAAYS,KACdA,KAGF,IAAIC,GAAaD,EAAQF,QAAUV,EAAUU,GACzCI,GAAUJ,OAAQG,EAEjBD,GAAQG,MACXD,EAAOC,IAAMX,EAAEY,OAAOL,EAAO,QAG3BR,EAAYS,EAAQK,OAASN,IAAyB,SAAfE,GAAwC,QAAfA,GAAuC,UAAfA,KAC1FC,EAAOG,KAAOC,KAAKC,UAAUP,EAAQQ,OAAST,EAAMU,OAAOT,KAI1C,QAAfC,GAAyBV,EAAYS,EAAQK,QAC/CH,EAAOA,OAASF,EAAQK,KAG1B,IAAIK,GAAY,SAASC,GACvBX,EAAQY,KACNC,OAAQF,EAASE,OACjBC,QAASH,EAASG,QAClBC,OAAQJ,EAASI,SAGdxB,EAAYS,EAAQgB,UAAYxB,EAAEyB,WAAWjB,EAAQgB,UACxDhB,EAAQgB,QAAQL,EAASN,OAIzBa,EAAU,SAASP,GACrBX,EAAQY,KACNC,OAAQF,EAASE,OACjBC,QAASH,EAASG,QAClBC,OAAQJ,EAASI,SAGdxB,EAAYS,EAAQmB,QAAU3B,EAAEyB,WAAWjB,EAAQmB,QACtDnB,EAAQmB,MAAMR,EAASN,OAIvBO,EAAMtB,EAAKE,EAAE4B,OAAOlB,EAAQF,GA6BhC,QA1BKT,EAAYqB,EAAII,UAAYxB,EAAEyB,WAAWL,EAAII,SAChDJ,EACGI,QAAQ,SAASX,EAAMQ,EAAQC,EAASC,GACvC,MAAOL,IACLL,KAAMA,EACNQ,OAAQA,EACRC,QAASA,EACTC,OAAQA,MAGXI,MAAM,SAASd,EAAMQ,EAAQC,EAASC,GACrC,MAAOG,IACLb,KAAMA,EACNQ,OAAQA,EACRC,QAASA,EACTC,OAAQA,MAMdH,EAAIS,KAAKX,EAAWQ,GAGtBnB,EAAMuB,QAAQ,UAAWvB,EAAOa,EAAKpB,EAAE4B,OAAOlB,EAAQF,IAE/CY,GASTtB,EAAO,WACL,MAAOH,GAAMoC,MAAMpC,EAAOqC,YAGrBhC,EAAE4B,OAAOK,UACdpC,KAAMA,EACNC,KAAMA,OAyDVJ,QAAQ,mBAAoB,aAAc,WAAY,SAASwC,EAAYD,GACzE,GAAIE,EAgBJ,OAdAA,GAAiB,SAASC,GACxB,GAAIC,GAAOC,IACXC,QAAOJ,eAAeG,KAAKE,YAAaJ,GACtCK,YAAY,EACZC,cAAc,EACdC,IAAK,WACH,MAAON,GAAKM,IAAIP,IAElBQ,IAAK,SAASC,GACZR,EAAKO,IAAIR,EAAKS,OAKbZ,EAASa,MAAMlB,QACpBmB,YAAa,WAmBX,MAlBAT,MAAKU,SACHC,UAAU,EACVC,SAAU,EACVC,QAAU,EACVC,SAAU,GAGZd,KAAKe,GAAG,UAAW,SAAS9C,EAAOa,EAAKZ,GACtC8B,KAAKgB,YACHL,SAA8B,WAAnBzC,EAAQF,OACnB4C,QAA8B,QAAnB1C,EAAQF,OACnB6C,OAA8B,SAAnB3C,EAAQF,QAAwC,QAAnBE,EAAQF,OAChD8C,SAAU,MAIdd,KAAKe,GAAG,aAAcf,KAAKiB,cAEpBtB,EAASa,MAAMf,MAAMO,KAAMN,YAGpCY,IAAK,SAASR,EAAKoB,EAAKhD,GACtB,GAAIiD,GAASxB,EAASa,MAAMY,UAAUd,IAAIb,MAAMO,KAAMN,UAOtD,OAJIyB,IACFnB,KAAKqB,YAAYvB,EAAKoB,EAAKhD,GAGtBiD,GAQTF,aAAc,WACZ,MAAOjB,MAAKgB,YACVL,UAAU,EACVC,SAAU,EACVC,QAAU,EACVC,SAAU,KAUdO,YAAa,SAASvB,EAAKoB,EAAKhD,GAC9B,GAAIoD,GAAM5C,EAAO6C,CAEjB,IAAI7D,EAAED,YAAYqC,GAChB,MAAOE,KAGLtC,GAAE8D,SAAS1B,IACbpB,EAAQoB,EACR5B,EAAUgD,IAETxC,MAAYoB,GAAOoB,EAGtBhD,EAAUA,MAENR,EAAED,YAAYuC,KAAKE,eACrBF,KAAKE,gBAGPqB,EAAQrD,EAAQqD,KAEhB,KAAKD,IAAQ5C,GACP6C,GAASvB,KAAKE,YAAYuB,eAAeH,SACpCtB,MAAKE,YAAYoB,GACdC,GAAUvB,KAAKE,YAAYoB,IACrCzB,EAAe6B,KAAK1B,KAAMsB,EAI9B,OAAOtB,OAWTgB,WAAY,SAASlB,EAAK6B,EAAOzD,GAC/B,GAAIoD,GAAM5C,CAEV,IAAIhB,EAAED,YAAYqC,GAChB,MAAOE,KAGLtC,GAAE8D,SAAS1B,IACbpB,EAAQoB,EACR5B,EAAUyD,IAETjD,MAAYoB,GAAO6B,EAGtBzD,EAAUA,KAEV,KAAKoD,IAAQtB,MAAKU,QACZhC,EAAM+C,eAAeH,IAAS5D,EAAEkE,UAAUlD,EAAM4C,MAClDtB,KAAKU,QAAQY,GAAQ5C,EAAM4C,KAKjCO,eAAgB,SAASP,EAAMpD,GAC7B,MAAO8B,MAAKqB,YAAYC,EAAM,OAAQ5D,EAAE4B,UAAWpB,GAAUqD,OAAO,WAuD1EnE,QAAQ,wBAAyB,WAAY,kBAAmB,SAASuC,EAAUmC,GACjF,MAAOnC,GAASoC,WAAWzC,QACzBrB,MAAO6D,EAEPrB,YAAa,WACX,GAAIV,GAAOC,IAGXA,MAAKU,SACHC,UAAU,EACVC,SAAU,EACVC,QAAU,EACVC,SAAU,GAGZd,KAAKe,GAAG,UAAW,SAAS9C,EAAOa,EAAKZ,GACtC8B,KAAKgB,YACHL,SAA8B,WAAnBzC,EAAQF,OACnB4C,QAA8B,QAAnB1C,EAAQF,OACnB6C,OAA8B,SAAnB3C,EAAQF,QAAwC,QAAnBE,EAAQF,OAChD8C,SAAU,MAIdd,KAAKe,GAAG,aAAcf,KAAKiB,cAG3BjB,KAAKe,GAAG,UAAWf,KAAKiB,cAExBhB,OAAOJ,eAAeG,KAAM,WAC1BG,YAAY,EACZE,IAAK,WACH,MAAON,GAAKiC,UAIhBrC,EAASoC,WAAWtC,MAAMO,KAAMN,YAWlCsB,WAAY,SAASlB,EAAK6B,EAAOzD,GAC/B,GAAIoD,GAAM5C,CAEV,IAAIhB,EAAED,YAAYqC,GAChB,MAAOE,KAGLtC,GAAE8D,SAAS1B,IACbpB,EAAQoB,EACR5B,EAAUyD,IAETjD,MAAYoB,GAAO6B,EAGtBzD,EAAUA,KAEV,KAAKoD,IAAQtB,MAAKU,QACZhC,EAAM+C,eAAeH,IAAS5D,EAAEkE,UAAUlD,EAAM4C,MAClDtB,KAAKU,QAAQY,GAAQ5C,EAAM4C,KAUjCL,aAAc,WACZ,MAAOjB,MAAKgB,YACVL,UAAU,EACVC,SAAU,EACVC,QAAU,EACVC,SAAU,WAMnB/D,OAAQC","file":"ng-backbone.min.js"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ng-backbone 2 | === 3 | Backbone data model and collection for AngularJS 4 | 5 | [![GitHub release](https://img.shields.io/github/release/adrianlee44/ng-backbone.svg?style=flat-square)]() 6 | [![Build Status](http://img.shields.io/travis/adrianlee44/ng-backbone.svg?style=flat-square)](https://travis-ci.org/adrianlee44/ng-backbone) 7 | [![Coverage Status](https://img.shields.io/coveralls/adrianlee44/ng-backbone/master.svg?style=flat-square)](https://coveralls.io/github/adrianlee44/ng-backbone?branch=master) 8 | 9 | [![David](https://img.shields.io/david/adrianlee44/ng-backbone.svg?style=flat-square)]() 10 | [![David](https://img.shields.io/david/dev/adrianlee44/ng-backbone.svg?style=flat-square)]() 11 | 12 | ### Dependencies 13 | - [AngualrJS](https://angularjs.org) 14 | - [UnderscoreJS](http://underscorejs.org) / [LoDash](http://lodash.com) 15 | - [BackboneJS](http://backbonejs.org) 16 | 17 | 18 | Backbone factory 19 | --- 20 | 21 | To make Backbone work properly with AngularJS, ng-backbone overrides Backbone's sync and ajax methods. 22 | 23 | 24 | NgBackboneModel 25 | --- 26 | 27 | Base NgBackbone model extends Backbone.model by adding additional properties and functions, including `$attributes` and `$status`. When overriding NgBackboneModel `set` method but you would like to keep `$attributes`, you'll have to explicitly call NgBackboneModel set: 28 | ```javascript 29 | var Sample = NgBackboneModel.extend({ 30 | set: function(key, val, options) { 31 | NgBackboneModel.prototype.set.apply(this, arguments); 32 | } 33 | }); 34 | ``` 35 | 36 | In rare cases when you want to override the constructor which allows you to replace the actual constructor function for your model, you should invoke NgBackboneModel constructor in the end. 37 | ```javascript 38 | var Sample = NgBackboneModel.extend({ 39 | constructor: function() { 40 | this.text = 'Sample!'; 41 | NgBackboneModel.apply(this, arguments); 42 | } 43 | }); 44 | ``` 45 | 46 | The `$attributes` property allows application to use AngularJS two-way binding to manipulate Backbone objects using Backbone `get` and `set`. 47 | HTML: 48 | ```html 49 | 50 | ``` 51 | 52 | Javascript: 53 | ```javascript 54 | $scope.person = new Person({ 55 | name: 'John' 56 | }); 57 | ``` 58 | 59 | The `$status` property is the hash containing model sync state. Since `$status` updates using Backbone event, passing `{silent: true}` will prevent `$status` from updating. `$status` contains four properties, including: 60 | - `deleting`: Set to true when invoking `destroy` method on model (HTTP `DELETE` request) 61 | - `loading`: Set to true when fetching model data from server (HTTP `GET` request) 62 | - `saving`: Set to true when creating or updating model (HTTP `POST` or `PUT` request) 63 | - `syncing`: Set to true whenever a model has started a request to the server 64 | 65 | HTML: 66 | ```html 67 | Loading 68 | 69 | ``` 70 | 71 | Javascript: 72 | ```javascript 73 | $scope.user = new User({id: '123'}); 74 | $scope.user.fetch(); 75 | ``` 76 | 77 | 78 | $resetStatus 79 | --- 80 | 81 | Reset all properties on `$status` including `deleting`, `loading`, `saving`, and `syncing` back to false 82 | 83 | 84 | $setStatus 85 | --- 86 | 87 | Update model status on `$status` 88 | 89 | 90 | ### Parameters 91 | **attributes** 92 | Type: `Object` 93 | Set one or multiple statuses 94 | 95 | **options** 96 | Type: `Object` 97 | Options 98 | 99 | 100 | 101 | NgBackboneCollection 102 | --- 103 | 104 | Base NgBackbone collection extends Backbone.collection by adding additonal properties and functions, such as `$models` and `$status`. 105 | 106 | Similar to NgBackboneModel, in rare cases where you may want to override the constructor, you should invoke NgBackboneCollection in the end. 107 | ```javascript 108 | var SampleCollection = NgBackboneCollection.extend({ 109 | constructor: function(models, options) { 110 | this.allSamples = false; 111 | 112 | NgBackboneCollection.apply(this, arguments); 113 | } 114 | }); 115 | ``` 116 | 117 | The `$models` property creates a one-way binding to collection `models` which is the Javascript array of models. Application can only access the array with `$models` but will not be able to modify it. 118 | HTML: 119 | ```html 120 | 123 | ``` 124 | 125 | Javascript: 126 | ``` 127 | $scope.users = new Users(); 128 | $scope.users.fetch(); 129 | ``` 130 | 131 | The `$status` property is the hash containing collection and its models sync state. Since `$status` updates using Backbone event, passing `{silent: true}` will prevent `$status` from updating. `$status` contains four properties, including: 132 | - `deleting`: Set to true when one of its models is getting destroyed (HTTP `DELETE` request) 133 | - `loading`: Set to true when fetching collection data from server (HTTP `GET` request) 134 | - `saving`: Set to true when creating or updating one of its models (HTTP `POST` or `PUT` request) 135 | - `syncing`: Set to true whenever a collection has started a request to the server 136 | 137 | HTML: 138 | ```html 139 | 143 | ``` 144 | 145 | Javascript: 146 | ``` 147 | $scope.users = new Users(); 148 | $scope.users.fetch(); 149 | ``` 150 | 151 | 152 | 153 | $setStatus 154 | --- 155 | 156 | Update collection status 157 | 158 | 159 | Type: `function` 160 | 161 | ### Parameters 162 | **attributes** 163 | Type: `Object` 164 | Set on or multiple statuses 165 | 166 | **options** 167 | Type: `Object` 168 | Options 169 | 170 | 171 | 172 | $resetStatus 173 | --- 174 | 175 | Reset all statuses including `deleting`, `loading`, `saving`, and `syncing` back to false 176 | 177 | Type: `function` 178 | -------------------------------------------------------------------------------- /test/backbone.spec.js: -------------------------------------------------------------------------------- 1 | describe('Backbone', function () { 2 | var Backbone, $httpBackend, tempModel; 3 | 4 | beforeEach(function () { 5 | module('ngBackbone'); 6 | 7 | inject(function(_$httpBackend_, _Backbone_) { 8 | $httpBackend = _$httpBackend_; 9 | Backbone = _Backbone_; 10 | }); 11 | 12 | tempModel = new Backbone.Model(); 13 | }); 14 | 15 | afterEach(function () { 16 | $httpBackend.verifyNoOutstandingExpectation(); 17 | $httpBackend.verifyNoOutstandingRequest(); 18 | }); 19 | 20 | it('should override Backbone.ajax with $http', function () { 21 | var ajaxDefinition = Backbone.ajax.toString(); 22 | expect(ajaxDefinition.indexOf('$http')).not.toBe(-1); 23 | }); 24 | 25 | describe('callbacks', function () { 26 | it('should call success', function () { 27 | var success = jasmine.createSpy('success'); 28 | 29 | $httpBackend.when('GET', '/test').respond(200, {}); 30 | 31 | Backbone.sync('read', tempModel, { 32 | url: '/test', 33 | success: success 34 | }); 35 | 36 | $httpBackend.flush(); 37 | 38 | expect(success).toHaveBeenCalled(); 39 | }); 40 | 41 | it('should call return data on success', function () { 42 | var success = function(data) { 43 | expect(data.message).toBe('success'); 44 | }; 45 | 46 | $httpBackend.when('GET', '/test').respond(200, {message: 'success'}); 47 | 48 | Backbone.sync('read', tempModel, { 49 | url: '/test', 50 | success: success 51 | }); 52 | 53 | $httpBackend.flush(); 54 | }); 55 | 56 | it('should call error', function () { 57 | var error = jasmine.createSpy('error'); 58 | 59 | $httpBackend.when('GET', '/test').respond(400, {}); 60 | 61 | Backbone.sync('read', tempModel, { 62 | url: '/test', 63 | error: error 64 | }); 65 | 66 | $httpBackend.flush(); 67 | 68 | expect(error).toHaveBeenCalled(); 69 | }); 70 | 71 | it('should call return data on error', function () { 72 | var error = function(data) { 73 | expect(data.message).toBe('test is broken'); 74 | }; 75 | 76 | $httpBackend.when('GET', '/test').respond(400, {message: 'test is broken'}); 77 | 78 | Backbone.sync('read', tempModel, { 79 | url: '/test', 80 | error: error 81 | }); 82 | 83 | $httpBackend.flush(); 84 | }); 85 | 86 | it('should set xhr params on options', function () { 87 | $httpBackend.when('GET', '/test').respond(400, {message: 'test is broken'}); 88 | 89 | var options = { 90 | url: '/test', 91 | error: angular.noop 92 | }; 93 | 94 | Backbone.sync('read', tempModel, options); 95 | 96 | $httpBackend.flush(); 97 | 98 | expect(options.xhr.status).toBe(400); 99 | }); 100 | }); 101 | 102 | it('should make a GET request', function () { 103 | $httpBackend.expectGET('/test').respond(200, {}); 104 | 105 | Backbone.sync('read', tempModel, { 106 | url: '/test' 107 | }); 108 | 109 | $httpBackend.flush(); 110 | }); 111 | 112 | it('should make a POST request', function () { 113 | $httpBackend.expectPOST('/test').respond(200, {}); 114 | 115 | Backbone.sync('create', tempModel, { 116 | url: '/test' 117 | }); 118 | 119 | $httpBackend.flush(); 120 | }); 121 | 122 | it('should make a POST request using fetch with options override', function () { 123 | $httpBackend.expectPOST('/test').respond(200, {}); 124 | 125 | Backbone.sync('create', tempModel, { 126 | method: 'POST', 127 | url: '/test' 128 | }); 129 | 130 | $httpBackend.flush(); 131 | }); 132 | 133 | it('should make a DELETE request', function () { 134 | $httpBackend.expectDELETE('/test').respond(200, {}); 135 | 136 | Backbone.sync('delete', tempModel, { 137 | url: '/test' 138 | }); 139 | 140 | $httpBackend.flush(); 141 | }); 142 | 143 | it('should make a PATCH request', function () { 144 | $httpBackend.expectPATCH('/test').respond(200, {}); 145 | 146 | Backbone.sync('patch', tempModel, { 147 | url: '/test' 148 | }); 149 | 150 | $httpBackend.flush(); 151 | }); 152 | 153 | it('should make a update request', function () { 154 | $httpBackend.expectPUT('/test').respond(200, {}); 155 | 156 | Backbone.sync('update', tempModel, { 157 | url: '/test' 158 | }); 159 | 160 | $httpBackend.flush(); 161 | }); 162 | 163 | it('should add querystring on read', function () { 164 | $httpBackend.expectGET('/test?hello=world').respond(200, {}); 165 | 166 | Backbone.sync('read', tempModel, { 167 | url: '/test', 168 | data: { 169 | hello: 'world' 170 | } 171 | }); 172 | 173 | $httpBackend.flush(); 174 | }); 175 | 176 | it('should not add querystring on read with post', function () { 177 | $httpBackend.expectPOST('/test', { 178 | hello: 'world' 179 | }).respond(200, {}); 180 | 181 | Backbone.sync('read', tempModel, { 182 | url: '/test', 183 | method: 'POST', 184 | data: { 185 | hello: 'world' 186 | } 187 | }); 188 | 189 | $httpBackend.flush(); 190 | }); 191 | 192 | it('should stingify attributes on create', function () { 193 | $httpBackend.expectPOST('/test', '{"hello":"world"}').respond(200, {}); 194 | 195 | 196 | Backbone.sync('create', tempModel, { 197 | url: '/test', 198 | attrs: { 199 | hello: 'world' 200 | } 201 | }); 202 | 203 | $httpBackend.flush(); 204 | 205 | }); 206 | 207 | it('should stringify with model toJSON method', function () { 208 | $httpBackend.expectPOST('/test', '{"hello":"world"}').respond(200, {}); 209 | 210 | tempModel.set({hello: 'world'}); 211 | 212 | Backbone.sync('create', tempModel, { 213 | url: '/test', 214 | attrs: { 215 | hello: 'world' 216 | } 217 | }); 218 | 219 | $httpBackend.flush(); 220 | }); 221 | 222 | it('should get the url on model', function () { 223 | tempModel.urlRoot = '/'; 224 | tempModel.set('id', 'test'); 225 | 226 | $httpBackend.expectPOST('/test').respond(200, {}); 227 | 228 | Backbone.sync('create', tempModel); 229 | 230 | $httpBackend.flush(); 231 | }); 232 | 233 | it('should trigger request event', function () { 234 | var request = jasmine.createSpy('request'); 235 | tempModel.on('request', request); 236 | 237 | $httpBackend.when('POST', '/test').respond(200, {}); 238 | 239 | Backbone.sync('create', tempModel, { 240 | url: '/test' 241 | }); 242 | 243 | $httpBackend.flush(); 244 | 245 | expect(request).toHaveBeenCalled(); 246 | }); 247 | 248 | }); 249 | -------------------------------------------------------------------------------- /test/ng-backbone-model.spec.js: -------------------------------------------------------------------------------- 1 | describe('NgBackboneModel', function() { 2 | var NgBackboneModel, tempModel; 3 | 4 | beforeEach(function() { 5 | module('ngBackbone'); 6 | 7 | inject(function(_NgBackboneModel_) { 8 | NgBackboneModel = _NgBackboneModel_; 9 | }); 10 | 11 | tempModel = new NgBackboneModel(); 12 | }); 13 | 14 | it('should have NgBackboneModel as the constructor name', function(){ 15 | expect(tempModel.constructor.name).toBe('NgBackboneModel'); 16 | }); 17 | 18 | it('should create $attributes object', function() { 19 | expect(tempModel.$attributes).toBeDefined(); 20 | }); 21 | 22 | it('should get the correct attribute', function() { 23 | tempModel = new NgBackboneModel({ 24 | hello: 'world', 25 | foo: 'bar' 26 | }); 27 | 28 | expect(tempModel.$attributes.hello).toBe('world'); 29 | expect(tempModel.$attributes.hello1).toBeUndefined(); 30 | }); 31 | 32 | it('should set an attribute', function() { 33 | tempModel = new NgBackboneModel({ 34 | test: 'foo' 35 | }); 36 | 37 | tempModel.$attributes.test = 'testing'; 38 | 39 | expect(tempModel.get('test')).toBe('testing'); 40 | }); 41 | 42 | it('should trigger a change event', function() { 43 | var change = jasmine.createSpy('change'), 44 | changeFoo = jasmine.createSpy('change:foo'); 45 | 46 | 47 | tempModel = new NgBackboneModel({ 48 | foo: 'bar' 49 | }); 50 | tempModel.on('change', change); 51 | tempModel.on('change:foo', changeFoo); 52 | 53 | tempModel.$attributes.foo = 'bar123'; 54 | 55 | expect(change).toHaveBeenCalled(); 56 | expect(changeFoo).toHaveBeenCalled(); 57 | }); 58 | 59 | it('should set a property on $attributes', function() { 60 | tempModel = new NgBackboneModel(); 61 | 62 | tempModel.set('foo', 'bar'); 63 | 64 | expect(tempModel.$attributes.hasOwnProperty('foo')).toBe(true); 65 | expect(tempModel.$attributes.foo).toBe('bar'); 66 | }); 67 | 68 | it('should not set a property on $attributes when key is not defined', function() { 69 | tempModel = new NgBackboneModel(); 70 | 71 | tempModel.set(undefined, 'bar'); 72 | 73 | expect(tempModel.$attributes.hasOwnProperty('foo')).toBe(false); 74 | }); 75 | 76 | it('should unset a property on $attributes', function() { 77 | tempModel = new NgBackboneModel({ 78 | foo: 'bar' 79 | }); 80 | 81 | tempModel.unset('foo'); 82 | 83 | expect(tempModel.$attributes.hasOwnProperty('foo')).toBe(false); 84 | }); 85 | 86 | it('should unset a property with removeBinding', function() { 87 | tempModel = new NgBackboneModel({ 88 | foo: 'bar' 89 | }); 90 | 91 | tempModel.$removeBinding('foo'); 92 | 93 | expect(tempModel.$attributes.hasOwnProperty('foo')).toBe(false); 94 | }); 95 | 96 | describe('$status', function() { 97 | var model, $httpBackend; 98 | 99 | beforeEach(inject(function(_$httpBackend_) { 100 | model = new NgBackboneModel(); 101 | 102 | $httpBackend = _$httpBackend_; 103 | })); 104 | 105 | it('should create $status object', function() { 106 | expect(model.$status).toBeDefined(); 107 | }); 108 | 109 | it('should default all status to false', function() { 110 | expect(model.$status.deleting).toBe(false); 111 | expect(model.$status.loading).toBe(false); 112 | expect(model.$status.saving).toBe(false); 113 | expect(model.$status.syncing).toBe(false); 114 | }); 115 | 116 | describe('syncing should be updated', function(){ 117 | it('should set on GET request', function() { 118 | $httpBackend.when('GET', '/get').respond({}); 119 | 120 | model.fetch({url: '/get'}); 121 | 122 | expect(model.$status.syncing).toBe(true); 123 | 124 | $httpBackend.flush(); 125 | }); 126 | 127 | it('should set on POST request', function() { 128 | $httpBackend.when('POST', '/post').respond({}); 129 | 130 | model.save({}, {url: '/post'}); 131 | 132 | expect(model.$status.syncing).toBe(true); 133 | 134 | $httpBackend.flush(); 135 | }); 136 | }); 137 | 138 | describe('loading should be updated', function() { 139 | it('should set on GET request', function() { 140 | $httpBackend.when('GET', '/get').respond({}); 141 | 142 | model.fetch({url: '/get'}); 143 | 144 | expect(model.$status.loading).toBe(true); 145 | 146 | $httpBackend.flush(); 147 | }); 148 | 149 | it('should not set on POST request', function() { 150 | $httpBackend.when('POST', '/post').respond({}); 151 | 152 | model.save({}, {url: '/post'}); 153 | 154 | expect(model.$status.loading).toBe(false); 155 | 156 | $httpBackend.flush(); 157 | }); 158 | }); 159 | 160 | describe('saving should be updated', function() { 161 | it('should set on POST request', function() { 162 | $httpBackend.when('POST', '/post').respond({}); 163 | 164 | model.save({}, {url: '/post'}); 165 | 166 | expect(model.$status.saving).toBe(true); 167 | 168 | $httpBackend.flush(); 169 | }); 170 | 171 | it('should set on PUT request', function() { 172 | $httpBackend.when('PUT', '/put').respond({}); 173 | 174 | model = new NgBackboneModel({ 175 | id: 'test-123' 176 | }); 177 | 178 | model.save({name: 'hello'}, {url: '/put'}); 179 | 180 | expect(model.$status.saving).toBe(true); 181 | 182 | $httpBackend.flush(); 183 | }); 184 | 185 | it('should not set on GET request', function() { 186 | $httpBackend.when('GET', '/get').respond({}); 187 | 188 | model.fetch({url: '/get'}); 189 | 190 | expect(model.$status.saving).toBe(false); 191 | 192 | $httpBackend.flush(); 193 | }); 194 | }); 195 | 196 | describe('deleting should be updated', function() { 197 | it('should set on DELETE request', function() { 198 | $httpBackend.when('DELETE', '/delete').respond({}); 199 | 200 | model = new NgBackboneModel({ 201 | id: 'test-123' 202 | }); 203 | 204 | model.destroy({url: '/delete'}); 205 | 206 | expect(model.$status.deleting).toBe(true); 207 | 208 | $httpBackend.flush(); 209 | }); 210 | 211 | it('should not set on POST request', function() { 212 | $httpBackend.when('POST', '/post').respond({}); 213 | 214 | model.save({}, {url: '/post'}); 215 | 216 | expect(model.$status.deleting).toBe(false); 217 | 218 | $httpBackend.flush(); 219 | }); 220 | }); 221 | 222 | it('should not set status when key is not defined', function() { 223 | var output = model.$setStatus(undefined, true); 224 | 225 | expect(output).toBe(model); 226 | expect(model.hasOwnProperty(undefined)).toBe(false); 227 | }); 228 | 229 | it('should set status when key exist', function() { 230 | model.$setStatus('deleting', true); 231 | 232 | expect(model.$status.deleting).toBe(true); 233 | }); 234 | 235 | it('should set status when key is invalid', function() { 236 | model.$setStatus('doesNotExist', true); 237 | 238 | expect(model.$status.hasOwnProperty('doesNotExist')).toBe(false); 239 | }); 240 | }); 241 | }); 242 | -------------------------------------------------------------------------------- /test/ng-backbone-collection.spec.js: -------------------------------------------------------------------------------- 1 | describe('NgBackboneCollection', function () { 2 | var NgBackboneCollection, collection; 3 | 4 | beforeEach(function () { 5 | module('ngBackbone'); 6 | 7 | inject(function (_NgBackboneCollection_) { 8 | NgBackboneCollection = _NgBackboneCollection_; 9 | }); 10 | 11 | collection = new NgBackboneCollection(); 12 | }); 13 | 14 | it('should have NgBackboneCollection as the constructor name', function () { 15 | expect(collection.constructor.name).toBe('NgBackboneCollection'); 16 | }); 17 | 18 | it('should create $models object', function () { 19 | expect(collection.$models).toBeDefined(); 20 | }); 21 | 22 | it('should be the same array as models', function () { 23 | var model; 24 | 25 | expect(collection.$models.length).toBe(0); 26 | 27 | model = new collection.model(); 28 | 29 | collection.add(model); 30 | 31 | expect(collection.$models.length).toBe(1); 32 | expect(collection.$models[0]).toBe(model); 33 | }); 34 | 35 | describe('$status', function () { 36 | var $httpBackend; 37 | 38 | beforeEach(inject(function (_$httpBackend_) { 39 | $httpBackend = _$httpBackend_; 40 | })); 41 | 42 | it('should create $status object', function () { 43 | expect(collection.$status).toBeDefined(); 44 | }); 45 | 46 | it('should default all status to false', function () { 47 | expect(collection.$status.deleting).toBe(false); 48 | expect(collection.$status.loading).toBe(false); 49 | expect(collection.$status.saving).toBe(false); 50 | expect(collection.$status.syncing).toBe(false); 51 | }); 52 | 53 | it('should not set status when key is not defined', function() { 54 | var output = collection.$setStatus(undefined, true); 55 | 56 | expect(output).toBe(collection); 57 | expect(collection.hasOwnProperty(undefined)).toBe(false); 58 | }); 59 | 60 | it('should set status when key exist', function() { 61 | collection.$setStatus('deleting', true); 62 | 63 | expect(collection.$status.deleting).toBe(true); 64 | }); 65 | 66 | it('should set status when key is invalid', function() { 67 | collection.$setStatus('doesNotExist', true); 68 | 69 | expect(collection.$status.hasOwnProperty('doesNotExist')).toBe(false); 70 | }); 71 | 72 | describe('syncing sould be updated', function () { 73 | it('should set on GET request', function () { 74 | $httpBackend.when('GET', '/get').respond({}); 75 | 76 | collection.fetch({url: '/get'}); 77 | 78 | expect(collection.$status.syncing).toBe(true); 79 | 80 | $httpBackend.flush(); 81 | }); 82 | 83 | it('should set on POST request', function () { 84 | $httpBackend.when('POST', '/post').respond({}); 85 | 86 | collection.create({hello: 'world'}, {url: '/post'}); 87 | 88 | expect(collection.$status.syncing).toBe(true); 89 | 90 | $httpBackend.flush(); 91 | }); 92 | 93 | it('should set when a model on the collection fetches', function () { 94 | var model = new collection.model(); 95 | 96 | collection.add(model); 97 | 98 | $httpBackend.when('GET', '/get').respond({}); 99 | 100 | model.fetch({url: '/get'}); 101 | 102 | expect(collection.$status.syncing).toBe(true); 103 | 104 | $httpBackend.flush(); 105 | }); 106 | 107 | it('should set when a model on the collection POST', function () { 108 | var model = new collection.model(); 109 | 110 | collection.url = '/post'; 111 | 112 | collection.add(model); 113 | 114 | $httpBackend.when('POST', '/post').respond({}); 115 | 116 | model.save(); 117 | 118 | expect(collection.$status.syncing).toBe(true); 119 | 120 | $httpBackend.flush(); 121 | }); 122 | }); 123 | 124 | describe('loading should be updated', function () { 125 | it('should set on GET request', function () { 126 | $httpBackend.when('GET', '/get').respond({}); 127 | 128 | collection.fetch({url: '/get'}); 129 | 130 | expect(collection.$status.loading).toBe(true); 131 | 132 | $httpBackend.flush(); 133 | }); 134 | 135 | it('should not set on POST request', function () { 136 | $httpBackend.when('POST', '/post').respond({}); 137 | 138 | collection.create({}, {url: '/post'}); 139 | 140 | expect(collection.$status.loading).toBe(false); 141 | 142 | $httpBackend.flush(); 143 | }); 144 | }); 145 | 146 | describe('saving should be updated', function () { 147 | it('should set on POST request', function () { 148 | $httpBackend.when('POST', '/post').respond({}); 149 | 150 | collection.create({}, {url: '/post'}); 151 | 152 | expect(collection.$status.saving).toBe(true); 153 | 154 | $httpBackend.flush(); 155 | }); 156 | 157 | it('should not set on GET request', function () { 158 | $httpBackend.when('GET', '/get').respond({}); 159 | 160 | collection.fetch({url: '/get'}); 161 | 162 | expect(collection.$status.saving).toBe(false); 163 | 164 | $httpBackend.flush(); 165 | }); 166 | 167 | it('should set when a model on collection is saving', function () { 168 | var model = new collection.model({id: 'test-123'}); 169 | 170 | collection.url = '/put'; 171 | 172 | collection.add(model); 173 | 174 | $httpBackend.when('PUT', '/put/test-123').respond({}); 175 | 176 | model.save({name: 'hello'}); 177 | 178 | expect(collection.$status.saving).toBe(true); 179 | 180 | $httpBackend.flush(); 181 | }); 182 | }); 183 | 184 | describe('deleting should be updated', function () { 185 | var model; 186 | 187 | beforeEach(function () { 188 | model = new collection.model({id: 'test-123'}); 189 | 190 | collection.url = '/collection'; 191 | 192 | collection.add(model); 193 | 194 | $httpBackend.when('DELETE', '/collection/test-123').respond({}); 195 | }); 196 | 197 | it('should set when a model on collection is getting destroyed', function () { 198 | model.destroy({wait: true}); 199 | 200 | expect(collection.$status.deleting).toBe(true); 201 | 202 | $httpBackend.flush(); 203 | }); 204 | 205 | it('should clear status when destroying without wait', function () { 206 | model.destroy(); 207 | 208 | $httpBackend.flush(); 209 | 210 | expect(collection.$status.deleting).toBe(false); 211 | expect(collection.$status.loading).toBe(false); 212 | }); 213 | 214 | it('should clear status when DELETE request complete', function () { 215 | model.destroy({wait: true}); 216 | 217 | $httpBackend.flush(); 218 | 219 | expect(collection.$status.deleting).toBe(false); 220 | expect(collection.$status.loading).toBe(false); 221 | }); 222 | }); 223 | }); 224 | 225 | describe('callbacks', function () { 226 | var $httpBackend; 227 | 228 | beforeEach(inject(function (_$httpBackend_) { 229 | $httpBackend = _$httpBackend_; 230 | })); 231 | 232 | it('should expose xhr params on the options argument', function (done) { 233 | $httpBackend.when('GET', '/get').respond(400); 234 | 235 | var error = function (collection, response, options) { 236 | expect(options.xhr.status).toBe(400); 237 | 238 | done(); 239 | }; 240 | 241 | collection.fetch({ 242 | url: '/get', 243 | error: error 244 | }); 245 | 246 | $httpBackend.flush(); 247 | }); 248 | }); 249 | }); 250 | -------------------------------------------------------------------------------- /ng-backbone.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name ng-backbone 3 | * @version 1.0.0 4 | * @author [adrianlee44](https://github.com/adrianlee44) 5 | * @license https://github.com/adrianlee44/ng-backbone/blob/master/LICENSE.md 6 | * @dependencies 7 | * - [AngualrJS](https://angularjs.org) 8 | * - [UnderscoreJS](http://underscorejs.org) / [LoDash](http://lodash.com) 9 | * - [BackboneJS](http://backbonejs.org) 10 | * @description 11 | * Backbone data model and collection for AngularJS 12 | */ 13 | 14 | (function(window, document, undefined) { 15 | 'use strict'; 16 | 17 | angular.module('ngBackbone', []). 18 | /** 19 | * @name Backbone factory 20 | * @description 21 | * To make Backbone work properly with AngularJS, ng-backbone overrides Backbone's sync and ajax methods. 22 | */ 23 | factory('Backbone', ['$http', function($http) { 24 | var methodMap, sync, ajax, isUndefined = _.isUndefined; 25 | 26 | methodMap = { 27 | create: 'POST', 28 | update: 'PUT', 29 | patch: 'PATCH', 30 | delete: 'DELETE', 31 | read: 'GET' 32 | }; 33 | 34 | sync = function(method, model, options) { 35 | // Default options to empty object 36 | if (isUndefined(options)) { 37 | options = {}; 38 | } 39 | 40 | var httpMethod = options.method || methodMap[method], 41 | params = {method: httpMethod}; 42 | 43 | if (!options.url) { 44 | params.url = _.result(model, 'url'); 45 | } 46 | 47 | if (isUndefined(options.data) && model && (httpMethod === 'POST' || httpMethod === 'PUT' || httpMethod === 'PATCH')) { 48 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); 49 | } 50 | 51 | // AngularJS $http doesn't convert data to querystring for GET method 52 | if (httpMethod === 'GET' && !isUndefined(options.data)) { 53 | params.params = options.data; 54 | } 55 | 56 | var successFn = function(response) { 57 | options.xhr = { 58 | status: response.status, 59 | headers: response.headers, 60 | config: response.config 61 | }; 62 | 63 | if (!isUndefined(options.success) && _.isFunction(options.success)) { 64 | options.success(response.data); 65 | } 66 | }; 67 | 68 | var errorFn = function(response) { 69 | options.xhr = { 70 | status: response.status, 71 | headers: response.headers, 72 | config: response.config 73 | }; 74 | 75 | if (!isUndefined(options.error) && _.isFunction(options.error)) { 76 | options.error(response.data); 77 | } 78 | }; 79 | 80 | var xhr = ajax(_.extend(params, options)); 81 | 82 | // If $http has a legacy promise 83 | if (!isUndefined(xhr.success) && _.isFunction(xhr.success)) { 84 | xhr 85 | .success(function(data, status, headers, config) { 86 | return successFn({ 87 | data: data, 88 | status: status, 89 | headers: headers, 90 | config: config 91 | }); 92 | }) 93 | .error(function(data, status, headers, config) { 94 | return errorFn({ 95 | data: data, 96 | status: status, 97 | headers: headers, 98 | config: config 99 | }); 100 | }); 101 | 102 | // If $http promise comes with `then` 103 | } else { 104 | xhr.then(successFn, errorFn); 105 | } 106 | 107 | model.trigger('request', model, xhr, _.extend(params, options)); 108 | 109 | return xhr; 110 | }; 111 | 112 | /** 113 | * @private 114 | * @name ajax 115 | * @description 116 | * Making ajax request 117 | */ 118 | ajax = function() { 119 | return $http.apply($http, arguments); 120 | }; 121 | 122 | return _.extend(Backbone, { 123 | sync: sync, 124 | ajax: ajax 125 | }); 126 | }]). 127 | 128 | /** 129 | * @name NgBackboneModel 130 | * @description 131 | * Base NgBackbone model extends Backbone.model by adding additional properties and functions, including `$attributes` and `$status`. When overriding NgBackboneModel `set` method but you would like to keep `$attributes`, you'll have to explicitly call NgBackboneModel set: 132 | * ```javascript 133 | * var Sample = NgBackboneModel.extend({ 134 | * set: function(key, val, options) { 135 | * NgBackboneModel.prototype.set.apply(this, arguments); 136 | * } 137 | * }); 138 | * ``` 139 | * 140 | * In rare cases when you want to override the constructor which allows you to replace the actual constructor function for your model, you should invoke NgBackboneModel constructor in the end. 141 | * ```javascript 142 | * var Sample = NgBackboneModel.extend({ 143 | * constructor: function() { 144 | * this.text = 'Sample!'; 145 | * NgBackboneModel.apply(this, arguments); 146 | * } 147 | * }); 148 | * ``` 149 | * 150 | * The `$attributes` property allows application to use AngularJS two-way binding to manipulate Backbone objects using Backbone `get` and `set`. 151 | * HTML: 152 | * ```html 153 | * 154 | * ``` 155 | * 156 | * Javascript: 157 | * ```javascript 158 | * $scope.person = new Person({ 159 | * name: 'John' 160 | * }); 161 | * ``` 162 | * 163 | * The `$status` property is the hash containing model sync state. Since `$status` updates using Backbone event, passing `{silent: true}` will prevent `$status` from updating. `$status` contains four properties, including: 164 | * - `deleting`: Set to true when invoking `destroy` method on model (HTTP `DELETE` request) 165 | * - `loading`: Set to true when fetching model data from server (HTTP `GET` request) 166 | * - `saving`: Set to true when creating or updating model (HTTP `POST` or `PUT` request) 167 | * - `syncing`: Set to true whenever a model has started a request to the server 168 | * 169 | * HTML: 170 | * ```html 171 | * Loading 172 | * 173 | * ``` 174 | * 175 | * Javascript: 176 | * ```javascript 177 | * $scope.user = new User({id: '123'}); 178 | * $scope.user.fetch(); 179 | * ``` 180 | */ 181 | factory('NgBackboneModel', ['$rootScope', 'Backbone', function($rootScope, Backbone) { 182 | var defineProperty; 183 | 184 | defineProperty = function(key) { 185 | var self = this; 186 | Object.defineProperty(this.$attributes, key, { 187 | enumerable: true, 188 | configurable: true, 189 | get: function() { 190 | return self.get(key); 191 | }, 192 | set: function(newValue) { 193 | self.set(key, newValue); 194 | } 195 | }); 196 | }; 197 | 198 | return Backbone.Model.extend({ 199 | constructor: function NgBackboneModel() { 200 | this.$status = { 201 | deleting: false, 202 | loading: false, 203 | saving: false, 204 | syncing: false 205 | }; 206 | 207 | this.on('request', function(model, xhr, options) { 208 | this.$setStatus({ 209 | deleting: (options.method === 'DELETE'), 210 | loading: (options.method === 'GET'), 211 | saving: (options.method === 'POST' || options.method === 'PUT'), 212 | syncing: true 213 | }); 214 | }); 215 | 216 | this.on('sync error', this.$resetStatus); 217 | 218 | return Backbone.Model.apply(this, arguments); 219 | }, 220 | 221 | set: function(key, val, options) { 222 | var output = Backbone.Model.prototype.set.apply(this, arguments); 223 | 224 | // Do not set binding if attributes are invalid 225 | if (output) { 226 | this.$setBinding(key, val, options); 227 | } 228 | 229 | return output; 230 | }, 231 | 232 | /** 233 | * @name $resetStatus 234 | * @description 235 | * Reset all properties on `$status` including `deleting`, `loading`, `saving`, and `syncing` back to false 236 | */ 237 | $resetStatus: function() { 238 | return this.$setStatus({ 239 | deleting: false, 240 | loading: false, 241 | saving: false, 242 | syncing: false 243 | }); 244 | }, 245 | 246 | /** 247 | * @private 248 | * @name setBinding 249 | * @description 250 | * Add binding on `$attributes` to a key on `attributes` 251 | */ 252 | $setBinding: function(key, val, options) { 253 | var attr, attrs, unset; 254 | 255 | if (_.isUndefined(key)) { 256 | return this; 257 | } 258 | 259 | if (_.isObject(key)) { 260 | attrs = key; 261 | options = val; 262 | } else { 263 | (attrs = {})[key] = val; 264 | } 265 | 266 | options = options || {}; 267 | 268 | if (_.isUndefined(this.$attributes)) { 269 | this.$attributes = {}; 270 | } 271 | 272 | unset = options.unset; 273 | 274 | for (attr in attrs) { 275 | if (unset && this.$attributes.hasOwnProperty(attr)) { 276 | delete this.$attributes[attr]; 277 | } else if (!unset && !this.$attributes[attr]) { 278 | defineProperty.call(this, attr); 279 | } 280 | } 281 | 282 | return this; 283 | }, 284 | 285 | /** 286 | * @name $setStatus 287 | * @description 288 | * Update model status on `$status` 289 | * 290 | * @param {Object} attributes Set one or multiple statuses 291 | * @param {Object} options Options 292 | */ 293 | $setStatus: function(key, value, options) { 294 | var attr, attrs; 295 | 296 | if (_.isUndefined(key)) { 297 | return this; 298 | } 299 | 300 | if (_.isObject(key)) { 301 | attrs = key; 302 | options = value; 303 | } else { 304 | (attrs = {})[key] = value; 305 | } 306 | 307 | options = options || {}; 308 | 309 | for (attr in this.$status) { 310 | if (attrs.hasOwnProperty(attr) && _.isBoolean(attrs[attr])) { 311 | this.$status[attr] = attrs[attr]; 312 | } 313 | } 314 | }, 315 | 316 | $removeBinding: function(attr, options) { 317 | return this.$setBinding(attr, void 0, _.extend({}, options, {unset: true})); 318 | } 319 | }); 320 | }]). 321 | 322 | /** 323 | * @name NgBackboneCollection 324 | * @description 325 | * Base NgBackbone collection extends Backbone.collection by adding additonal properties and functions, such as `$models` and `$status`. 326 | 327 | * Similar to NgBackboneModel, in rare cases where you may want to override the constructor, you should invoke NgBackboneCollection in the end. 328 | * ```javascript 329 | * var SampleCollection = NgBackboneCollection.extend({ 330 | * constructor: function(models, options) { 331 | * this.allSamples = false; 332 | * 333 | * NgBackboneCollection.apply(this, arguments); 334 | * } 335 | * }); 336 | * ``` 337 | * 338 | * The `$models` property creates a one-way binding to collection `models` which is the Javascript array of models. Application can only access the array with `$models` but will not be able to modify it. 339 | * HTML: 340 | * ```html 341 | * 344 | * ``` 345 | * 346 | * Javascript: 347 | * ``` 348 | * $scope.users = new Users(); 349 | * $scope.users.fetch(); 350 | * ``` 351 | * 352 | * The `$status` property is the hash containing collection and its models sync state. Since `$status` updates using Backbone event, passing `{silent: true}` will prevent `$status` from updating. `$status` contains four properties, including: 353 | * - `deleting`: Set to true when one of its models is getting destroyed (HTTP `DELETE` request) 354 | * - `loading`: Set to true when fetching collection data from server (HTTP `GET` request) 355 | * - `saving`: Set to true when creating or updating one of its models (HTTP `POST` or `PUT` request) 356 | * - `syncing`: Set to true whenever a collection has started a request to the server 357 | * 358 | * HTML: 359 | * ```html 360 | * 364 | * ``` 365 | * 366 | * Javascript: 367 | * ``` 368 | * $scope.users = new Users(); 369 | * $scope.users.fetch(); 370 | * ``` 371 | */ 372 | factory('NgBackboneCollection', ['Backbone', 'NgBackboneModel', function(Backbone, NgBackboneModel) { 373 | return Backbone.Collection.extend({ 374 | model: NgBackboneModel, 375 | 376 | constructor: function NgBackboneCollection() { 377 | var self = this; 378 | 379 | // Initialize status object 380 | this.$status = { 381 | deleting: false, 382 | loading: false, 383 | saving: false, 384 | syncing: false 385 | }; 386 | 387 | this.on('request', function(model, xhr, options) { 388 | this.$setStatus({ 389 | deleting: (options.method === 'DELETE'), 390 | loading: (options.method === 'GET'), 391 | saving: (options.method === 'POST' || options.method === 'PUT'), 392 | syncing: true 393 | }); 394 | }); 395 | 396 | this.on('sync error', this.$resetStatus); 397 | 398 | // For clearing status when destroy model on collection 399 | this.on('destroy', this.$resetStatus); 400 | 401 | Object.defineProperty(this, '$models', { 402 | enumerable: true, 403 | get: function() { 404 | return self.models; 405 | } 406 | }); 407 | 408 | Backbone.Collection.apply(this, arguments); 409 | }, 410 | 411 | /** 412 | * @name $setStatus 413 | * @description 414 | * Update collection status 415 | * 416 | * @param {Object} attributes Set on or multiple statuses 417 | * @param {Object} options Options 418 | */ 419 | $setStatus: function(key, value, options) { 420 | var attr, attrs; 421 | 422 | if (_.isUndefined(key)) { 423 | return this; 424 | } 425 | 426 | if (_.isObject(key)) { 427 | attrs = key; 428 | options = value; 429 | } else { 430 | (attrs = {})[key] = value; 431 | } 432 | 433 | options = options || {}; 434 | 435 | for (attr in this.$status) { 436 | if (attrs.hasOwnProperty(attr) && _.isBoolean(attrs[attr])) { 437 | this.$status[attr] = attrs[attr]; 438 | } 439 | } 440 | }, 441 | 442 | /** 443 | * @name $resetStatus 444 | * @description 445 | * Reset all statuses including `deleting`, `loading`, `saving`, and `syncing` back to false 446 | */ 447 | $resetStatus: function() { 448 | return this.$setStatus({ 449 | deleting: false, 450 | loading: false, 451 | saving: false, 452 | syncing: false 453 | }); 454 | } 455 | }); 456 | }]); 457 | 458 | })(window, document); 459 | --------------------------------------------------------------------------------