• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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