├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierrc ├── .tool-versions ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── constants.d.ts ├── package.json ├── src ├── constants.ts └── xr.ts ├── tsconfig.json ├── tsfmt.json ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{ts,tsx}] 2 | indent_size = 4 3 | indent_style = space -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | # don't lint nyc coverage output 6 | coverage -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // Use this file as a starting point for your project's .eslintrc. 2 | // Copy this file, and add rule overrides as needed. 3 | { 4 | "parser": "babel-eslint", 5 | "globals": { 6 | "I": "Immutable", 7 | "google": true 8 | }, 9 | "extends": "airbnb", 10 | "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments 11 | "browser": true, // browser global variables 12 | "node": true, // Node.js global variables and Node.js-specific rules 13 | "mocha": true // Mocha global variables for unit tests 14 | }, 15 | "rules": { 16 | "strict": 0, 17 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 18 | "newIsCap": true, 19 | "capIsNewExceptions": ["Map", "List", "OrderedMap", "Set", "Range", "Iterable"] 20 | }], 21 | "react/no-multi-comp": 0, 22 | "react/jsx-no-bind": 0, 23 | "react/jsx-closing-bracket-location": 0, 24 | "quote-props": 0, 25 | "no-nested-ternary": 0, 26 | "no-restricted-syntax": 0, 27 | "no-continue": 0 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 6 | rules: { 7 | '@typescript-eslint/no-explicit-any': 'off', 8 | '@typescript-eslint/explicit-module-boundary-types': 'off', 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | build/ 4 | *.log 5 | .idea 6 | .vscode 7 | .cache 8 | .parcel-cache 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "trailingComma": "all", 4 | "tabWidth": 4, 5 | "semi": false, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 14.15.4 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Andrey Boldyrev 2 | Chris DeLuca 3 | Firtina Ozbalikci 4 | James Cleveland 5 | Jared Lewis 6 | Jordan Stephens 7 | Matt Bilbow 8 | Michael J. Zoidl 9 | Tom Byrer 10 | Will Prater 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | xr Changelog 2 | ============ 3 | 4 | 0.3.2 5 | ----- 6 | 7 | * Remove `querystring` dependency 8 | * Use prettier/eslint instead of tslint/tsfmt 9 | * More type tweaks 10 | 11 | 0.3.1 12 | ----- 13 | 14 | * Update dependencies and TS version 15 | * Use Parcel v2 for packaging 16 | * Fixing types 17 | 18 | 0.3.0 19 | ----- 20 | 21 | * Rewrote in TypeScript 22 | * Deprecating project in favour of the new fetch API 23 | 24 | 25 | 0.2.0 26 | ------ 27 | 28 | * [API Change] Resolving promis now includes a "res" object, as opposed to just raw data. 29 | * Added "abort" method to returned promise. 30 | * Remove hot update from build (whoops!). 31 | * Add withCredentials argument for CORS. 32 | 33 | 0.1.15 34 | ------ 35 | 36 | * Using `querystring` for URL encoding instead of previous function. 37 | * Build process uses webpack. 38 | * Readme updates. 39 | 40 | 0.1.13 41 | ------ 42 | 43 | * Using xhr.responseText instead of xhr.response to fix an issue with IE9. 44 | 45 | 0.1.12 46 | ------ 47 | 48 | * Refactored according to the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript). 49 | 50 | 51 | 0.1.10 52 | ------ 53 | 54 | * Added ability to override XMLHttpRequest with xmlHttpRequest option. 55 | * Fix bug with IE where sending `undefined` with a DELETE request would send the string. 56 | 57 | 0.1.9 58 | ----- 59 | 60 | * Added xr.configure, a function to globally configure xr. 61 | * Slight change of the way Promises are passed into configuration, 62 | where `new` (it being a class) is not assumed necessary. They are 63 | now passed in a function which takes one argument (the first argument 64 | to the promise), and returns a Promise, e.g. `fn => new Promise(fn)`. 65 | 66 | 0.1.8 67 | ----- 68 | 69 | * Handling abort/error/timeout with a rejection. 70 | * Exposing `xr.Events` which are constants based on the XMLHttpRequest spec. 71 | 72 | 0.1.7 73 | ------ 74 | 75 | * Raw mode applies to sending and loading. 76 | * Updated readme with more examples. 77 | * Fix bug where data wasn't loaded. 78 | 79 | 0.1.6 80 | ----- 81 | 82 | * Fix bug where empty response causes exception. 83 | 84 | 0.1.5 85 | ----- 86 | 87 | * Allowing data to be sent directly using opts.raw 88 | * AUTHORS text file 89 | 90 | 0.1.4 91 | ----- 92 | 93 | * Remove Promise warning, because it would be annoying if you're providing your 94 | own promises. 95 | * Add PATCH and OPTIONS. 96 | 97 | 0.1.3 98 | ----- 99 | 100 | * Able to inject promise class (will be instantiated with `new`) 101 | * Using internal `assign` function so Object.assign is not needed. 102 | * Added Changelog 103 | 104 | 0.1.0 105 | ----- 106 | 107 | * resolve anything between 200-300 instead of only 200. 108 | * Change dumpFn/loadFn to dump/load. 109 | 110 | 0.0.10 111 | ------ 112 | 113 | * HackerNews debut! 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 James Cleveland 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE: Package is mostly unsupported. Would recommend fetch or axios** 2 | 3 | xr 4 | ======== 5 | 6 | Really simple wrapper around XHR that provides a few bits of nice 7 | functionality, exposes the XHR object wherever relevant, and returns an 8 | ES6 Promise (or whatever Promise is set to globally, if you want to use 9 | something else). 10 | 11 | The idea was to make a pragmatic library that's pre-configured for the 90% 12 | use case, but override-able for anyone that wants to do anything 13 | a bit off the beaten track. 14 | 15 | For instance, the library is by default set up to send/receive JSON (with 16 | the associated headers and parser/dumper already set up), but if you wanted to 17 | use something like XML, it's easy enough to override that with a few lines. 18 | 19 | It's lightweight, has no dependencies (other than having either Promise 20 | in the global namespace or provided via `xr.config`), and adds pretty 21 | much no overhead over the standard XHR API. 22 | 23 | Install 24 | ---------- 25 | `npm install xr --save` 26 | 27 | Quickstart 28 | ---------- 29 | 30 | ```javascript 31 | import xr from 'xr'; 32 | 33 | const res = await xr.get('/api/items', { take: 5 }); 34 | logger.log(res.data); 35 | 36 | const res = await xr.post('/api/item', { name: 'hello' }); 37 | logger.log('new item', res.data); 38 | ``` 39 | 40 | Extended syntax: 41 | 42 | ```javascript 43 | import xr from 'xr'; 44 | 45 | xr({ 46 | method: xr.Methods.GET, 47 | url: '/api/items', 48 | params: {take: 5}, 49 | events: { 50 | [xr.Events.PROGRESS]: (xhr, xhrProgressEvent) => { 51 | logger.log("xhr", xhr); 52 | logger.log("progress", xhrProgressEvent); 53 | }, 54 | }, 55 | }); 56 | ``` 57 | 58 | Custom promise: 59 | 60 | ```javascript 61 | xr.get('/url', {}, { 62 | promise: fn => new myPromiseClass(fn), 63 | }); 64 | ``` 65 | 66 | Raw mode (data is not dumped/loaded): 67 | 68 | ```javascript 69 | xr.put('/url', 'some data', { 70 | raw: true, 71 | }); 72 | ``` 73 | 74 | Custom dump/load: 75 | 76 | ```javascript 77 | xr.post('/url', { 'some': 'data' }, { 78 | dump: data => msgpack.encode(data), 79 | load: data => msgpack.decode(data), 80 | }); 81 | ``` 82 | 83 | Global configuration 84 | -------------------- 85 | 86 | One thing that I've always found irritating with libraries it that if you want to 87 | override the defaults, you have to do it per-request, or wrap the libraries. 88 | 89 | With XR, this is simple, as you can globally configure the module for your project. 90 | 91 | ```javascript 92 | xr.configure({ 93 | promise: fn => new myPromise(fn), 94 | }); 95 | ``` 96 | 97 | 98 | API is simple, for now consult [source](https://github.com/radiosilence/xr/blob/master/src/xr.js). 99 | 100 | Features 101 | -------- 102 | 103 | * Returns ES6 promises. 104 | * Has query parameter generation. 105 | * Supports events. 106 | 107 | Alias Methods 108 | ------------- 109 | 110 | You can do some quick aliases to requests, for instance: 111 | 112 | ```javascript 113 | xr.get('/my-url'); 114 | ``` 115 | 116 | Requirements 117 | ------------ 118 | 119 | There must be a [polyfill](https://github.com/jakearchibald/es6-promise) or [browser](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#Browser_compatibility) that supports at least the standard ES6 promise API 120 | (xr will use whatever's there). 121 | 122 | License 123 | ------- 124 | 125 | See LICENSE 126 | -------------------------------------------------------------------------------- /constants.d.ts: -------------------------------------------------------------------------------- 1 | export interface Methods { 2 | GET: 'GET'; 3 | POST: 'POST'; 4 | PUT: 'PUT'; 5 | DELETE: 'DELETE'; 6 | PATCH: 'PATCH'; 7 | OPTIONS: 'OPTIONS'; 8 | HEAD: 'HEAD'; 9 | } 10 | export declare const METHODS: Methods; 11 | export interface Events { 12 | READY_STATE_CHANGE: 'readystatechange'; 13 | LOAD_START: 'loadstart'; 14 | PROGRESS: 'progress'; 15 | ABORT: 'abort'; 16 | ERROR: 'error'; 17 | LOAD: 'load'; 18 | TIMEOUT: 'timeout'; 19 | LOAD_END: 'loadend'; 20 | } 21 | export declare const EVENTS: { 22 | READY_STATE_CHANGE: string; 23 | LOAD_START: string; 24 | PROGRESS: string; 25 | ABORT: string; 26 | ERROR: string; 27 | LOAD: string; 28 | TIMEOUT: string; 29 | LOAD_END: string; 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xr", 3 | "version": "0.3.2", 4 | "description": "Ultra-simple wrapper around XMLHttpRequest.", 5 | "main": "./dist/xr.js", 6 | "scripts": { 7 | "update-authors": "git log --format='%aN <%aE>' | sort -u | grep -v noreply > AUTHORS", 8 | "prepublishOnly": "npm run update-authors && npm run build", 9 | "build": "NODE_ENV=production tsc && parcel build src/xr.ts", 10 | "lint": "eslint src" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/radiosilence/xr" 15 | }, 16 | "keywords": [ 17 | "request", 18 | "xmlhttprequest", 19 | "json", 20 | "ajax" 21 | ], 22 | "author": "James Cleveland", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/radiosilence/xr/issues" 26 | }, 27 | "homepage": "https://github.com/radiosilence/xr", 28 | "devDependencies": { 29 | "@typescript-eslint/eslint-plugin": "^4.14.0", 30 | "@typescript-eslint/parser": "^4.14.0", 31 | "eslint": "^7.18.0", 32 | "parcel": "^2.0.0-nightly.551", 33 | "typescript": "^4.1.3" 34 | }, 35 | "dependencies": {} 36 | } 37 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export interface Methods { 2 | GET: 'GET' 3 | POST: 'POST' 4 | PUT: 'PUT' 5 | DELETE: 'DELETE' 6 | PATCH: 'PATCH' 7 | OPTIONS: 'OPTIONS' 8 | HEAD: 'HEAD' 9 | } 10 | 11 | export const METHODS: Methods = { 12 | GET: 'GET', 13 | POST: 'POST', 14 | PUT: 'PUT', 15 | DELETE: 'DELETE', 16 | PATCH: 'PATCH', 17 | OPTIONS: 'OPTIONS', 18 | HEAD: 'HEAD', 19 | } 20 | 21 | export interface Events { 22 | READY_STATE_CHANGE: 'readystatechange' 23 | LOAD_START: 'loadstart' 24 | PROGRESS: 'progress' 25 | ABORT: 'abort' 26 | ERROR: 'error' 27 | LOAD: 'load' 28 | TIMEOUT: 'timeout' 29 | LOAD_END: 'loadend' 30 | } 31 | 32 | export const EVENTS = { 33 | READY_STATE_CHANGE: 'readystatechange', 34 | LOAD_START: 'loadstart', 35 | PROGRESS: 'progress', 36 | ABORT: 'abort', 37 | ERROR: 'error', 38 | LOAD: 'load', 39 | TIMEOUT: 'timeout', 40 | LOAD_END: 'loadend', 41 | } 42 | -------------------------------------------------------------------------------- /src/xr.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * xr (c) James Cleveland 2015 3 | * URL: https://github.com/radiosilence/xr 4 | * License: BSD 5 | */ 6 | 7 | import { EVENTS, Methods, METHODS } from './constants' 8 | 9 | export interface Config { 10 | url?: string 11 | method: keyof Methods 12 | data?: Document | BodyInit 13 | headers: { [key: string]: string } 14 | dump: (data: T) => string 15 | load: (str: string) => T 16 | xmlHttpRequest: () => XMLHttpRequest 17 | promise: (fn: () => Promise) => Promise 18 | abort?: any 19 | params?: string[][] | Record | string | URLSearchParams 20 | withCredentials: boolean 21 | raw?: boolean 22 | events?: { [key: string]: () => void } 23 | } 24 | 25 | const defaults: Config = { 26 | method: METHODS.GET, 27 | data: undefined, 28 | headers: { 29 | Accept: 'application/json', 30 | 'Content-Type': 'application/json', 31 | }, 32 | dump: JSON.stringify, 33 | load: JSON.parse, 34 | xmlHttpRequest: (): XMLHttpRequest => new XMLHttpRequest(), 35 | promise: (fn: () => Promise) => new Promise(fn), 36 | withCredentials: false, 37 | } 38 | 39 | export interface Response { 40 | status: number 41 | response: Record 42 | data?: string | Record 43 | xhr: XMLHttpRequest 44 | } 45 | 46 | const res = ( 47 | xhr: XMLHttpRequest, 48 | data?: string | Record, 49 | ): Response => ({ 50 | status: xhr.status, 51 | response: xhr.response, 52 | data, 53 | xhr, 54 | }) 55 | 56 | let config: Config = { ...defaults } 57 | 58 | const configure = (opts: Partial): void => { 59 | config = { ...config, ...opts } 60 | } 61 | 62 | const promise = (args: Partial, fn: any) => 63 | (args && args.promise ? args.promise : config.promise || defaults.promise)( 64 | fn, 65 | ) 66 | 67 | const xr = (args: Partial): Promise => 68 | promise(args, (resolve: any, reject: any) => { 69 | const opts: Config = { ...defaults, ...config, ...args } 70 | const xhr = opts.xmlHttpRequest() 71 | 72 | if (opts.abort && typeof args.abort === 'function') { 73 | args.abort(() => { 74 | reject(res(xhr)) 75 | xhr.abort() 76 | }) 77 | } 78 | 79 | if (opts.url === undefined) throw new Error('No URL defined') 80 | 81 | xhr.open( 82 | opts.method, 83 | opts.params 84 | ? `${opts.url.split('?')[0]}?${new URLSearchParams( 85 | opts.params, 86 | )}` 87 | : opts.url, 88 | true, 89 | ) 90 | 91 | // setting after open for compatibility with IE versions <=10 92 | xhr.withCredentials = opts.withCredentials 93 | 94 | xhr.addEventListener(EVENTS.LOAD, () => { 95 | if (xhr.status >= 200 && xhr.status < 300) { 96 | let responseData 97 | if (xhr.responseText) { 98 | responseData = 99 | opts.raw === true 100 | ? xhr.responseText 101 | : opts.load(xhr.responseText) 102 | } 103 | resolve(res(xhr, responseData)) 104 | } else { 105 | reject(res(xhr)) 106 | } 107 | }) 108 | 109 | xhr.addEventListener(EVENTS.ABORT, () => reject(res(xhr))) 110 | xhr.addEventListener(EVENTS.ERROR, () => reject(res(xhr))) 111 | xhr.addEventListener(EVENTS.TIMEOUT, () => reject(res(xhr))) 112 | 113 | for (const k in opts.headers) { 114 | if (!{}.hasOwnProperty.call(opts.headers, k)) continue 115 | xhr.setRequestHeader(k, opts.headers[k]) 116 | } 117 | 118 | if (opts.events) { 119 | for (const k in opts.events) { 120 | if (!{}.hasOwnProperty.call(opts.events, k)) continue 121 | xhr.addEventListener(k, opts.events[k].bind(null, xhr), false) 122 | } 123 | } 124 | 125 | const data = 126 | typeof opts.data === 'object' && !opts.raw 127 | ? opts.dump(opts.data) 128 | : opts.data 129 | 130 | if (data !== undefined) xhr.send(data) 131 | else xhr.send() 132 | }) 133 | 134 | const api = { 135 | configure, 136 | EVENTS, 137 | METHODS, 138 | get: (url: string, params?: Record, args?: Partial) => 139 | xr({ url, method: METHODS.GET, params, ...args }), 140 | put: (url: string, data: any, args: Partial) => 141 | xr({ url, method: METHODS.PUT, data, ...args }), 142 | post: (url: string, data: any, args: Partial) => 143 | xr({ url, method: METHODS.POST, data, ...args }), 144 | patch: (url: string, data: any, args: Partial) => 145 | xr({ url, method: METHODS.PATCH, data, ...args }), 146 | del: (url: string, args: Partial) => 147 | xr({ url, method: METHODS.DELETE, ...args }), 148 | options: (url: string, args: Partial) => 149 | xr({ url, method: METHODS.OPTIONS, ...args }), 150 | } 151 | 152 | export default api 153 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./build", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "module": "commonjs", 8 | "target": "es5", 9 | "noImplicitAny": false, 10 | "strictNullChecks": true, 11 | "jsx": "react", 12 | "baseUrl": "./src", 13 | "lib": [ 14 | "ES2015", 15 | "dom" 16 | ], 17 | "typeRoots": [ 18 | "node_modules/@types" 19 | ] 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ], 24 | "include": [ 25 | "src" 26 | ] 27 | } -------------------------------------------------------------------------------- /tsfmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "newLineCharacter": "\n", 3 | "insertSpaceAfterCommaDelimiter": true, 4 | "insertSpaceAfterSemicolonInForStatements": true, 5 | "insertSpaceBeforeAndAfterBinaryOperators": true, 6 | "insertSpaceAfterKeywordsInControlFlowStatements": true, 7 | "insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, 8 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, 9 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, 10 | "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, 11 | "insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": false, 12 | "placeOpenBraceOnNewLineForFunctions": false, 13 | "placeOpenBraceOnNewLineForControlBlocks": false 14 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const fs = require('fs'); 3 | 4 | module.exports = { 5 | debug: false, 6 | entry: { 7 | xr: ['./dist/xr.js'], 8 | }, 9 | output: { 10 | path: __dirname, 11 | publicPath: '/', 12 | filename: 'xr.js', 13 | library: 'xr.js', 14 | libraryTarget: 'umd', 15 | pathinfo: true, 16 | }, 17 | plugins: [ 18 | new webpack.NoErrorsPlugin(), 19 | new webpack.optimize.UglifyJsPlugin({ 20 | minimize: true, 21 | compress: { 22 | drop_console: true, 23 | warnings: false, 24 | }, 25 | output: { 26 | comments: false, 27 | }, 28 | }), 29 | new webpack.DefinePlugin({ 30 | 'process.env': { 31 | NODE_ENV: JSON.stringify('production'), 32 | }, 33 | }), 34 | ], 35 | resolve: { 36 | extensions: ['', '.js', '.ts'], 37 | }, 38 | module: { 39 | loaders: [ 40 | ], 41 | }, 42 | }; 43 | --------------------------------------------------------------------------------