├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist └── ajaxable.min.js ├── docs └── API.md ├── package.json ├── src ├── Ajaxable.js └── index.js ├── test ├── helper.js └── index.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | .eslintrc 4 | *.html 5 | 6 | private/ 7 | coverage/ 8 | node_modules/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.6" 4 | after_script: babel-node ./node_modules/istanbul/lib/cli cover node_modules/.bin/_mocha -- --compilers js:babel-core/register --require ./test/helper.js --recursive -R spec ./test/*.js && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Artur Arseniev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Ajaxable](https://ajaxable.js.org/) 2 | 3 | [![Build Status](https://travis-ci.org/artf/ajaxable.svg?branch=master)](https://travis-ci.org/artf/ajaxable) 4 | [![Coverage Status](https://coveralls.io/repos/github/artf/ajaxable/badge.svg?branch=master)](https://coveralls.io/github/artf/ajaxable?branch=master) 5 | 6 | This library simply takes the standard HTML forms as an input and make them send requests via AJAX keeping HTML5 validations. 7 | 8 | [Demo](https://ajaxable.js.org/) 9 | 10 | 11 | ## Installation 12 | 13 | Download the file from [here](https://cdn.rawgit.com/artf/ajaxable/master/dist/ajaxable.min.js), via `npm i ajaxable` or get it directly from the `/dist` folder 14 | 15 | 16 | ## Usage 17 | 18 | Basic 19 | 20 | ```html 21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 | 29 | 32 | ``` 33 | 34 | Listen events 35 | 36 | ```js 37 | ajaxable('#myform') 38 | .onStart(function(params) { 39 | // Make stuff before each request, eg. start 'loading animation' 40 | }) 41 | .onEnd(function(params) { 42 | // Make stuff after each request, eg. stop 'loading animation' 43 | }) 44 | .onResponse(function(res, params) { 45 | // Make stuff after on response of each request 46 | }) 47 | .onError(function(err, params) { 48 | // Make stuff on errors 49 | }); 50 | ``` 51 | The `params` argument is an object containing additional data about the specific request. For example, `el` is the form element which made the request and `activeRequests` indicates how many requests are still pending (useful with multiple forms) 52 | 53 | 54 | ## Development 55 | 56 | Clone the repository and enter inside the folder 57 | 58 | ```sh 59 | $ git clone https://github.com/artf/ajaxable.git 60 | $ cd ajaxable 61 | ``` 62 | 63 | Install it 64 | 65 | ```sh 66 | $ npm i 67 | ``` 68 | 69 | Start the dev server 70 | 71 | ```sh 72 | $ npm start 73 | ``` 74 | 75 | Build before the commit. This will also increase the patch level version of the package 76 | 77 | ```sh 78 | $ npm run build 79 | ``` 80 | 81 | 82 | ## API 83 | 84 | [API Reference here](./docs/API.md) 85 | 86 | 87 | ## Testing 88 | 89 | Run tests 90 | 91 | ```sh 92 | $ npm test 93 | ``` 94 | 95 | Run and watch tests 96 | 97 | ```sh 98 | $ npm run test:dev 99 | ``` 100 | 101 | ## Compatibility 102 | 103 | All modern browsers (IE > 9) 104 | 105 | ## License 106 | 107 | MIT 108 | -------------------------------------------------------------------------------- /dist/ajaxable.min.js: -------------------------------------------------------------------------------- 1 | /*! ajaxable - 0.2.3 */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.ajaxable=t():e.ajaxable=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}var o=n(1),i=r(o);e.exports=function(e,t){return new i["default"](e,t)}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:"";o(this,t);var r=i(this,(t.__proto__||Object.getPrototypeOf(t)).call(this));if(!e)throw new Error("The element is empty");var s={responseType:"json",headers:{}},a=n||{};for(var u in s)u in a||(a[u]=s[u]);var f="X-Requested-With";""!=a.headers[f]&&(a.headers[f]="XMLHttpRequest"),r.els=r.parseEl(e),r.opts=a;for(var c=0;c 2 | 3 | # constructor 4 | 5 | Init the form by providing the element, it can be either HTML selector or the form element (HTMLFormElement). 6 | 7 | **Parameters** 8 | 9 | - `el` **([string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [HTMLFormElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement))** 10 | - `options` **\[[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)]** Options 11 | - `options.responseType` **\[[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)]** Define the response type, eg. `json`(default), `blob`, `arraybuffer`, leave empty if undefined 12 | - `options.headers` **\[[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)]** Define custom headers 13 | 14 | **Examples** 15 | 16 | ```javascript 17 | ajaxable('form.ajaxable', { 18 | responseType: '', 19 | headers: { 20 | 'Content-Type': 'text/html; charset=UTF-8' 21 | } 22 | }); 23 | ``` 24 | 25 | # onStart 26 | 27 | Bind a callback and execute it on start of each request 28 | The callback accepts parameters object as argument 29 | 30 | **Parameters** 31 | 32 | - `clb` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** Callback function 33 | 34 | **Examples** 35 | 36 | ```javascript 37 | ajaxable('...').onStart((params) => { 38 | // do stuff 39 | }) 40 | ``` 41 | 42 | # onEnd 43 | 44 | Bind a callback and execute it on end of each request 45 | The callback accepts parameters object as argument 46 | 47 | **Parameters** 48 | 49 | - `clb` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** Callback function 50 | 51 | **Examples** 52 | 53 | ```javascript 54 | ajaxable('...').onEnd((params) => { 55 | // do stuff 56 | }) 57 | ``` 58 | 59 | # onResponse 60 | 61 | Bind a callback and execute it on response of each request 62 | The callback accepts the response and parameters as arguments 63 | 64 | **Parameters** 65 | 66 | - `clb` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** Callback function 67 | 68 | **Examples** 69 | 70 | ```javascript 71 | ajaxable('...').onResponse((res, params) => { 72 | // do stuff 73 | }) 74 | ``` 75 | 76 | # onError 77 | 78 | Bind a callback and execute it on error of each request 79 | The callback accepts the error and parameters as arguments 80 | 81 | **Parameters** 82 | 83 | - `clb` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** Callback function 84 | 85 | **Examples** 86 | 87 | ```javascript 88 | ajaxable('...').onError((err, params) => { 89 | // do stuff 90 | }) 91 | ``` 92 | 93 | # submit 94 | 95 | Submit the request 96 | 97 | **Examples** 98 | 99 | ```javascript 100 | ajaxable('...').submit(); 101 | ``` 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ajaxable", 3 | "version": "0.2.3", 4 | "description": "Make your form instantly ajaxable", 5 | "main": "dist/ajaxable.min.js", 6 | "homepage": "https://github.com/artf/ajaxable", 7 | "author": "Artur Arseniev", 8 | "license": "MIT", 9 | "bugs": { 10 | "url": "https://github.com/artf/ajaxable/issues" 11 | }, 12 | "babel": { 13 | "presets": [ 14 | "es2015" 15 | ] 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/artf/ajaxable.git" 20 | }, 21 | "keywords": [ 22 | "form", 23 | "ajax", 24 | "ajaxable", 25 | "request" 26 | ], 27 | "scripts": { 28 | "lint": "eslint src", 29 | "test": "mocha --compilers js:babel-core/register --require ./test/helper.js --recursive ./test/*.js", 30 | "test:dev": "npm test -- -R min -w", 31 | "build": "WEBPACK_ENV=prod && npm run test && npm run build:doc && npm version --no-git-tag-version patch && webpack", 32 | "build:doc": "documentation build src/index.js -o ./docs/API.md -f md", 33 | "cover": "babel-node ./node_modules/istanbul/lib/cli cover node_modules/.bin/_mocha -- --compilers js:babel-core/register --require ./test/helper.js --recursive -R spec ./test/*.js", 34 | "start": "WEBPACK_ENV=dev webpack-dev-server --progress --colors" 35 | }, 36 | "dependencies": { 37 | "tiny-emitter": "^1.1.0" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "^6.16.0", 41 | "babel-core": "^6.17.0", 42 | "babel-loader": "^6.2.5", 43 | "babel-preset-es2015": "^6.16.0", 44 | "coveralls": "^2.11.14", 45 | "documentation": "^4.0.0-beta11", 46 | "eslint": "^3.7.1", 47 | "expect": "^1.20.2", 48 | "istanbul": "^1.1.0-alpha.1", 49 | "jsdom": "^9.8.0", 50 | "mocha": "^3.1.1", 51 | "sinon": "^1.17.6", 52 | "webpack": "^1.13.2", 53 | "webpack-dev-server": "^1.16.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Ajaxable.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'tiny-emitter'; 2 | 3 | class Ajaxable extends EventEmitter{ 4 | 5 | /** 6 | * Init the form by providing the element, it can be either HTML selector or the form element (HTMLFormElement). 7 | * @param {string|HTMLFormElement} el 8 | * @param {Object} [options] Options 9 | * @param {string} [options.responseType] Define the response type, eg. `json`(default), `blob`, `arraybuffer`, leave empty if undefined 10 | * @param {Object} [options.headers] Define custom headers 11 | * @example 12 | * ajaxable('form.ajaxable', { 13 | * responseType: '', 14 | * headers: { 15 | * 'Content-Type': 'text/html; charset=UTF-8' 16 | * } 17 | * }); 18 | */ 19 | constructor(el, options = '') { 20 | super(); 21 | if(!el){ 22 | throw new Error('The element is empty'); 23 | } 24 | // Default options 25 | const defaults = { 26 | responseType: 'json', 27 | headers: {}, 28 | }; 29 | 30 | let opts = options || {}; 31 | for (let name in defaults) { 32 | if (!(name in opts)) 33 | opts[name] = defaults[name]; 34 | } 35 | 36 | // Set default X-Requested-With if not setted 37 | const reqHead = 'X-Requested-With'; 38 | if(opts.headers[reqHead] != '') 39 | opts.headers[reqHead] = 'XMLHttpRequest'; 40 | 41 | this.els = this.parseEl(el); 42 | this.opts = opts; 43 | 44 | for (let i = 0; i < this.els.length; i++) { 45 | this.bindForm(this.els[i]); 46 | } 47 | } 48 | 49 | /** 50 | * Bind a callback and execute it on start of each request 51 | * The callback accepts parameters object as argument 52 | * @param {Function} clb Callback function 53 | * @example 54 | * ajaxable('...').onStart((params) => { 55 | * // do stuff 56 | * }) 57 | */ 58 | onStart(clb){ 59 | return this.on('start', clb); 60 | } 61 | 62 | /** 63 | * Bind a callback and execute it on end of each request 64 | * The callback accepts parameters object as argument 65 | * @param {Function} clb Callback function 66 | * @example 67 | * ajaxable('...').onEnd((params) => { 68 | * // do stuff 69 | * }) 70 | */ 71 | onEnd(clb){ 72 | return this.on('end', clb); 73 | } 74 | 75 | /** 76 | * Bind a callback and execute it on response of each request 77 | * The callback accepts the response and parameters as arguments 78 | * @param {Function} clb Callback function 79 | * @example 80 | * ajaxable('...').onResponse((res, params) => { 81 | * // do stuff 82 | * }) 83 | */ 84 | onResponse(clb){ 85 | return this.on('response', clb); 86 | } 87 | 88 | /** 89 | * Bind a callback and execute it on error of each request 90 | * The callback accepts the error and parameters as arguments 91 | * @param {Function} clb Callback function 92 | * @example 93 | * ajaxable('...').onError((err, params) => { 94 | * // do stuff 95 | * }) 96 | */ 97 | onError(clb){ 98 | return this.on('error', clb); 99 | } 100 | 101 | /** 102 | * Submit the request 103 | * @example 104 | * ajaxable('...').submit(); 105 | */ 106 | submit() { 107 | for (let i = 0; i < this.els.length; i++) { 108 | this.send(this.els[i]); 109 | } 110 | } 111 | 112 | /** 113 | * Trigger form submit. 114 | * If I need to submit form programmatically and trigger 115 | * HTML5 Validation .submit() doesn't work, it's necessary 116 | * to 'click()' on a submitable element. 117 | * @param {HTMLFormElement} el Form element 118 | * @private 119 | */ 120 | send(el) { 121 | const id = '_aj_btn'; 122 | let subEl = el.querySelector('#' + id); 123 | if(!subEl){ 124 | subEl = el.appendChild(document.createElement('button')); 125 | subEl.id = id; 126 | subEl.style.display = 'none'; 127 | } 128 | subEl.click(); 129 | } 130 | 131 | /** 132 | * Parse element data and return iterable object 133 | * @param {string|HTMLFormElement} el Form/s element 134 | * @return {Array|NodeList} 135 | * @private 136 | */ 137 | parseEl(el) { 138 | if(typeof el === 'string'){ 139 | el = document.querySelectorAll(el); 140 | }if(!el.length || 141 | el instanceof window.HTMLElement){ 142 | el = [el]; 143 | } 144 | return el; 145 | } 146 | 147 | /** 148 | * Bind submit event to the form 149 | * @param {HTMLFormElement} el Form element 150 | * @private 151 | */ 152 | bindForm(el) { 153 | this.checkForm(el); 154 | const ev = 'submit'; 155 | const checkForm = (e) => { 156 | if(el.checkValidity()){ 157 | e.preventDefault(); 158 | this.sendForm(el); 159 | } 160 | }; 161 | this.removeListeners(el, ev); 162 | this.addListener(el, ev, checkForm); 163 | } 164 | 165 | /** 166 | * Send form data 167 | * @param {HTMLFormElement} el Form element 168 | * @private 169 | */ 170 | sendForm(el) { 171 | const formData = this.fetchData(el); 172 | let req = new XMLHttpRequest(); 173 | let headers = this.opts.headers; 174 | this._ar++; 175 | let params = { 176 | el, 177 | req, 178 | activeRequests: this._ar, 179 | requestData: this.fetchFormData(formData) 180 | }; 181 | this.emit('start', params); 182 | req.addEventListener("progress", (e) => 183 | this.emit('progress', e, el, req) 184 | ); 185 | req.addEventListener("load", (e) => { 186 | const toJson = this.opts.responseType == 'json'; 187 | let response = ''; 188 | try{ 189 | response = toJson ? JSON.parse(req.responseText) : req.response; 190 | }catch(err){ 191 | this.emit('error', err, params); 192 | return; 193 | } 194 | this.emit('response', response, params); 195 | }); 196 | req.addEventListener("error", (e) => 197 | this.emit('error', e, params) 198 | ); 199 | req.addEventListener("loadend", (e) => { 200 | this._ar--; 201 | params.activeRequests = this._ar; 202 | this.emit('end', params) 203 | }); 204 | req.open(el.method, el.action); 205 | 206 | // Set headers 207 | for (let head in headers){ 208 | req.setRequestHeader(head, headers[head]); 209 | } 210 | 211 | req.send(formData); 212 | } 213 | 214 | /** 215 | * Fetch data from the form element 216 | * @param {HTMLFormElement} el Form element 217 | * @return {FormData} 218 | * @private 219 | */ 220 | fetchData(el) { 221 | this.checkForm(el); 222 | let formData = new window.FormData(el); 223 | return formData; 224 | } 225 | 226 | /** 227 | * Fetch data from the FormData object 228 | * @param {FormData} fd 229 | * @return {Object} 230 | * @private 231 | */ 232 | fetchFormData(fd) { 233 | let obj = {}; 234 | if(fd.entries){ 235 | for(let pair of fd.entries()) { 236 | obj[pair[0]] = pair[1]; 237 | } 238 | } 239 | return obj; 240 | } 241 | 242 | /** 243 | * Check if the element is a valid form node 244 | * @param {HTMLFormElement} el 245 | * @private 246 | */ 247 | checkForm(el) { 248 | if(!el || !(el instanceof window.HTMLFormElement)){ 249 | let name = el.constructor.name; 250 | throw new Error(`The element is not a valid form, ${name} given`); 251 | } 252 | } 253 | 254 | /** 255 | * Add listener helper 256 | * @param {HTMLElement} node 257 | * @param {string} event 258 | * @param {function} handler 259 | * @private 260 | */ 261 | addListener(node, event, handler) { 262 | if(!(node in this._eh)) 263 | this._eh[node] = {}; 264 | if(!(event in this._eh[node])) 265 | this._eh[node][event] = []; 266 | this._eh[node][event].push(handler); 267 | node.addEventListener(event, handler); 268 | } 269 | 270 | /** 271 | * Remove listener helper 272 | * @param {HTMLElement} node 273 | * @param {string} event 274 | * @private 275 | */ 276 | removeListeners(node, event) { 277 | if(node in this._eh) { 278 | let handlers = this._eh[node]; 279 | if(event in handlers) { 280 | let eventHandlers = handlers[event]; 281 | for(let i = eventHandlers.length; i--;) { 282 | let handler = eventHandlers[i]; 283 | node.removeEventListener(event, handler); 284 | } 285 | } 286 | } 287 | } 288 | } 289 | 290 | // Define static event handler 291 | Ajaxable.prototype._eh = {}; 292 | // Define static variable which indicates active requests 293 | Ajaxable.prototype._ar = 0; 294 | 295 | export default Ajaxable; 296 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Ajaxable from './Ajaxable'; 2 | 3 | module.exports = (el, opt) => new Ajaxable(el, opt); 4 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | 3 | //const virtualConsole = jsdom.createVirtualConsole(); 4 | //virtualConsole.on('log', console.log); 5 | 6 | const doc = jsdom.jsdom(''); 7 | const win = doc.defaultView; 8 | 9 | 10 | global.document = doc; 11 | global.window = win; 12 | 13 | Object.keys(window).forEach((key) => { 14 | if (!(key in global)) { 15 | global[key] = window[key]; 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import sinon from 'sinon'; 3 | import ajaxable from './../src'; 4 | 5 | describe('Ajaxable', () => { 6 | let obj; 7 | let form; 8 | let d = document; 9 | 10 | beforeEach(() => { 11 | form = `
12 |
13 |
14 | 15 | 16 |
`; 17 | d.body.innerHTML = form; 18 | }); 19 | 20 | it('Throws error without element', () => { 21 | expect(() => { 22 | obj = ajaxable(); 23 | }).toThrow(); 24 | }); 25 | 26 | it('Init with string, single element', () => { 27 | const el = ajaxable('#form1').els; 28 | expect(el.length).toEqual(1); 29 | }); 30 | 31 | it('Init with string, multiple elements', () => { 32 | const el = ajaxable('.forms').els; 33 | expect(el.length).toEqual(3); 34 | }); 35 | 36 | it('Init with the DOM', () => { 37 | const el = ajaxable(d.getElementById('form1')).els; 38 | expect(el.length).toEqual(1); 39 | }); 40 | 41 | it('Init with the DOM which has children', () => { 42 | const el = ajaxable(d.getElementById('form3')).els; 43 | expect(el.length).toEqual(1); 44 | }); 45 | 46 | it('Throw on wrong element with checkForm', () => { 47 | obj = ajaxable('#form1'); 48 | expect(() => { 49 | obj.checkForm(); 50 | }).toThrow(); 51 | }); 52 | 53 | describe('Ajaxable methods', () => { 54 | let formEl; 55 | let formEl2; 56 | let requests; 57 | let xhr; 58 | 59 | beforeEach(() => { 60 | form = `
61 | 62 |
63 |
64 | 65 | 66 |
`; 67 | d.body.innerHTML = form; 68 | formEl = d.getElementById('formel1'); 69 | formEl2 = d.getElementById('formel2'); 70 | obj = ajaxable('#formel2'); 71 | xhr = sinon.useFakeXMLHttpRequest(); 72 | requests = []; 73 | xhr.onCreate = function(xhr) { 74 | requests.push(xhr); 75 | }; 76 | }); 77 | 78 | afterEach(function() { 79 | xhr.restore(); 80 | }); 81 | 82 | it('Adds event listener correctly', (done) => { 83 | obj.addListener(formEl, 'submit', (e) => { 84 | done(); 85 | }); 86 | d.getElementById('send-data').click(); 87 | }); 88 | 89 | it('Removes event listener correctly', (done) => { 90 | obj.addListener(formEl, 'submit', (e) => { 91 | done(new Error('Not expected to submit')); 92 | }); 93 | obj.removeListeners(formEl, 'submit'); 94 | d.getElementById('send-data').onclick = () => { 95 | done(); 96 | }; 97 | d.getElementById('send-data').click(); 98 | }); 99 | 100 | it('Bind form correctly', (done) => { 101 | obj.sendForm = () => {done()}; 102 | formEl2.checkValidity = () => true; 103 | obj.bindForm(formEl2); 104 | obj.submit(); 105 | }); 106 | 107 | it('Fetch data from the FormData', () => { 108 | let fd = new window.FormData(); 109 | obj.fetchFormData(fd); 110 | fd.entries = () => [['test', 'value']]; 111 | let res = obj.fetchFormData(fd); 112 | expect(res).toEqual({test:'value'}); 113 | }); 114 | 115 | it('Bind form correctly, with fail on checkValidity', (done) => { 116 | obj.sendForm = () => {done()}; 117 | formEl2.checkValidity = () => false; 118 | obj.bindForm(formEl2); 119 | obj.submit(); 120 | done(); 121 | }); 122 | 123 | it('Triggers onStart on sendForm', (done) => { 124 | let formEl = d.getElementById('formel2'); 125 | obj.onStart((params) => { 126 | expect(params.el.id).toEqual(formEl.id); 127 | done(); 128 | }); 129 | obj.sendForm(formEl); 130 | }); 131 | 132 | it('Triggers onEnd on sendForm', (done) => { 133 | let formEl = d.getElementById('formel2'); 134 | obj.onEnd((params) => { 135 | expect(params.el.id).toEqual(formEl.id); 136 | done(); 137 | }); 138 | obj.sendForm(formEl); 139 | requests[0].respond(); 140 | }); 141 | 142 | it('Triggers onResponse on sendForm, auto parse JSON', (done) => { 143 | const data = {test: 'value'}; 144 | obj.onResponse((res, params) => { 145 | expect(res).toEqual(data); 146 | done(); 147 | }); 148 | obj.sendForm(formEl2); 149 | requests[0].respond(200, 150 | {'Content-Type': 'text/json'}, 151 | JSON.stringify(data)); 152 | }); 153 | 154 | it('Triggers onResponse on sendForm, do not parse JSON', (done) => { 155 | obj = ajaxable('#formel2', {responseType: ''}); 156 | const data = {test: 'value'}; 157 | obj.onResponse((res, params) => { 158 | expect(res).toEqual(JSON.stringify(data)); 159 | done(); 160 | }); 161 | obj.sendForm(formEl2); 162 | requests[0].respond(200, 163 | {'Content-Type': 'text/json'}, 164 | JSON.stringify(data)); 165 | }); 166 | 167 | it('Triggers onError on sendForm, with wrong JSON', (done) => { 168 | const data = {test: 'value'}; 169 | obj.onError((err, params) => { 170 | expect(err).toExist(); 171 | done(); 172 | }); 173 | obj.sendForm(formEl2); 174 | requests[0].respond(200, 175 | {'Content-Type': 'text/json'}, 176 | JSON.stringify(data)+'test'); 177 | }); 178 | 179 | it('Triggers onError on sendForm', (done) => { 180 | const data = {test: 'value'}; 181 | obj.onError((err, params) => { 182 | expect(err).toExist(); 183 | done(); 184 | }); 185 | obj.sendForm(formEl2); 186 | requests[0].respond(); 187 | }); 188 | 189 | it('Sends X-Requested-With in the headers', () => { 190 | obj.sendForm(formEl2); 191 | const reqHead = requests[0].requestHeaders; 192 | expect(reqHead['X-Requested-With']).toExist(); 193 | expect(reqHead['X-Requested-With']).toEqual('XMLHttpRequest'); 194 | }); 195 | 196 | it('Sends empty X-Requested-With headers', () => { 197 | obj = ajaxable('#formel2', { 198 | headers: {'X-Requested-With': ''} 199 | }); 200 | obj.sendForm(formEl2); 201 | const reqHead = requests[0].requestHeaders; 202 | expect(reqHead['X-Requested-With']).toNotExist(); 203 | }); 204 | 205 | 206 | }) 207 | 208 | }); 209 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var pkg = require('./package.json'); 4 | var name = 'ajaxable'; 5 | var env = process.env.WEBPACK_ENV; 6 | var plugins = []; 7 | 8 | if(env !== 'dev'){ 9 | plugins.push(new webpack.optimize.UglifyJsPlugin({ compressor: { warnings: false } })); 10 | plugins.push(new webpack.BannerPlugin(pkg.name + ' - ' + pkg.version)); 11 | } 12 | 13 | module.exports = { 14 | entry: './src/index', 15 | output: { 16 | filename: './dist/' + name + '.min.js', 17 | library: name, 18 | libraryTarget: 'umd', 19 | }, 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.jsx?$/, 24 | loader: 'babel-loader', 25 | include: /src/, 26 | query: { 27 | presets: ['es2015'] 28 | } 29 | }, 30 | ], 31 | }, 32 | plugins: plugins 33 | }; 34 | --------------------------------------------------------------------------------