├── .gitignore ├── .travis.yml ├── examples ├── package.json ├── index.js ├── index.html └── js │ └── backbone.service.js ├── package.json ├── Gruntfile.js ├── specs ├── index.html ├── vendor │ ├── sinon-chai.js │ ├── mocha.css │ ├── phantom-mocha.js │ ├── underscore.js │ └── backbone.js └── backbone.service_spec.js ├── backbone.service.min.js ├── README.md └── backbone.service.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | 5 | script: mocha-phantomjs specs/index.html 6 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.service.example", 3 | "dependencies": { 4 | "express": "^4.13.4" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | app.post('/login', function (req, res) { 5 | res.json({ auth: true }); 6 | }); 7 | 8 | app.use("/", express.static(__dirname)); 9 | 10 | app.listen(3000); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.service", 3 | "author": { 4 | "name": "Michal Kuklis", 5 | "email": "michal.kuklis@gmail.com" 6 | }, 7 | "version": "0.2.0", 8 | "analyze": false, 9 | "devDependencies": { 10 | "grunt": "latest", 11 | "grunt-contrib-uglify": "0.2.0", 12 | "grunt-contrib-jshint": "0.3.0", 13 | "mocha-phantomjs": "latest" 14 | }, 15 | "optionalDependencies": {}, 16 | "engines": { 17 | "node": "*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.initConfig({ 4 | uglify: { 5 | dist: { 6 | src: 'backbone.service.js', 7 | dest: 'backbone.service.min.js' 8 | } 9 | }, 10 | 11 | jshint: { 12 | options: { 13 | asi: true, 14 | browser: true, 15 | curly: false, 16 | eqeqeq: false, 17 | expr: true, 18 | forin: false, 19 | newcap: true, 20 | laxcomma: true, 21 | strict: false, 22 | validthis: true, 23 | globals: { 24 | "Backbone": true 25 | } 26 | } 27 | }, 28 | 29 | lint: { 30 | files: ['/*.js'] 31 | } 32 | }); 33 | 34 | grunt.loadNpmTasks('grunt-contrib-jshint'); 35 | grunt.loadNpmTasks('grunt-contrib-uglify'); 36 | grunt.registerTask('default', ['jshint', 'uglify']); 37 | } 38 | -------------------------------------------------------------------------------- /specs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /backbone.service.min.js: -------------------------------------------------------------------------------- 1 | (function(t){"object"==typeof exports?module.exports=t(require("underscore"),require("backbone")):"function"==typeof define&&define.amd?define(["underscore","backbone"],t):t(_,Backbone)})(function(t,e){"use strict";function r(e){this.options=e||{},this.targets=s(this.options.targets||{}),t(this.targets).each(this.createMethod,this)}function s(e){var r;return t(e).map(function(e,s){return r={name:s,path:e,method:"GET"},t.isArray(e)&&t.extend(r,{path:e[0],method:e[1]}),r})}function n(e,r){var s=[],n=e.replace(/\{([^{}]*)\}/g,function(t,e){return e in r?(s.push(e),r[e]):e});return t(s).each(function(t){delete r[t]}),n}function o(t){this.context=t||this,this.success=[],this.error=[]}r.prototype.createMethod=function(t){var r,s;this[t.name]=function(n,c){return r=new o(this),c=this.createOptions(r,t,n,c),s=i[t.method.toUpperCase()],e.sync(s,this,c),r}},r.prototype.createOptions=function(e,r,s,o){var i=this,c=t.result(this.options,"url")+n(r.path,s);return o||(o={}),t.extend({url:c,data:s,success:function(t){o.success&&o.success.call(i,t),e.resolve(t)},error:function(t,r,s){o.error&&o.error.apply(i,[s,t]),e.reject(s,t)}},o)};var i={POST:"create",PUT:"update",DELETE:"delete",GET:"read"};t.extend(r.prototype,e.Events),e.Service=r,o.prototype={constructor:o,then:function(t,e){return t&&(this.resolved?t.apply(this.context,this.resolved):this.success.push(t)),e&&(this.rejected?e.apply(this.context,this.rejected):this.error.push(e)),this},resolve:function(){var t;for(this.resolved=arguments,this.error=[];t=this.success.shift();)t.apply(this.context,this.resolved)},reject:function(){var t;for(this.rejected=arguments,this.success=[];t=this.error.shift();)t.apply(this.context,this.rejected)}}}); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Backbone.Service 2 | ================ 3 | [](http://travis-ci.org/mkuklis/backbone.service) 4 | 5 | Backbone.Service aims to help with the cases when restul API is not an option. 6 | 7 | ## Install 8 | 9 | ````javascript 10 | 11 | ```` 12 | 13 | ## Usage 14 | 15 | You can use backbone.service as a standalone object or extend backbone model or collection. 16 | 17 | ````javascript 18 | 19 | // define server targets / endpoints 20 | var targets = { 21 | login: ["/login", "post"], 22 | signup: ["/signup", "post"], 23 | logout: ["/logout", "get"], 24 | search: "/search" // defaults to get 25 | resetPassword: ["/resetpassword", "post"], 26 | updateSettings: ["/updateSettings", "post"] 27 | }; 28 | 29 | // standalone service 30 | var service = new Backbone.Service({ url: "http://localhost:5000", targets: targets }); 31 | 32 | // extend backbone model 33 | var User = Backbone.Model.extend(service); 34 | 35 | ```` 36 | 37 | Each target passed to Backbone.Service becomes a method on the model or collection. 38 | 39 | User model has now access to new methods: `login`, `signup`, `logout`, `search`, `resetPassword`, `updateSettings`. 40 | Each new method takes two arguments: `data` and `options`. 41 | 42 | You can use it like this: 43 | 44 | ````javascript 45 | 46 | var user = new User(); 47 | user.login({ username: 'bob', password: 'secret' }); 48 | 49 | ```` 50 | 51 | ## Promises / Callbacks 52 | 53 | Backbone.service comes with a simple implementation of promises. You can use them like this: 54 | 55 | ````javascript 56 | 57 | user.updateSettings(settings).then(function (res) { 58 | // do something after successful update 59 | }, function (err, res) { 60 | // do something in case of an error 61 | }); 62 | 63 | 64 | ```` 65 | 66 | Callbacks are still supported. You can pass them as a second argument in your calls: 67 | 68 | ````javascript 69 | user.updateSettings(settings, { 70 | success: function (res) { 71 | // do something after successful update 72 | }, 73 | error: function (err, res) { 74 | // do something in case of an error 75 | } 76 | }); 77 | 78 | ```` 79 | 80 | ##Contributors 81 | 82 | * [@mkuklis](http://github.com/mkuklis) 83 | * [@scttnlsn](http://github.com/scttnlsn) 84 | 85 | ##License: 86 |87 | (The MIT License) 88 | 89 | Copyright (c) 2012 Michal Kuklis 90 | 91 |92 | -------------------------------------------------------------------------------- /backbone.service.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | if (typeof exports === 'object') { 3 | module.exports = factory(require('underscore'), require('backbone')); 4 | } else if (typeof define === 'function' && define.amd) { 5 | define(['underscore', 'backbone'], factory); 6 | } else { 7 | factory(_, Backbone); 8 | } 9 | })(function (_, Backbone) { 10 | 11 | "use strict"; 12 | 13 | function Service(options) { 14 | this.options = options || {}; 15 | this.targets = parseTargets(this.options.targets || {}); 16 | _(this.targets).each(this.createMethod, this); 17 | } 18 | 19 | Service.prototype.createMethod = function (target) { 20 | var promise, method; 21 | 22 | this[target.name] = function (data, options) { 23 | promise = new Promise(this); 24 | options = this.createOptions(promise, target, data, options); 25 | method = methodMap[target.method.toUpperCase()]; 26 | Backbone.sync(method, this, options); 27 | 28 | return promise; 29 | } 30 | } 31 | 32 | Service.prototype.createOptions = function (promise, target, data, options) { 33 | var self = this; 34 | var url = _.result(this.options, 'url') + evaluate(target.path, data); 35 | 36 | options || (options = {}); 37 | 38 | return _.extend({ 39 | url: url, 40 | data: data, 41 | success: function (resp, status, xhr) { 42 | options.success && options.success.call(self, resp); 43 | promise.resolve(resp); 44 | }, 45 | error: function (xhr, status, error) { 46 | options.error && options.error.apply(self, [error, xhr]); 47 | promise.reject(error, xhr); 48 | } 49 | }, options); 50 | }; 51 | 52 | var methodMap = { 53 | 'POST': 'create', 54 | 'PUT': 'update', 55 | 'DELETE': 'delete', 56 | 'GET': 'read' 57 | }; 58 | 59 | function parseTargets(targets) { 60 | var target; 61 | 62 | return _(targets).map(function (props, name) { 63 | target = { name: name, path: props, method: "GET" }; 64 | 65 | if (_.isArray(props)) { 66 | _.extend(target, { path: props[0], method: props[1] }); 67 | } 68 | 69 | return target; 70 | }); 71 | } 72 | 73 | function evaluate(template, data) { 74 | var mapped = []; 75 | var result = template.replace(/\{([^{}]*)\}/g, function (ignore, key) { 76 | if (key in data) { 77 | mapped.push(key); 78 | return data[key]; 79 | } 80 | return key; 81 | }); 82 | 83 | _(mapped).each(function (key) { 84 | delete data[key]; 85 | }); 86 | 87 | return result; 88 | } 89 | 90 | _.extend(Service.prototype, Backbone.Events); 91 | Backbone.Service = Service; 92 | 93 | // simple promise implementation 94 | function Promise(context) { 95 | this.context = context || this; 96 | this.success = []; 97 | this.error = []; 98 | } 99 | 100 | Promise.prototype = { 101 | constructor: Promise, 102 | 103 | then: function (success, error) { 104 | if (success) { 105 | if (this.resolved) { 106 | success.apply(this.context, this.resolved); 107 | } 108 | else { 109 | this.success.push(success); 110 | } 111 | } 112 | 113 | if (error) { 114 | if (this.rejected) { 115 | error.apply(this.context, this.rejected); 116 | } 117 | else { 118 | this.error.push(error); 119 | } 120 | } 121 | 122 | return this; 123 | }, 124 | 125 | resolve: function () { 126 | var callback; 127 | 128 | this.resolved = arguments; 129 | this.error = []; 130 | 131 | while (callback = this.success.shift()) { 132 | callback.apply(this.context, this.resolved); 133 | } 134 | }, 135 | 136 | reject: function () { 137 | var callback; 138 | 139 | this.rejected = arguments; 140 | this.success = []; 141 | 142 | while (callback = this.error.shift()) { 143 | callback.apply(this.context, this.rejected); 144 | } 145 | } 146 | }; 147 | }) 148 | //(Backbone); 149 | -------------------------------------------------------------------------------- /examples/js/backbone.service.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | if (typeof exports === 'object') { 3 | module.exports = factory(require('underscore'), require('backbone')); 4 | } else if (typeof define === 'function' && define.amd) { 5 | define(['underscore', 'backbone'], factory); 6 | } else { 7 | factory(_, Backbone); 8 | } 9 | })(function (_, Backbone) { 10 | 11 | "use strict"; 12 | 13 | function Service(options) { 14 | this.options = options || {}; 15 | this.targets = parseTargets(this.options.targets || {}); 16 | _(this.targets).each(this.createMethod, this); 17 | } 18 | 19 | Service.prototype.createMethod = function (target) { 20 | var promise, method; 21 | 22 | this[target.name] = function (data, options) { 23 | promise = new Promise(this); 24 | options = this.createOptions(promise, target, data, options); 25 | method = methodMap[target.method.toUpperCase()]; 26 | Backbone.sync(method, this, options); 27 | 28 | return promise; 29 | } 30 | } 31 | 32 | Service.prototype.createOptions = function (promise, target, data, options) { 33 | var self = this; 34 | var url = _.result(this.options, 'url') + evaluate(target.path, data); 35 | 36 | options || (options = {}); 37 | 38 | return _.extend({ 39 | url: url, 40 | data: data, 41 | success: function (resp, status, xhr) { 42 | options.success && options.success.call(self, resp); 43 | promise.resolve(resp); 44 | }, 45 | error: function (xhr, status, error) { 46 | options.error && options.error.apply(self, [error, xhr]); 47 | promise.reject(error, xhr); 48 | } 49 | }, options); 50 | }; 51 | 52 | var methodMap = { 53 | 'POST': 'create', 54 | 'PUT': 'update', 55 | 'DELETE': 'delete', 56 | 'GET': 'read' 57 | }; 58 | 59 | function parseTargets(targets) { 60 | var target; 61 | 62 | return _(targets).map(function (props, name) { 63 | target = { name: name, path: props, method: "GET" }; 64 | 65 | if (_.isArray(props)) { 66 | _.extend(target, { path: props[0], method: props[1] }); 67 | } 68 | 69 | return target; 70 | }); 71 | } 72 | 73 | function evaluate(template, data) { 74 | var mapped = []; 75 | var result = template.replace(/\{([^{}]*)\}/g, function (ignore, key) { 76 | if (key in data) { 77 | mapped.push(key); 78 | return data[key]; 79 | } 80 | return key; 81 | }); 82 | 83 | _(mapped).each(function (key) { 84 | delete data[key]; 85 | }); 86 | 87 | return result; 88 | } 89 | 90 | _.extend(Service.prototype, Backbone.Events); 91 | Backbone.Service = Service; 92 | 93 | // simple promise implementation 94 | function Promise(context) { 95 | this.context = context || this; 96 | this.success = []; 97 | this.error = []; 98 | } 99 | 100 | Promise.prototype = { 101 | constructor: Promise, 102 | 103 | then: function (success, error) { 104 | if (success) { 105 | if (this.resolved) { 106 | success.apply(this.context, this.resolved); 107 | } 108 | else { 109 | this.success.push(success); 110 | } 111 | } 112 | 113 | if (error) { 114 | if (this.rejected) { 115 | error.apply(this.context, this.rejected); 116 | } 117 | else { 118 | this.error.push(error); 119 | } 120 | } 121 | 122 | return this; 123 | }, 124 | 125 | resolve: function () { 126 | var callback; 127 | 128 | this.resolved = arguments; 129 | this.error = []; 130 | 131 | while (callback = this.success.shift()) { 132 | callback.apply(this.context, this.resolved); 133 | } 134 | }, 135 | 136 | reject: function () { 137 | var callback; 138 | 139 | this.rejected = arguments; 140 | this.success = []; 141 | 142 | while (callback = this.error.shift()) { 143 | callback.apply(this.context, this.rejected); 144 | } 145 | } 146 | }; 147 | }) 148 | //(Backbone); 149 | -------------------------------------------------------------------------------- /specs/vendor/sinon-chai.js: -------------------------------------------------------------------------------- 1 | (function (sinonChai) { 2 | "use strict"; 3 | 4 | // Module systems magic dance. 5 | 6 | if (typeof require === "function" && typeof exports === "object" && typeof module === "object") { 7 | // NodeJS 8 | module.exports = sinonChai; 9 | } else if (typeof define === "function" && define.amd) { 10 | // AMD 11 | define(function () { 12 | return sinonChai; 13 | }); 14 | } else { 15 | // Other environment (usually