blob: c6a189e4358fbe59ab54d97813728c33eb6fdd86 [file] [log] [blame]
<!--
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">
<link rel="import" href="iron-request.html">
<!--
The `iron-ajax` element exposes network request functionality.
<iron-ajax
auto
url="http://gdata.youtube.com/feeds/api/videos/"
params='{"alt":"json", "q":"chrome"}'
handle-as="json"
on-response="handleResponse"
debounce-duration="300"></iron-ajax>
With `auto` set to `true`, the element performs a request whenever
its `url`, `params` or `body` properties are changed. Automatically generated
requests will be debounced in the case that multiple attributes are changed
sequentially.
Note: The `params` attribute must be double quoted JSON.
You can trigger a request explicitly by calling `generateRequest` on the
element.
@demo demo/index.html
@hero hero.svg
-->
<script>
'use strict';
Polymer({
is: 'iron-ajax',
/**
* Fired when a request is sent.
*
* @event request
*/
/**
* Fired when a response is received.
*
* @event response
*/
/**
* Fired when an error is received.
*
* @event error
*/
hostAttributes: {
hidden: true
},
properties: {
/**
* The URL target of the request.
*/
url: {
type: String
},
/**
* An object that contains query parameters to be appended to the
* specified `url` when generating a request. If you wish to set the body
* content when making a POST request, you should use the `body` property
* instead.
*/
params: {
type: Object,
value: function() {
return {};
}
},
/**
* The HTTP method to use such as 'GET', 'POST', 'PUT', or 'DELETE'.
* Default is 'GET'.
*/
method: {
type: String,
value: 'GET'
},
/**
* HTTP request headers to send.
*
* Example:
*
* <iron-ajax
* auto
* url="http://somesite.com"
* headers='{"X-Requested-With": "XMLHttpRequest"}'
* handle-as="json"></iron-ajax>
*
* Note: setting a `Content-Type` header here will override the value
* specified by the `contentType` property of this element.
*/
headers: {
type: Object,
value: function() {
return {};
}
},
/**
* Content type to use when sending data. If the `contentType` property
* is set and a `Content-Type` header is specified in the `headers`
* property, the `headers` property value will take precedence.
*/
contentType: {
type: String,
value: null
},
/**
* Body content to send with the request, typically used with "POST"
* requests.
*
* If body is a string it will be sent unmodified.
*
* If Content-Type is set to a value listed below, then
* the body will be encoded accordingly.
*
* * `content-type="application/json"`
* * body is encoded like `{"foo":"bar baz","x":1}`
* * `content-type="application/x-www-form-urlencoded"`
* * body is encoded like `foo=bar+baz&x=1`
*
* Otherwise the body will be passed to the browser unmodified, and it
* will handle any encoding (e.g. for FormData, Blob, ArrayBuffer).
*
* @type (ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string|undefined|Object)
*/
body: {
type: Object,
value: null
},
/**
* Toggle whether XHR is synchronous or asynchronous. Don't change this
* to true unless You Know What You Are Doing™.
*/
sync: {
type: Boolean,
value: false
},
/**
* Specifies what data to store in the `response` property, and
* to deliver as `event.detail.response` in `response` events.
*
* One of:
*
* `text`: uses `XHR.responseText`.
*
* `xml`: uses `XHR.responseXML`.
*
* `json`: uses `XHR.responseText` parsed as JSON.
*
* `arraybuffer`: uses `XHR.response`.
*
* `blob`: uses `XHR.response`.
*
* `document`: uses `XHR.response`.
*/
handleAs: {
type: String,
value: 'json'
},
/**
* Set the withCredentials flag on the request.
*/
withCredentials: {
type: Boolean,
value: false
},
/**
* Set the timeout flag on the request.
*/
timeout: {
type: Number,
value: 0
},
/**
* If true, automatically performs an Ajax request when either `url` or
* `params` changes.
*/
auto: {
type: Boolean,
value: false
},
/**
* If true, error messages will automatically be logged to the console.
*/
verbose: {
type: Boolean,
value: false
},
/**
* The most recent request made by this iron-ajax element.
*/
lastRequest: {
type: Object,
notify: true,
readOnly: true
},
/**
* True while lastRequest is in flight.
*/
loading: {
type: Boolean,
notify: true,
readOnly: true
},
/**
* lastRequest's response.
*
* Note that lastResponse and lastError are set when lastRequest finishes,
* so if loading is true, then lastResponse and lastError will correspond
* to the result of the previous request.
*
* The type of the response is determined by the value of `handleAs` at
* the time that the request was generated.
*
* @type {Object}
*/
lastResponse: {
type: Object,
notify: true,
readOnly: true
},
/**
* lastRequest's error, if any.
*
* @type {Object}
*/
lastError: {
type: Object,
notify: true,
readOnly: true
},
/**
* An Array of all in-flight requests originating from this iron-ajax
* element.
*/
activeRequests: {
type: Array,
notify: true,
readOnly: true,
value: function() {
return [];
}
},
/**
* Length of time in milliseconds to debounce multiple automatically generated requests.
*/
debounceDuration: {
type: Number,
value: 0,
notify: true
},
/**
* Prefix to be stripped from a JSON response before parsing it.
*
* In order to prevent an attack using CSRF with Array responses
* (http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx/)
* many backends will mitigate this by prefixing all JSON response bodies
* with a string that would be nonsensical to a JavaScript parser.
*
*/
jsonPrefix: {
type: String,
value: ''
},
_boundHandleResponse: {
type: Function,
value: function() {
return this._handleResponse.bind(this);
}
}
},
observers: [
'_requestOptionsChanged(url, method, params.*, headers, contentType, ' +
'body, sync, handleAs, jsonPrefix, withCredentials, timeout, auto)'
],
/**
* The query string that should be appended to the `url`, serialized from
* the current value of `params`.
*
* @return {string}
*/
get queryString () {
var queryParts = [];
var param;
var value;
for (param in this.params) {
value = this.params[param];
param = window.encodeURIComponent(param);
if (Array.isArray(value)) {
for (var i = 0; i < value.length; i++) {
queryParts.push(param + '=' + window.encodeURIComponent(value[i]));
}
} else if (value !== null) {
queryParts.push(param + '=' + window.encodeURIComponent(value));
} else {
queryParts.push(param);
}
}
return queryParts.join('&');
},
/**
* The `url` with query string (if `params` are specified), suitable for
* providing to an `iron-request` instance.
*
* @return {string}
*/
get requestUrl() {
var queryString = this.queryString;
if (queryString) {
var bindingChar = this.url.indexOf('?') >= 0 ? '&' : '?';
return this.url + bindingChar + queryString;
}
return this.url;
},
/**
* An object that maps header names to header values, first applying the
* the value of `Content-Type` and then overlaying the headers specified
* in the `headers` property.
*
* @return {Object}
*/
get requestHeaders() {
var headers = {};
var contentType = this.contentType;
if (contentType == null && (typeof this.body === 'string')) {
contentType = 'application/x-www-form-urlencoded';
}
if (contentType) {
headers['content-type'] = contentType;
}
var header;
if (this.headers instanceof Object) {
for (header in this.headers) {
headers[header] = this.headers[header].toString();
}
}
return headers;
},
/**
* Request options suitable for generating an `iron-request` instance based
* on the current state of the `iron-ajax` instance's properties.
*
* @return {{
* 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)}}
*/
toRequestOptions: function() {
return {
url: this.requestUrl || '',
method: this.method,
headers: this.requestHeaders,
body: this.body,
async: !this.sync,
handleAs: this.handleAs,
jsonPrefix: this.jsonPrefix,
withCredentials: this.withCredentials,
timeout: this.timeout
};
},
/**
* Performs an AJAX request to the specified URL.
*
* @return {!IronRequestElement}
*/
generateRequest: function() {
var request = /** @type {!IronRequestElement} */ (document.createElement('iron-request'));
var requestOptions = this.toRequestOptions();
this.activeRequests.push(request);
request.completes.then(
this._boundHandleResponse
).catch(
this._handleError.bind(this, request)
).then(
this._discardRequest.bind(this, request)
);
request.send(requestOptions);
this._setLastRequest(request);
this._setLoading(true);
this.fire('request', {
request: request,
options: requestOptions
}, {bubbles: false});
return request;
},
_handleResponse: function(request) {
if (request === this.lastRequest) {
this._setLastResponse(request.response);
this._setLastError(null);
this._setLoading(false);
}
this.fire('response', request, {bubbles: false});
},
_handleError: function(request, error) {
if (this.verbose) {
console.error(error);
}
if (request === this.lastRequest) {
this._setLastError({
request: request,
error: error
});
this._setLastResponse(null);
this._setLoading(false);
}
this.fire('error', {
request: request,
error: error
}, {bubbles: false});
},
_discardRequest: function(request) {
var requestIndex = this.activeRequests.indexOf(request);
if (requestIndex > -1) {
this.activeRequests.splice(requestIndex, 1);
}
},
_requestOptionsChanged: function() {
this.debounce('generate-request', function() {
if (this.url == null) {
return;
}
if (this.auto) {
this.generateRequest();
}
}, this.debounceDuration);
},
});
</script>