├── .gitignore ├── bower.json ├── .travis.yml ├── LICENSE ├── package.json ├── README.md ├── karma.conf.js ├── backbone.nativeajax.js └── test └── ajax.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | reports 3 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.nativeajax", 3 | "version": "0.4.4", 4 | "homepage": "https://github.com/akre54/backbone.nativeajax", 5 | "author": "Adam Krebs ", 6 | "description": "A Backbone.Ajax function powered by native XHR methods", 7 | "main": "backbone.nativeajax.js", 8 | "keywords": [ 9 | "Backbone", 10 | "ajax", 11 | "plugin", 12 | "native", 13 | "xhr" 14 | ], 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "iojs" 5 | before_script: 6 | - export CHROME_BIN=chromium-browser 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | deploy: 10 | api_key: 11 | secure: BSNCzL7RL4Qd09kVIannM4ufZZHzNYFLh8SzJPeiMOKWvyNLtKhpGPLPUn+GSsds3y+S3x6jcRWVasV4jqsQJiFkr8ad+iV5D7TFUmap1K6ckdD7UnpAVVdVBaT6lPX8QTpO6/qKyNz4wZoJFAso88XCkpRXBD0MhJiJp7f+Wb8= 12 | email: amk528@cs.nyu.edu 13 | provider: npm 14 | notifications: 15 | email: true 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Adam Krebs 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.nativeajax", 3 | "version": "0.4.4", 4 | "author": "Adam Krebs ", 5 | "description": "A Backbone.Ajax function powered by native XHR methods", 6 | "license": "MIT", 7 | "main": "backbone.nativeajax.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/akre54/backbone.nativeajax" 11 | }, 12 | "engines": { 13 | "node": "*", 14 | "npm": "*" 15 | }, 16 | "scripts": { 17 | "test": "karma start", 18 | "release": "npm run release-patch", 19 | "release-patch": "git checkout master && npm version patch -m 'Backbone.NativeAjax %s' && git push origin master --tags && npm publish", 20 | "release-minor": "git checkout master && npm version minor -m 'Backbone.NativeAjax %s' && git push origin master --tags && npm publish", 21 | "release-major": "git checkout master && npm version major -m 'Backbone.NativeAjax %s' && git push origin master --tags && npm publish" 22 | }, 23 | "devDependencies": { 24 | "chai": "^3.0.0", 25 | "karma": "^0.13.22", 26 | "karma-chrome-launcher": "^0.2.2", 27 | "karma-cli": "0.1.2", 28 | "karma-firefox-launcher": "^0.1.6", 29 | "karma-junit-reporter": "^0.4.0", 30 | "karma-mocha": "^0.2.2", 31 | "karma-sourcemap-loader": "^0.3.5", 32 | "karma-webpack": "^1.5.1", 33 | "mocha": "^2.2.5", 34 | "mocha-loader": "^0.7.1", 35 | "mock-xhr": "^0.1.0", 36 | "native-promise-only": "^0.8.1", 37 | "sinon": "git://github.com/cjohansen/Sinon.JS#b672042043517b9f84e14ed0fb8265126168778a", 38 | "sinon-chai": "^2.8.0", 39 | "webpack": "^1.9.11", 40 | "webpack-dev-server": "^1.9.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Backbone.NativeAjax 2 | =================== 3 | 4 | A drop-in replacement for Backbone.Ajax that uses only native XMLHttpRequest 5 | methods for sync. It has no dependency on jQuery. 6 | 7 | You might consider using the [window.fetch pollyfill](https://github.com/github/fetch) 8 | coupled with [a simple plugin for Backbone](https://gist.github.com/akre54/9891fc85ff46afd85814) 9 | instead of this project if you need better Promise or header support. 10 | 11 | To Use: 12 | ------- 13 | Load Backbone.NativeAjax with your favorite module loader or add as a script 14 | tag after you have loaded Backbone in the page. 15 | 16 | If loading with AMD or CommonJS you should set `Backbone.ajax` yourself: 17 | 18 | ```js 19 | // AMD 20 | define(['backbone', 'nativeajax'], function(Backbone, ajax) { 21 | Backbone.ajax = ajax; 22 | }); 23 | 24 | // CommonJS 25 | var Backbone = require('backbone'); 26 | Backbone.ajax = require('backbone.nativeajax'); 27 | ``` 28 | 29 | Features: 30 | --------- 31 | * Accepts `success` and `error` callbacks 32 | * Set headers with a `headers` object 33 | * `beforeSend` 34 | 35 | Requirements: 36 | ------------- 37 | NativeAjax uses XMLHttpRequest which is supported in modern browsers. 38 | See the [compatibility chart](http://caniuse.com/#search=XMLHttpRequest). 39 | 40 | It also makes use of `Function.prototype.bind` which can be easily pollyfilled 41 | in older environments. 42 | 43 | Set a `Promise` object on the global or on `Backbone.ajax` to return a promise. 44 | 45 | Notes: 46 | ------ 47 | * The ajax function accepts a `success` and `error` callbacks. To return 48 | a promise object, set a global `Promise` or `Backbone.ajax.Promise`. 49 | * Unlike jQuery, we don't automatically set the `X-Requested-With` header, so 50 | things like Rails' `request.xhr?` will break. If you need it, you can pass it 51 | in yourself. 52 | 53 | Uses code from [Exoskeleton](https://github.com/paulmillr/exoskeleton). See that 54 | project for more information and other features. 55 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Set `DEBUG=true karma start` to enable some debugging options. 2 | var DEBUG = process.env.DEBUG; 3 | 4 | module.exports = function(config) { 5 | 6 | var configuration = { 7 | basePath: '', 8 | 9 | // The test runner name. Change to jasmine, jest, whatever. Must load 10 | // a matching karma plugin later in the plugins array. 11 | frameworks: ['mocha'], 12 | 13 | // Can be a single file, array, or glob of files. 14 | files: [ 15 | 'test/ajax.js' 16 | ], 17 | 18 | // Any files to ignore. 19 | exclude: [], 20 | 21 | // Run webpack to give us module loader support, sourcemap to give us correct 22 | // line numbers for errors. 23 | preprocessors: { 24 | 'test/ajax.js': ['webpack', 'sourcemap'] 25 | }, 26 | 27 | // Plugins loaded by karma. 28 | plugins: [ 29 | require('karma-webpack'), 30 | require('karma-mocha'), 31 | require('karma-chrome-launcher'), 32 | require('karma-firefox-launcher'), 33 | require('karma-junit-reporter'), 34 | require('karma-sourcemap-loader') 35 | ], 36 | 37 | // Any options to webpack. 38 | webpack: {}, 39 | 40 | webpackServer: { 41 | stats: { 42 | colors: true 43 | } 44 | }, 45 | 46 | // test results reporter to use 47 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 48 | reporters: ['progress', 'junit'], 49 | 50 | junitReporter: { 51 | outputFile: 'reports/karma-test-results.xml', 52 | suite: '', 53 | }, 54 | 55 | // Web server port. 56 | port: 9876, 57 | 58 | // Enable / disable colors in the output (reporters and logs). 59 | colors: true, 60 | 61 | // Level of logging 62 | // Possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 63 | logLevel: config.LOG_DEBUG, 64 | 65 | // Enable / disable watching file and executing tests whenever any file changes 66 | autoWatch: DEBUG, 67 | 68 | // Start these browsers, currently available: 69 | // - Chrome 70 | // - ChromeCanary 71 | // - Firefox 72 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 73 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 74 | // - PhantomJS 75 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 76 | browsers: ['Firefox', 'Chrome'], 77 | 78 | // If browser does not capture in given timeout [ms], kill it 79 | captureTimeout: 60000, 80 | 81 | // Continuous Integration mode 82 | // if true, it capture browsers, run tests and exit 83 | singleRun: !DEBUG, 84 | 85 | // See http://stackoverflow.com/a/27873086/1517919 86 | customLaunchers: { 87 | Chrome_travis_ci: { 88 | base: 'Chrome', 89 | flags: ['--no-sandbox'] 90 | } 91 | } 92 | }; 93 | 94 | if (process.env.TRAVIS){ 95 | configuration.browsers = ['Firefox', 'Chrome_travis_ci']; 96 | } 97 | 98 | config.set(configuration); 99 | }; 100 | -------------------------------------------------------------------------------- /backbone.nativeajax.js: -------------------------------------------------------------------------------- 1 | // Backbone.NativeAjax.js 0.4.4 2 | // --------------- 3 | 4 | // (c) 2016 Adam Krebs, Paul Miller, Exoskeleton Project 5 | // Backbone.NativeAjax may be freely distributed under the MIT license. 6 | // For all details and documentation: 7 | // https://github.com/akre54/Backbone.NativeAjax 8 | 9 | (function (factory) { 10 | if (typeof define === 'function' && define.amd) { define(factory); 11 | } else if (typeof exports === 'object') { module.exports = factory(); 12 | } else { Backbone.ajax = factory(); } 13 | }(function() { 14 | // Make an AJAX request to the server. 15 | // Usage: 16 | // var req = Backbone.ajax({url: 'url', type: 'PATCH', data: 'data'}); 17 | // req.then(..., ...) // if Promise is set 18 | var ajax = (function() { 19 | var xmlRe = /^(?:application|text)\/xml/; 20 | var jsonRe = /^application\/json/; 21 | 22 | var getData = function(accepts, xhr) { 23 | if (accepts == null) accepts = xhr.getResponseHeader('content-type'); 24 | if (xmlRe.test(accepts)) { 25 | return xhr.responseXML; 26 | } else if (jsonRe.test(accepts) && xhr.responseText !== '') { 27 | return JSON.parse(xhr.responseText); 28 | } else { 29 | return xhr.responseText; 30 | } 31 | }; 32 | 33 | var isValid = function(xhr) { 34 | return (xhr.status >= 200 && xhr.status < 300) || 35 | (xhr.status === 304) || 36 | (xhr.status === 0 && window.location.protocol === 'file:') 37 | }; 38 | 39 | var end = function(xhr, options, promise, resolve, reject) { 40 | return function() { 41 | proxyPromise(xhr, promise); 42 | 43 | if (xhr.readyState !== 4) return; 44 | 45 | var status = xhr.status; 46 | var data = getData(options.headers && options.headers.Accept, xhr); 47 | 48 | // Check for validity. 49 | if (isValid(xhr)) { 50 | if (options.success) options.success(data); 51 | if (resolve) resolve(data); 52 | } else { 53 | var error = new Error('Server responded with a status of ' + status); 54 | if (options.error) options.error(xhr, status, error); 55 | if (reject) reject(xhr); 56 | } 57 | } 58 | }; 59 | 60 | var proxyPromise = function(xhr, promise) { 61 | if (!promise) return; 62 | 63 | var props = ['readyState', 'status', 'statusText', 'responseText', 64 | 'responseXML', 'setRequestHeader', 'getAllResponseHeaders', 65 | 'getResponseHeader', 'statusCode', 'abort']; 66 | 67 | for (var i = 0; i < props.length; i++) { 68 | var prop = props[i]; 69 | try { 70 | promise[prop] = typeof xhr[prop] === 'function' ? 71 | xhr[prop].bind(xhr) : 72 | xhr[prop]; 73 | } catch (e) { 74 | console.log(e); 75 | } 76 | } 77 | return promise; 78 | } 79 | 80 | return function(options) { 81 | if (options == null) throw new Error('You must provide options'); 82 | if (options.type == null) options.type = 'GET'; 83 | 84 | var resolve, reject, xhr = new XMLHttpRequest(); 85 | var PromiseFn = ajax.Promise || (typeof Promise !== 'undefined' && Promise); 86 | var promise = PromiseFn && new PromiseFn(function(res, rej) { 87 | resolve = res; 88 | reject = rej; 89 | }); 90 | 91 | if (options.contentType) { 92 | if (options.headers == null) options.headers = {}; 93 | options.headers['Content-Type'] = options.contentType; 94 | } 95 | 96 | // Stringify GET query params. 97 | if (options.type === 'GET' && typeof options.data === 'object') { 98 | var query = ''; 99 | var stringifyKeyValuePair = function(key, value) { 100 | return value == null ? '' : 101 | '&' + encodeURIComponent(key) + 102 | '=' + encodeURIComponent(value); 103 | }; 104 | for (var key in options.data) { 105 | query += stringifyKeyValuePair(key, options.data[key]); 106 | } 107 | 108 | if (query) { 109 | var sep = (options.url.indexOf('?') === -1) ? '?' : '&'; 110 | options.url += sep + query.substring(1); 111 | } 112 | } 113 | 114 | xhr.onreadystatechange = end(xhr, options, promise, resolve, reject); 115 | xhr.open(options.type, options.url, options.async !== false); 116 | 117 | if(!(options.headers && options.headers.Accept)) { 118 | var allTypes = "*/".concat("*"); 119 | var xhrAccepts = { 120 | "*": allTypes, 121 | text: "text/plain", 122 | html: "text/html", 123 | xml: "application/xml, text/xml", 124 | json: "application/json, text/javascript" 125 | }; 126 | xhr.setRequestHeader( 127 | "Accept", 128 | options.dataType && xhrAccepts[options.dataType] ? 129 | xhrAccepts[options.dataType] + (options.dataType !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : 130 | xhrAccepts["*"] 131 | ); 132 | } 133 | 134 | if (options.headers) for (var key in options.headers) { 135 | xhr.setRequestHeader(key, options.headers[key]); 136 | } 137 | if (options.beforeSend) options.beforeSend(xhr); 138 | xhr.send(options.data); 139 | 140 | options.originalXhr = xhr; 141 | 142 | proxyPromise(xhr, promise); 143 | 144 | return promise ? promise : xhr; 145 | }; 146 | })(); 147 | return ajax; 148 | })); 149 | -------------------------------------------------------------------------------- /test/ajax.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | sinon = require('sinon'), 3 | Promise = require('native-promise-only'); 4 | 5 | chai.use(require('sinon-chai')); 6 | 7 | var expect = chai.expect; 8 | 9 | var root = typeof window != 'undefined' ? window : global; 10 | 11 | var XMLHttpRequest = root.XMLHttpRequest = require("mock-xhr").request; 12 | 13 | var open = sinon.spy(XMLHttpRequest.prototype, 'open'); 14 | var setRequestHeader = sinon.spy(XMLHttpRequest.prototype, 'setRequestHeader'); 15 | 16 | var ajax = require('..'); 17 | 18 | describe('Backbone.NativeAjax', function() { 19 | 20 | afterEach(function() { 21 | open.reset(); 22 | setRequestHeader.reset(); 23 | }); 24 | 25 | 26 | describe('creating a request', function() { 27 | it('should throw when no options object is passed', function() { 28 | expect(ajax).to.throw(Error, /You must provide options/); 29 | }); 30 | it('should pass the url to XHR', function() { 31 | ajax({url: 'test'}); 32 | expect(open).to.have.been.calledOnce; 33 | expect(open).to.have.been.calledWithExactly('GET', 'test', true); 34 | }); 35 | it('should stringify GET data when present', function() { 36 | ajax({url: 'test', data: {a: 1, b: 2}}); 37 | expect(open).to.have.been.calledOnce; 38 | expect(open).to.have.been.calledWithExactly('GET', 'test?a=1&b=2', true); 39 | }); 40 | it('should append GET data to the URL when querystring already exists', function() { 41 | ajax({url: 'test?a=1', data: {b: 2}}); 42 | expect(open).to.have.been.calledOnce; 43 | expect(open).to.have.been.calledWithExactly('GET', 'test?a=1&b=2', true); 44 | }); 45 | }); 46 | 47 | describe('headers', function() { 48 | it('should set headers if none passed in', function() { 49 | ajax({url: 'test', dataType: 'json'}); 50 | 51 | expect(setRequestHeader).to.have.been.calledOnce; 52 | expect(setRequestHeader).to.have.been.calledWithExactly('Accept', "application/json, text/javascript, */*; q=0.01") 53 | }); 54 | 55 | it('should use headers if passed in', function() { 56 | ajax({url: 'test', dataType: '*', headers: {a: 1, b: 2}}); 57 | expect(setRequestHeader).to.have.been.calledThrice; 58 | // expect(setRequestHeader.firstCall).to.have.been.calledWithExactly() 59 | }); 60 | 61 | it('should use custom accept header if passed in', function() { 62 | ajax({url: 'test', dataType: 'json', headers: {Accept: 'application/xml, application/json;q=0.9'}}); 63 | 64 | expect(setRequestHeader).to.have.been.calledOnce; 65 | expect(setRequestHeader).to.have.been.calledWithExactly('Accept', 'application/xml, application/json;q=0.9'); 66 | }); 67 | }); 68 | 69 | describe('finishing a request', function() { 70 | it('should invoke the success callback on complete', function() { 71 | var success = sinon.mock(), error = sinon.mock(); 72 | 73 | var options = {url: 'test', success: success, error: error}; 74 | var req = ajax(options); 75 | var xhr = options.originalXhr; 76 | 77 | xhr.receive(200, {id: 1}); 78 | 79 | expect(success).to.have.been.calledOnce; 80 | expect(success).to.have.been.calledWithExactly({id: 1}); 81 | expect(error).not.to.have.been.called; 82 | }); 83 | 84 | it('should invoke the error callback on error', function() { 85 | var success = sinon.mock(), error = sinon.mock(); 86 | 87 | var options = {url: 'test', success: success, error: error}; 88 | var req = ajax(options); 89 | var xhr = options.originalXhr; 90 | 91 | xhr.err(); 92 | 93 | expect(error).to.have.been.calledOnce; 94 | expect(error).to.have.been.calledWithExactly(xhr, 0, sinon.match.instanceOf(Error)); 95 | expect(success).not.to.have.been.called; 96 | }); 97 | }); 98 | 99 | describe('Promise', function() { 100 | afterEach(function() { 101 | delete root.Promise; 102 | delete ajax.Promise; 103 | }); 104 | 105 | it('should respect a global Promise constructor if one set', function() { 106 | root.Promise = Promise; 107 | expect(ajax({url: 'test'})).to.be.an.instanceof(Promise); 108 | }); 109 | 110 | it('should prefer Backbone.ajax.Promise over global', function() { 111 | root.Promise = function() {}; 112 | ajax.Promise = Promise; 113 | var req = ajax({url: 'test'}); 114 | expect(req).to.be.an.instanceof(Promise); 115 | expect(req).not.to.be.an.instanceof(root.Promise); 116 | }); 117 | 118 | describe('with a Promise set', function() { 119 | 120 | var xhr, req; 121 | beforeEach(function() { 122 | ajax.Promise = Promise; 123 | var options = {url: 'test'}; 124 | req = ajax(options); 125 | xhr = options.originalXhr; 126 | }); 127 | 128 | it('should resolve the deferred on complete', function(done) { 129 | req.then(function(resp) { 130 | expect(resp).to.deep.equal({id: 1}); 131 | done() 132 | }).catch(function(err) { 133 | throw new Error('Should not have been called'); 134 | }); 135 | 136 | // specific to mock-xhr 137 | xhr.receive(200, {id: 1}); 138 | }); 139 | 140 | it('should reject the deferred on error', function(done) { 141 | req.then(function() { 142 | throw new Error('Should not have been called'); 143 | }).catch(function(err) { 144 | expect(err).to.equal(xhr); 145 | done(); 146 | }) 147 | 148 | // specific to mock-xhr 149 | xhr.err(); 150 | }); 151 | 152 | }); 153 | }); 154 | 155 | describe('XHR', function() { 156 | 157 | var req, xhr; 158 | beforeEach(function() { 159 | var options = {url: 'test'}; 160 | req = ajax(options); 161 | xhr = options.originalXhr; 162 | }); 163 | 164 | it('should expose common XHR methods and properties', function() { 165 | var props = ['readyState', 'status', 'statusText', 'responseText', 'responseXML']; 166 | var methods = ['setRequestHeader', 'getAllResponseHeaders', 'getResponseHeader', 'statusCode', 'abort']; 167 | 168 | props.forEach(function(prop) { 169 | expect(prop).to.exist; 170 | }); 171 | 172 | methods.forEach(function(method) { 173 | expect(method).to.be.a.function; 174 | }); 175 | 176 | it('should update XHR methods and properties onreadystatechange', function(done) { 177 | expect(req.status).to.equal(0); 178 | expect(req.status).to.equal(xhr.status); 179 | 180 | xhr.receive(200, {id: 1}); 181 | 182 | expect(req.status).to.equal(200); 183 | expect(req.status).to.equal(xhr.status); 184 | }); 185 | }); 186 | }); 187 | }); 188 | --------------------------------------------------------------------------------