1<!-- 2@license 3Copyright (c) 2015 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9--> 10 11<link rel="import" href="../polymer/polymer.html"> 12<link rel="import" href="iron-request.html"> 13 14<!-- 15The `iron-ajax` element exposes network request functionality. 16 17 <iron-ajax 18 auto 19 url="https://www.googleapis.com/youtube/v3/search" 20 params='{"part":"snippet", "q":"polymer", "key": "YOUTUBE_API_KEY", "type": "video"}' 21 handle-as="json" 22 on-response="handleResponse" 23 debounce-duration="300"></iron-ajax> 24 25With `auto` set to `true`, the element performs a request whenever 26its `url`, `params` or `body` properties are changed. Automatically generated 27requests will be debounced in the case that multiple attributes are changed 28sequentially. 29 30Note: The `params` attribute must be double quoted JSON. 31 32You can trigger a request explicitly by calling `generateRequest` on the 33element. 34 35@demo demo/index.html 36@hero hero.svg 37--> 38 39<script> 40 'use strict'; 41 42 Polymer({ 43 44 is: 'iron-ajax', 45 46 /** 47 * Fired before a request is sent. 48 * 49 * @event iron-ajax-presend 50 */ 51 52 /** 53 * Fired when a request is sent. 54 * 55 * @event request 56 * @event iron-ajax-request 57 */ 58 59 /** 60 * Fired when a response is received. 61 * 62 * @event response 63 * @event iron-ajax-response 64 */ 65 66 /** 67 * Fired when an error is received. 68 * 69 * @event error 70 * @event iron-ajax-error 71 */ 72 73 hostAttributes: { 74 hidden: true 75 }, 76 77 properties: { 78 /** 79 * The URL target of the request. 80 */ 81 url: { 82 type: String 83 }, 84 85 /** 86 * An object that contains query parameters to be appended to the 87 * specified `url` when generating a request. If you wish to set the body 88 * content when making a POST request, you should use the `body` property 89 * instead. 90 */ 91 params: { 92 type: Object, 93 value: function() { 94 return {}; 95 } 96 }, 97 98 /** 99 * The HTTP method to use such as 'GET', 'POST', 'PUT', or 'DELETE'. 100 * Default is 'GET'. 101 */ 102 method: { 103 type: String, 104 value: 'GET' 105 }, 106 107 /** 108 * HTTP request headers to send. 109 * 110 * Example: 111 * 112 * <iron-ajax 113 * auto 114 * url="http://somesite.com" 115 * headers='{"X-Requested-With": "XMLHttpRequest"}' 116 * handle-as="json"></iron-ajax> 117 * 118 * Note: setting a `Content-Type` header here will override the value 119 * specified by the `contentType` property of this element. 120 */ 121 headers: { 122 type: Object, 123 value: function() { 124 return {}; 125 } 126 }, 127 128 /** 129 * Content type to use when sending data. If the `contentType` property 130 * is set and a `Content-Type` header is specified in the `headers` 131 * property, the `headers` property value will take precedence. 132 * 133 * Varies the handling of the `body` param. 134 */ 135 contentType: { 136 type: String, 137 value: null 138 }, 139 140 /** 141 * Body content to send with the request, typically used with "POST" 142 * requests. 143 * 144 * If body is a string it will be sent unmodified. 145 * 146 * If Content-Type is set to a value listed below, then 147 * the body will be encoded accordingly. 148 * 149 * * `content-type="application/json"` 150 * * body is encoded like `{"foo":"bar baz","x":1}` 151 * * `content-type="application/x-www-form-urlencoded"` 152 * * body is encoded like `foo=bar+baz&x=1` 153 * 154 * Otherwise the body will be passed to the browser unmodified, and it 155 * will handle any encoding (e.g. for FormData, Blob, ArrayBuffer). 156 * 157 * @type (ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string|undefined|Object) 158 */ 159 body: { 160 type: Object, 161 value: null 162 }, 163 164 /** 165 * Toggle whether XHR is synchronous or asynchronous. Don't change this 166 * to true unless You Know What You Are Doing™. 167 */ 168 sync: { 169 type: Boolean, 170 value: false 171 }, 172 173 /** 174 * Specifies what data to store in the `response` property, and 175 * to deliver as `event.detail.response` in `response` events. 176 * 177 * One of: 178 * 179 * `text`: uses `XHR.responseText`. 180 * 181 * `xml`: uses `XHR.responseXML`. 182 * 183 * `json`: uses `XHR.responseText` parsed as JSON. 184 * 185 * `arraybuffer`: uses `XHR.response`. 186 * 187 * `blob`: uses `XHR.response`. 188 * 189 * `document`: uses `XHR.response`. 190 */ 191 handleAs: { 192 type: String, 193 value: 'json' 194 }, 195 196 /** 197 * Set the withCredentials flag on the request. 198 */ 199 withCredentials: { 200 type: Boolean, 201 value: false 202 }, 203 204 /** 205 * Set the timeout flag on the request. 206 */ 207 timeout: { 208 type: Number, 209 value: 0 210 }, 211 212 /** 213 * If true, automatically performs an Ajax request when either `url` or 214 * `params` changes. 215 */ 216 auto: { 217 type: Boolean, 218 value: false 219 }, 220 221 /** 222 * If true, error messages will automatically be logged to the console. 223 */ 224 verbose: { 225 type: Boolean, 226 value: false 227 }, 228 229 /** 230 * The most recent request made by this iron-ajax element. 231 */ 232 lastRequest: { 233 type: Object, 234 notify: true, 235 readOnly: true 236 }, 237 238 /** 239 * True while lastRequest is in flight. 240 */ 241 loading: { 242 type: Boolean, 243 notify: true, 244 readOnly: true 245 }, 246 247 /** 248 * lastRequest's response. 249 * 250 * Note that lastResponse and lastError are set when lastRequest finishes, 251 * so if loading is true, then lastResponse and lastError will correspond 252 * to the result of the previous request. 253 * 254 * The type of the response is determined by the value of `handleAs` at 255 * the time that the request was generated. 256 * 257 * @type {Object} 258 */ 259 lastResponse: { 260 type: Object, 261 notify: true, 262 readOnly: true 263 }, 264 265 /** 266 * lastRequest's error, if any. 267 * 268 * @type {Object} 269 */ 270 lastError: { 271 type: Object, 272 notify: true, 273 readOnly: true 274 }, 275 276 /** 277 * An Array of all in-flight requests originating from this iron-ajax 278 * element. 279 */ 280 activeRequests: { 281 type: Array, 282 notify: true, 283 readOnly: true, 284 value: function() { 285 return []; 286 } 287 }, 288 289 /** 290 * Length of time in milliseconds to debounce multiple automatically generated requests. 291 */ 292 debounceDuration: { 293 type: Number, 294 value: 0, 295 notify: true 296 }, 297 298 /** 299 * Prefix to be stripped from a JSON response before parsing it. 300 * 301 * In order to prevent an attack using CSRF with Array responses 302 * (http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx/) 303 * many backends will mitigate this by prefixing all JSON response bodies 304 * with a string that would be nonsensical to a JavaScript parser. 305 * 306 */ 307 jsonPrefix: { 308 type: String, 309 value: '' 310 }, 311 312 /** 313 * By default, iron-ajax's events do not bubble. Setting this attribute will cause its 314 * request and response events as well as its iron-ajax-request, -response, and -error 315 * events to bubble to the window object. The vanilla error event never bubbles when 316 * using shadow dom even if this.bubbles is true because a scoped flag is not passed with 317 * it (first link) and because the shadow dom spec did not used to allow certain events, 318 * including events named error, to leak outside of shadow trees (second link). 319 * https://www.w3.org/TR/shadow-dom/#scoped-flag 320 * https://www.w3.org/TR/2015/WD-shadow-dom-20151215/#events-that-are-not-leaked-into-ancestor-trees 321 */ 322 bubbles: { 323 type: Boolean, 324 value: false 325 }, 326 327 /** 328 * Changes the [`completes`](iron-request#property-completes) promise chain 329 * from `generateRequest` to reject with an object 330 * containing the original request, as well an error message. 331 * If false (default), the promise rejects with an error message only. 332 */ 333 rejectWithRequest: { 334 type: Boolean, 335 value: false 336 }, 337 338 _boundHandleResponse: { 339 type: Function, 340 value: function() { 341 return this._handleResponse.bind(this); 342 } 343 } 344 }, 345 346 observers: [ 347 '_requestOptionsChanged(url, method, params.*, headers, contentType, ' + 348 'body, sync, handleAs, jsonPrefix, withCredentials, timeout, auto)' 349 ], 350 351 /** 352 * The query string that should be appended to the `url`, serialized from 353 * the current value of `params`. 354 * 355 * @return {string} 356 */ 357 get queryString () { 358 var queryParts = []; 359 var param; 360 var value; 361 362 for (param in this.params) { 363 value = this.params[param]; 364 param = window.encodeURIComponent(param); 365 366 if (Array.isArray(value)) { 367 for (var i = 0; i < value.length; i++) { 368 queryParts.push(param + '=' + window.encodeURIComponent(value[i])); 369 } 370 } else if (value !== null) { 371 queryParts.push(param + '=' + window.encodeURIComponent(value)); 372 } else { 373 queryParts.push(param); 374 } 375 } 376 377 return queryParts.join('&'); 378 }, 379 380 /** 381 * The `url` with query string (if `params` are specified), suitable for 382 * providing to an `iron-request` instance. 383 * 384 * @return {string} 385 */ 386 get requestUrl() { 387 var queryString = this.queryString; 388 var url = this.url || ''; 389 390 if (queryString) { 391 var bindingChar = url.indexOf('?') >= 0 ? '&' : '?'; 392 return url + bindingChar + queryString; 393 } 394 395 return url; 396 }, 397 398 /** 399 * An object that maps header names to header values, first applying the 400 * the value of `Content-Type` and then overlaying the headers specified 401 * in the `headers` property. 402 * 403 * @return {Object} 404 */ 405 get requestHeaders() { 406 var headers = {}; 407 var contentType = this.contentType; 408 if (contentType == null && (typeof this.body === 'string')) { 409 contentType = 'application/x-www-form-urlencoded'; 410 } 411 if (contentType) { 412 headers['content-type'] = contentType; 413 } 414 var header; 415 416 if (typeof this.headers === 'object') { 417 for (header in this.headers) { 418 headers[header] = this.headers[header].toString(); 419 } 420 } 421 422 return headers; 423 }, 424 425 /** 426 * Request options suitable for generating an `iron-request` instance based 427 * on the current state of the `iron-ajax` instance's properties. 428 * 429 * @return {{ 430 * url: string, 431 * method: (string|undefined), 432 * async: (boolean|undefined), 433 * body: (ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string|undefined|Object), 434 * headers: (Object|undefined), 435 * handleAs: (string|undefined), 436 * jsonPrefix: (string|undefined), 437 * withCredentials: (boolean|undefined)}} 438 */ 439 toRequestOptions: function() { 440 return { 441 url: this.requestUrl || '', 442 method: this.method, 443 headers: this.requestHeaders, 444 body: this.body, 445 async: !this.sync, 446 handleAs: this.handleAs, 447 jsonPrefix: this.jsonPrefix, 448 withCredentials: this.withCredentials, 449 timeout: this.timeout, 450 rejectWithRequest: this.rejectWithRequest, 451 }; 452 }, 453 454 /** 455 * Performs an AJAX request to the specified URL. 456 * 457 * @return {!IronRequestElement} 458 */ 459 generateRequest: function() { 460 var request = /** @type {!IronRequestElement} */ (document.createElement('iron-request')); 461 var requestOptions = this.toRequestOptions(); 462 463 this.push('activeRequests', request); 464 465 request.completes.then( 466 this._boundHandleResponse 467 ).catch( 468 this._handleError.bind(this, request) 469 ).then( 470 this._discardRequest.bind(this, request) 471 ); 472 473 var evt = this.fire('iron-ajax-presend', { 474 request: request, 475 options: requestOptions 476 }, {bubbles: this.bubbles, cancelable: true}); 477 478 if (evt.defaultPrevented) { 479 request.abort(); 480 request.rejectCompletes(request); 481 return request; 482 } 483 484 request.send(requestOptions); 485 486 this._setLastRequest(request); 487 this._setLoading(true); 488 489 this.fire('request', { 490 request: request, 491 options: requestOptions 492 }, { 493 bubbles: this.bubbles, 494 composed: true 495 }); 496 497 this.fire('iron-ajax-request', { 498 request: request, 499 options: requestOptions 500 }, { 501 bubbles: this.bubbles, 502 composed: true 503 }); 504 505 return request; 506 }, 507 508 _handleResponse: function(request) { 509 if (request === this.lastRequest) { 510 this._setLastResponse(request.response); 511 this._setLastError(null); 512 this._setLoading(false); 513 } 514 this.fire('response', request, { 515 bubbles: this.bubbles, 516 composed: true 517 }); 518 this.fire('iron-ajax-response', request, { 519 bubbles: this.bubbles, 520 composed: true 521 }); 522 }, 523 524 _handleError: function(request, error) { 525 if (this.verbose) { 526 Polymer.Base._error(error); 527 } 528 529 if (request === this.lastRequest) { 530 this._setLastError({ 531 request: request, 532 error: error, 533 status: request.xhr.status, 534 statusText: request.xhr.statusText, 535 response: request.xhr.response 536 }); 537 this._setLastResponse(null); 538 this._setLoading(false); 539 } 540 541 // Tests fail if this goes after the normal this.fire('error', ...) 542 this.fire('iron-ajax-error', { 543 request: request, 544 error: error 545 }, { 546 bubbles: this.bubbles, 547 composed: true 548 }); 549 550 this.fire('error', { 551 request: request, 552 error: error 553 }, { 554 bubbles: this.bubbles, 555 composed: true 556 }); 557 }, 558 559 _discardRequest: function(request) { 560 var requestIndex = this.activeRequests.indexOf(request); 561 562 if (requestIndex > -1) { 563 this.splice('activeRequests', requestIndex, 1); 564 } 565 }, 566 567 _requestOptionsChanged: function() { 568 this.debounce('generate-request', function() { 569 if (this.url == null) { 570 return; 571 } 572 573 if (this.auto) { 574 this.generateRequest(); 575 } 576 }, this.debounceDuration); 577 }, 578 579 }); 580</script> 581