blob: 0bb3b7f68dd4ca7a68edbf2311253cd9b7e2f14b [file] [log] [blame]
<!--
@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>