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