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