| <!-- |
| @license |
| Copyright (c) 2015 The Polymer Project Authors. All rights reserved. |
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt |
| Code distributed by Google as part of the polymer project is also |
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt |
| --> |
| |
| <link rel="import" href="../polymer/polymer.html"> |
| |
| <!-- |
| iron-request can be used to perform XMLHttpRequests. |
| |
| <iron-request id="xhr"></iron-request> |
| ... |
| this.$.xhr.send({url: url, body: params}); |
| --> |
| <script> |
| 'use strict'; |
| |
| Polymer({ |
| is: 'iron-request', |
| |
| hostAttributes: { |
| hidden: true |
| }, |
| |
| properties: { |
| |
| /** |
| * A reference to the XMLHttpRequest instance used to generate the |
| * network request. |
| * |
| * @type {XMLHttpRequest} |
| */ |
| xhr: { |
| type: Object, |
| notify: true, |
| readOnly: true, |
| value: function() { |
| return new XMLHttpRequest(); |
| } |
| }, |
| |
| /** |
| * A reference to the parsed response body, if the `xhr` has completely |
| * resolved. |
| * |
| * @type {*} |
| * @default null |
| */ |
| response: { |
| type: Object, |
| notify: true, |
| readOnly: true, |
| value: function() { |
| return null; |
| } |
| }, |
| |
| /** |
| * A reference to the status code, if the `xhr` has completely resolved. |
| */ |
| status: { |
| type: Number, |
| notify: true, |
| readOnly: true, |
| value: 0 |
| }, |
| |
| /** |
| * A reference to the status text, if the `xhr` has completely resolved. |
| */ |
| statusText: { |
| type: String, |
| notify: true, |
| readOnly: true, |
| value: '' |
| }, |
| |
| /** |
| * A promise that resolves when the `xhr` response comes back, or rejects |
| * if there is an error before the `xhr` completes. |
| * The resolve callback is called with the original request as an argument. |
| * By default, the reject callback is called with an `Error` as an argument. |
| * If `rejectWithRequest` is true, the reject callback is called with an |
| * object with two keys: `request`, the original request, and `error`, the |
| * error object. |
| * |
| * @type {Promise} |
| */ |
| completes: { |
| type: Object, |
| readOnly: true, |
| notify: true, |
| value: function() { |
| return new Promise(function(resolve, reject) { |
| this.resolveCompletes = resolve; |
| this.rejectCompletes = reject; |
| }.bind(this)); |
| } |
| }, |
| |
| /** |
| * An object that contains progress information emitted by the XHR if |
| * available. |
| * |
| * @default {} |
| */ |
| progress: { |
| type: Object, |
| notify: true, |
| readOnly: true, |
| value: function() { |
| return {}; |
| } |
| }, |
| |
| /** |
| * Aborted will be true if an abort of the request is attempted. |
| */ |
| aborted: { |
| type: Boolean, |
| notify: true, |
| readOnly: true, |
| value: false, |
| }, |
| |
| /** |
| * Errored will be true if the browser fired an error event from the |
| * XHR object (mainly network errors). |
| */ |
| errored: { |
| type: Boolean, |
| notify: true, |
| readOnly: true, |
| value: false |
| }, |
| |
| /** |
| * TimedOut will be true if the XHR threw a timeout event. |
| */ |
| timedOut: { |
| type: Boolean, |
| notify: true, |
| readOnly: true, |
| value: false |
| } |
| }, |
| |
| /** |
| * Succeeded is true if the request succeeded. The request succeeded if it |
| * loaded without error, wasn't aborted, and the status code is ≥ 200, and |
| * < 300, or if the status code is 0. |
| * |
| * The status code 0 is accepted as a success because some schemes - e.g. |
| * file:// - don't provide status codes. |
| * |
| * @return {boolean} |
| */ |
| get succeeded() { |
| if (this.errored || this.aborted || this.timedOut) { |
| return false; |
| } |
| var status = this.xhr.status || 0; |
| |
| // Note: if we are using the file:// protocol, the status code will be 0 |
| // for all outcomes (successful or otherwise). |
| return status === 0 || |
| (status >= 200 && status < 300); |
| }, |
| |
| /** |
| * Sends an HTTP request to the server and returns a promise (see the `completes` |
| * property for details). |
| * |
| * The handling of the `body` parameter will vary based on the Content-Type |
| * header. See the docs for iron-ajax's `body` property for details. |
| * |
| * @param {{ |
| * url: string, |
| * method: (string|undefined), |
| * async: (boolean|undefined), |
| * body: (ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string|undefined|Object), |
| * headers: (Object|undefined), |
| * handleAs: (string|undefined), |
| * jsonPrefix: (string|undefined), |
| * withCredentials: (boolean|undefined), |
| * timeout: (Number|undefined), |
| * rejectWithRequest: (boolean|undefined)}} options - |
| * - url The url to which the request is sent. |
| * - method The HTTP method to use, default is GET. |
| * - async By default, all requests are sent asynchronously. To send synchronous requests, |
| * set to false. |
| * - body The content for the request body for POST method. |
| * - headers HTTP request headers. |
| * - handleAs The response type. Default is 'text'. |
| * - withCredentials Whether or not to send credentials on the request. Default is false. |
| * - timeout - Timeout for request, in milliseconds. |
| * - rejectWithRequest Set to true to include the request object with promise rejections. |
| * @return {Promise} |
| */ |
| send: function(options) { |
| var xhr = this.xhr; |
| |
| if (xhr.readyState > 0) { |
| return null; |
| } |
| |
| xhr.addEventListener('progress', function(progress) { |
| this._setProgress({ |
| lengthComputable: progress.lengthComputable, |
| loaded: progress.loaded, |
| total: progress.total |
| }); |
| }.bind(this)); |
| |
| xhr.addEventListener('error', function(error) { |
| this._setErrored(true); |
| this._updateStatus(); |
| var response = options.rejectWithRequest ? { |
| error: error, |
| request: this |
| } : error; |
| this.rejectCompletes(response); |
| }.bind(this)); |
| |
| xhr.addEventListener('timeout', function(error) { |
| this._setTimedOut(true); |
| this._updateStatus(); |
| var response = options.rejectWithRequest ? { |
| error: error, |
| request: this |
| } : error; |
| this.rejectCompletes(response); |
| }.bind(this)); |
| |
| xhr.addEventListener('abort', function() { |
| this._setAborted(true); |
| this._updateStatus(); |
| var error = new Error('Request aborted.'); |
| var response = options.rejectWithRequest ? { |
| error: error, |
| request: this |
| } : error; |
| this.rejectCompletes(response); |
| }.bind(this)); |
| |
| // Called after all of the above. |
| xhr.addEventListener('loadend', function() { |
| this._updateStatus(); |
| this._setResponse(this.parseResponse()); |
| |
| if (!this.succeeded) { |
| var error = new Error('The request failed with status code: ' + this.xhr.status); |
| var response = options.rejectWithRequest ? { |
| error: error, |
| request: this |
| } : error; |
| this.rejectCompletes(response); |
| return; |
| } |
| |
| this.resolveCompletes(this); |
| }.bind(this)); |
| |
| this.url = options.url; |
| xhr.open( |
| options.method || 'GET', |
| options.url, |
| options.async !== false |
| ); |
| |
| var acceptType = { |
| 'json': 'application/json', |
| 'text': 'text/plain', |
| 'html': 'text/html', |
| 'xml': 'application/xml', |
| 'arraybuffer': 'application/octet-stream' |
| }[options.handleAs]; |
| var headers = options.headers || Object.create(null); |
| var newHeaders = Object.create(null); |
| for (var key in headers) { |
| newHeaders[key.toLowerCase()] = headers[key]; |
| } |
| headers = newHeaders; |
| |
| if (acceptType && !headers['accept']) { |
| headers['accept'] = acceptType; |
| } |
| Object.keys(headers).forEach(function(requestHeader) { |
| if (/[A-Z]/.test(requestHeader)) { |
| Polymer.Base._error('Headers must be lower case, got', requestHeader); |
| } |
| xhr.setRequestHeader( |
| requestHeader, |
| headers[requestHeader] |
| ); |
| }, this); |
| |
| if (options.async !== false) { |
| if (options.async) { |
| xhr.timeout = options.timeout; |
| } |
| |
| var handleAs = options.handleAs; |
| |
| // If a JSON prefix is present, the responseType must be 'text' or the |
| // browser won’t be able to parse the response. |
| if (!!options.jsonPrefix || !handleAs) { |
| handleAs = 'text'; |
| } |
| |
| // In IE, `xhr.responseType` is an empty string when the response |
| // returns. Hence, caching it as `xhr._responseType`. |
| xhr.responseType = xhr._responseType = handleAs; |
| |
| // Cache the JSON prefix, if it exists. |
| if (!!options.jsonPrefix) { |
| xhr._jsonPrefix = options.jsonPrefix; |
| } |
| } |
| |
| xhr.withCredentials = !!options.withCredentials; |
| |
| |
| var body = this._encodeBodyObject(options.body, headers['content-type']); |
| |
| xhr.send( |
| /** @type {ArrayBuffer|ArrayBufferView|Blob|Document|FormData| |
| null|string|undefined} */ |
| (body)); |
| |
| return this.completes; |
| }, |
| |
| /** |
| * Attempts to parse the response body of the XHR. If parsing succeeds, |
| * the value returned will be deserialized based on the `responseType` |
| * set on the XHR. |
| * |
| * @return {*} The parsed response, |
| * or undefined if there was an empty response or parsing failed. |
| */ |
| parseResponse: function() { |
| var xhr = this.xhr; |
| var responseType = xhr.responseType || xhr._responseType; |
| var preferResponseText = !this.xhr.responseType; |
| var prefixLen = (xhr._jsonPrefix && xhr._jsonPrefix.length) || 0; |
| |
| try { |
| switch (responseType) { |
| case 'json': |
| // If the xhr object doesn't have a natural `xhr.responseType`, |
| // we can assume that the browser hasn't parsed the response for us, |
| // and so parsing is our responsibility. Likewise if response is |
| // undefined, as there's no way to encode undefined in JSON. |
| if (preferResponseText || xhr.response === undefined) { |
| // Try to emulate the JSON section of the response body section of |
| // the spec: https://xhr.spec.whatwg.org/#response-body |
| // That is to say, we try to parse as JSON, but if anything goes |
| // wrong return null. |
| try { |
| return JSON.parse(xhr.responseText); |
| } catch (_) { |
| return null; |
| } |
| } |
| |
| return xhr.response; |
| case 'xml': |
| return xhr.responseXML; |
| case 'blob': |
| case 'document': |
| case 'arraybuffer': |
| return xhr.response; |
| case 'text': |
| default: { |
| // If `prefixLen` is set, it implies the response should be parsed |
| // as JSON once the prefix of length `prefixLen` is stripped from |
| // it. Emulate the behavior above where null is returned on failure |
| // to parse. |
| if (prefixLen) { |
| try { |
| return JSON.parse(xhr.responseText.substring(prefixLen)); |
| } catch (_) { |
| return null; |
| } |
| } |
| return xhr.responseText; |
| } |
| } |
| } catch (e) { |
| this.rejectCompletes(new Error('Could not parse response. ' + e.message)); |
| } |
| }, |
| |
| /** |
| * Aborts the request. |
| */ |
| abort: function() { |
| this._setAborted(true); |
| this.xhr.abort(); |
| }, |
| |
| /** |
| * @param {*} body The given body of the request to try and encode. |
| * @param {?string} contentType The given content type, to infer an encoding |
| * from. |
| * @return {*} Either the encoded body as a string, if successful, |
| * or the unaltered body object if no encoding could be inferred. |
| */ |
| _encodeBodyObject: function(body, contentType) { |
| if (typeof body == 'string') { |
| return body; // Already encoded. |
| } |
| var bodyObj = /** @type {Object} */ (body); |
| switch(contentType) { |
| case('application/json'): |
| return JSON.stringify(bodyObj); |
| case('application/x-www-form-urlencoded'): |
| return this._wwwFormUrlEncode(bodyObj); |
| } |
| return body; |
| }, |
| |
| /** |
| * @param {Object} object The object to encode as x-www-form-urlencoded. |
| * @return {string} . |
| */ |
| _wwwFormUrlEncode: function(object) { |
| if (!object) { |
| return ''; |
| } |
| var pieces = []; |
| Object.keys(object).forEach(function(key) { |
| // TODO(rictic): handle array values here, in a consistent way with |
| // iron-ajax params. |
| pieces.push( |
| this._wwwFormUrlEncodePiece(key) + '=' + |
| this._wwwFormUrlEncodePiece(object[key])); |
| }, this); |
| return pieces.join('&'); |
| }, |
| |
| /** |
| * @param {*} str A key or value to encode as x-www-form-urlencoded. |
| * @return {string} . |
| */ |
| _wwwFormUrlEncodePiece: function(str) { |
| // Spec says to normalize newlines to \r\n and replace %20 spaces with +. |
| // jQuery does this as well, so this is likely to be widely compatible. |
| if (str === null || str === undefined || !str.toString) { |
| return ''; |
| } |
| |
| return encodeURIComponent(str.toString().replace(/\r?\n/g, '\r\n')) |
| .replace(/%20/g, '+'); |
| }, |
| |
| /** |
| * Updates the status code and status text. |
| */ |
| _updateStatus: function() { |
| this._setStatus(this.xhr.status); |
| this._setStatusText((this.xhr.statusText === undefined) ? '' : this.xhr.statusText); |
| } |
| }); |
| </script> |