1<!-- 2@license 3Copyright (c) 2015 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9--> 10 11<link rel="import" href="../polymer/polymer.html"> 12 13<!-- 14iron-request can be used to perform XMLHttpRequests. 15 16 <iron-request id="xhr"></iron-request> 17 ... 18 this.$.xhr.send({url: url, body: params}); 19--> 20<script> 21 'use strict'; 22 23 Polymer({ 24 is: 'iron-request', 25 26 hostAttributes: { 27 hidden: true 28 }, 29 30 properties: { 31 32 /** 33 * A reference to the XMLHttpRequest instance used to generate the 34 * network request. 35 * 36 * @type {XMLHttpRequest} 37 */ 38 xhr: { 39 type: Object, 40 notify: true, 41 readOnly: true, 42 value: function() { 43 return new XMLHttpRequest(); 44 } 45 }, 46 47 /** 48 * A reference to the parsed response body, if the `xhr` has completely 49 * resolved. 50 * 51 * @type {*} 52 * @default null 53 */ 54 response: { 55 type: Object, 56 notify: true, 57 readOnly: true, 58 value: function() { 59 return null; 60 } 61 }, 62 63 /** 64 * A reference to the status code, if the `xhr` has completely resolved. 65 */ 66 status: { 67 type: Number, 68 notify: true, 69 readOnly: true, 70 value: 0 71 }, 72 73 /** 74 * A reference to the status text, if the `xhr` has completely resolved. 75 */ 76 statusText: { 77 type: String, 78 notify: true, 79 readOnly: true, 80 value: '' 81 }, 82 83 /** 84 * A promise that resolves when the `xhr` response comes back, or rejects 85 * if there is an error before the `xhr` completes. 86 * The resolve callback is called with the original request as an argument. 87 * By default, the reject callback is called with an `Error` as an argument. 88 * If `rejectWithRequest` is true, the reject callback is called with an 89 * object with two keys: `request`, the original request, and `error`, the 90 * error object. 91 * 92 * @type {Promise} 93 */ 94 completes: { 95 type: Object, 96 readOnly: true, 97 notify: true, 98 value: function() { 99 return new Promise(function(resolve, reject) { 100 this.resolveCompletes = resolve; 101 this.rejectCompletes = reject; 102 }.bind(this)); 103 } 104 }, 105 106 /** 107 * An object that contains progress information emitted by the XHR if 108 * available. 109 * 110 * @default {} 111 */ 112 progress: { 113 type: Object, 114 notify: true, 115 readOnly: true, 116 value: function() { 117 return {}; 118 } 119 }, 120 121 /** 122 * Aborted will be true if an abort of the request is attempted. 123 */ 124 aborted: { 125 type: Boolean, 126 notify: true, 127 readOnly: true, 128 value: false, 129 }, 130 131 /** 132 * Errored will be true if the browser fired an error event from the 133 * XHR object (mainly network errors). 134 */ 135 errored: { 136 type: Boolean, 137 notify: true, 138 readOnly: true, 139 value: false 140 }, 141 142 /** 143 * TimedOut will be true if the XHR threw a timeout event. 144 */ 145 timedOut: { 146 type: Boolean, 147 notify: true, 148 readOnly: true, 149 value: false 150 } 151 }, 152 153 /** 154 * Succeeded is true if the request succeeded. The request succeeded if it 155 * loaded without error, wasn't aborted, and the status code is ≥ 200, and 156 * < 300, or if the status code is 0. 157 * 158 * The status code 0 is accepted as a success because some schemes - e.g. 159 * file:// - don't provide status codes. 160 * 161 * @return {boolean} 162 */ 163 get succeeded() { 164 if (this.errored || this.aborted || this.timedOut) { 165 return false; 166 } 167 var status = this.xhr.status || 0; 168 169 // Note: if we are using the file:// protocol, the status code will be 0 170 // for all outcomes (successful or otherwise). 171 return status === 0 || 172 (status >= 200 && status < 300); 173 }, 174 175 /** 176 * Sends an HTTP request to the server and returns a promise (see the `completes` 177 * property for details). 178 * 179 * The handling of the `body` parameter will vary based on the Content-Type 180 * header. See the docs for iron-ajax's `body` property for details. 181 * 182 * @param {{ 183 * url: string, 184 * method: (string|undefined), 185 * async: (boolean|undefined), 186 * body: (ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string|undefined|Object), 187 * headers: (Object|undefined), 188 * handleAs: (string|undefined), 189 * jsonPrefix: (string|undefined), 190 * withCredentials: (boolean|undefined), 191 * timeout: (Number|undefined), 192 * rejectWithRequest: (boolean|undefined)}} options - 193 * - url The url to which the request is sent. 194 * - method The HTTP method to use, default is GET. 195 * - async By default, all requests are sent asynchronously. To send synchronous requests, 196 * set to false. 197 * - body The content for the request body for POST method. 198 * - headers HTTP request headers. 199 * - handleAs The response type. Default is 'text'. 200 * - withCredentials Whether or not to send credentials on the request. Default is false. 201 * - timeout - Timeout for request, in milliseconds. 202 * - rejectWithRequest Set to true to include the request object with promise rejections. 203 * @return {Promise} 204 */ 205 send: function(options) { 206 var xhr = this.xhr; 207 208 if (xhr.readyState > 0) { 209 return null; 210 } 211 212 xhr.addEventListener('progress', function(progress) { 213 this._setProgress({ 214 lengthComputable: progress.lengthComputable, 215 loaded: progress.loaded, 216 total: progress.total 217 }); 218 }.bind(this)); 219 220 xhr.addEventListener('error', function(error) { 221 this._setErrored(true); 222 this._updateStatus(); 223 var response = options.rejectWithRequest ? { 224 error: error, 225 request: this 226 } : error; 227 this.rejectCompletes(response); 228 }.bind(this)); 229 230 xhr.addEventListener('timeout', function(error) { 231 this._setTimedOut(true); 232 this._updateStatus(); 233 var response = options.rejectWithRequest ? { 234 error: error, 235 request: this 236 } : error; 237 this.rejectCompletes(response); 238 }.bind(this)); 239 240 xhr.addEventListener('abort', function() { 241 this._setAborted(true); 242 this._updateStatus(); 243 var error = new Error('Request aborted.'); 244 var response = options.rejectWithRequest ? { 245 error: error, 246 request: this 247 } : error; 248 this.rejectCompletes(response); 249 }.bind(this)); 250 251 // Called after all of the above. 252 xhr.addEventListener('loadend', function() { 253 this._updateStatus(); 254 this._setResponse(this.parseResponse()); 255 256 if (!this.succeeded) { 257 var error = new Error('The request failed with status code: ' + this.xhr.status); 258 var response = options.rejectWithRequest ? { 259 error: error, 260 request: this 261 } : error; 262 this.rejectCompletes(response); 263 return; 264 } 265 266 this.resolveCompletes(this); 267 }.bind(this)); 268 269 this.url = options.url; 270 xhr.open( 271 options.method || 'GET', 272 options.url, 273 options.async !== false 274 ); 275 276 var acceptType = { 277 'json': 'application/json', 278 'text': 'text/plain', 279 'html': 'text/html', 280 'xml': 'application/xml', 281 'arraybuffer': 'application/octet-stream' 282 }[options.handleAs]; 283 var headers = options.headers || Object.create(null); 284 var newHeaders = Object.create(null); 285 for (var key in headers) { 286 newHeaders[key.toLowerCase()] = headers[key]; 287 } 288 headers = newHeaders; 289 290 if (acceptType && !headers['accept']) { 291 headers['accept'] = acceptType; 292 } 293 Object.keys(headers).forEach(function(requestHeader) { 294 if (/[A-Z]/.test(requestHeader)) { 295 Polymer.Base._error('Headers must be lower case, got', requestHeader); 296 } 297 xhr.setRequestHeader( 298 requestHeader, 299 headers[requestHeader] 300 ); 301 }, this); 302 303 if (options.async !== false) { 304 if (options.async) { 305 xhr.timeout = options.timeout; 306 } 307 308 var handleAs = options.handleAs; 309 310 // If a JSON prefix is present, the responseType must be 'text' or the 311 // browser won’t be able to parse the response. 312 if (!!options.jsonPrefix || !handleAs) { 313 handleAs = 'text'; 314 } 315 316 // In IE, `xhr.responseType` is an empty string when the response 317 // returns. Hence, caching it as `xhr._responseType`. 318 xhr.responseType = xhr._responseType = handleAs; 319 320 // Cache the JSON prefix, if it exists. 321 if (!!options.jsonPrefix) { 322 xhr._jsonPrefix = options.jsonPrefix; 323 } 324 } 325 326 xhr.withCredentials = !!options.withCredentials; 327 328 329 var body = this._encodeBodyObject(options.body, headers['content-type']); 330 331 xhr.send( 332 /** @type {ArrayBuffer|ArrayBufferView|Blob|Document|FormData| 333 null|string|undefined} */ 334 (body)); 335 336 return this.completes; 337 }, 338 339 /** 340 * Attempts to parse the response body of the XHR. If parsing succeeds, 341 * the value returned will be deserialized based on the `responseType` 342 * set on the XHR. 343 * 344 * @return {*} The parsed response, 345 * or undefined if there was an empty response or parsing failed. 346 */ 347 parseResponse: function() { 348 var xhr = this.xhr; 349 var responseType = xhr.responseType || xhr._responseType; 350 var preferResponseText = !this.xhr.responseType; 351 var prefixLen = (xhr._jsonPrefix && xhr._jsonPrefix.length) || 0; 352 353 try { 354 switch (responseType) { 355 case 'json': 356 // If the xhr object doesn't have a natural `xhr.responseType`, 357 // we can assume that the browser hasn't parsed the response for us, 358 // and so parsing is our responsibility. Likewise if response is 359 // undefined, as there's no way to encode undefined in JSON. 360 if (preferResponseText || xhr.response === undefined) { 361 // Try to emulate the JSON section of the response body section of 362 // the spec: https://xhr.spec.whatwg.org/#response-body 363 // That is to say, we try to parse as JSON, but if anything goes 364 // wrong return null. 365 try { 366 return JSON.parse(xhr.responseText); 367 } catch (_) { 368 return null; 369 } 370 } 371 372 return xhr.response; 373 case 'xml': 374 return xhr.responseXML; 375 case 'blob': 376 case 'document': 377 case 'arraybuffer': 378 return xhr.response; 379 case 'text': 380 default: { 381 // If `prefixLen` is set, it implies the response should be parsed 382 // as JSON once the prefix of length `prefixLen` is stripped from 383 // it. Emulate the behavior above where null is returned on failure 384 // to parse. 385 if (prefixLen) { 386 try { 387 return JSON.parse(xhr.responseText.substring(prefixLen)); 388 } catch (_) { 389 return null; 390 } 391 } 392 return xhr.responseText; 393 } 394 } 395 } catch (e) { 396 this.rejectCompletes(new Error('Could not parse response. ' + e.message)); 397 } 398 }, 399 400 /** 401 * Aborts the request. 402 */ 403 abort: function() { 404 this._setAborted(true); 405 this.xhr.abort(); 406 }, 407 408 /** 409 * @param {*} body The given body of the request to try and encode. 410 * @param {?string} contentType The given content type, to infer an encoding 411 * from. 412 * @return {*} Either the encoded body as a string, if successful, 413 * or the unaltered body object if no encoding could be inferred. 414 */ 415 _encodeBodyObject: function(body, contentType) { 416 if (typeof body == 'string') { 417 return body; // Already encoded. 418 } 419 var bodyObj = /** @type {Object} */ (body); 420 switch(contentType) { 421 case('application/json'): 422 return JSON.stringify(bodyObj); 423 case('application/x-www-form-urlencoded'): 424 return this._wwwFormUrlEncode(bodyObj); 425 } 426 return body; 427 }, 428 429 /** 430 * @param {Object} object The object to encode as x-www-form-urlencoded. 431 * @return {string} . 432 */ 433 _wwwFormUrlEncode: function(object) { 434 if (!object) { 435 return ''; 436 } 437 var pieces = []; 438 Object.keys(object).forEach(function(key) { 439 // TODO(rictic): handle array values here, in a consistent way with 440 // iron-ajax params. 441 pieces.push( 442 this._wwwFormUrlEncodePiece(key) + '=' + 443 this._wwwFormUrlEncodePiece(object[key])); 444 }, this); 445 return pieces.join('&'); 446 }, 447 448 /** 449 * @param {*} str A key or value to encode as x-www-form-urlencoded. 450 * @return {string} . 451 */ 452 _wwwFormUrlEncodePiece: function(str) { 453 // Spec says to normalize newlines to \r\n and replace %20 spaces with +. 454 // jQuery does this as well, so this is likely to be widely compatible. 455 if (str === null || str === undefined || !str.toString) { 456 return ''; 457 } 458 459 return encodeURIComponent(str.toString().replace(/\r?\n/g, '\r\n')) 460 .replace(/%20/g, '+'); 461 }, 462 463 /** 464 * Updates the status code and status text. 465 */ 466 _updateStatus: function() { 467 this._setStatus(this.xhr.status); 468 this._setStatusText((this.xhr.statusText === undefined) ? '' : this.xhr.statusText); 469 } 470 }); 471</script> 472