• 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
13<!--
14iron-request can be used to perform XMLHttpRequests.
15
16    <iron-request id="xhr"></iron-request>
17    ...
18    this.$.xhr.send({url: url, body: params});
19-->
20<script>
21  'use strict';
22
23  Polymer({
24    is: 'iron-request',
25
26    hostAttributes: {
27      hidden: true
28    },
29
30    properties: {
31
32      /**
33       * A reference to the XMLHttpRequest instance used to generate the
34       * network request.
35       *
36       * @type {XMLHttpRequest}
37       */
38      xhr: {
39        type: Object,
40        notify: true,
41        readOnly: true,
42        value: function() {
43          return new XMLHttpRequest();
44        }
45      },
46
47      /**
48       * A reference to the parsed response body, if the `xhr` has completely
49       * resolved.
50       *
51       * @type {*}
52       * @default null
53       */
54      response: {
55        type: Object,
56        notify: true,
57        readOnly: true,
58        value: function() {
59          return null;
60        }
61      },
62
63      /**
64       * A reference to the status code, if the `xhr` has completely resolved.
65       */
66      status: {
67        type: Number,
68        notify: true,
69        readOnly: true,
70        value: 0
71      },
72
73      /**
74       * A reference to the status text, if the `xhr` has completely resolved.
75       */
76      statusText: {
77        type: String,
78        notify: true,
79        readOnly: true,
80        value: ''
81      },
82
83      /**
84       * A promise that resolves when the `xhr` response comes back, or rejects
85       * if there is an error before the `xhr` completes.
86       * The resolve callback is called with the original request as an argument.
87       * By default, the reject callback is called with an `Error` as an argument.
88       * If `rejectWithRequest` is true, the reject callback is called with an
89       * object with two keys: `request`, the original request, and `error`, the
90       * error object.
91       *
92       * @type {Promise}
93       */
94      completes: {
95        type: Object,
96        readOnly: true,
97        notify: true,
98        value: function() {
99          return new Promise(function(resolve, reject) {
100            this.resolveCompletes = resolve;
101            this.rejectCompletes = reject;
102          }.bind(this));
103        }
104      },
105
106      /**
107       * An object that contains progress information emitted by the XHR if
108       * available.
109       *
110       * @default {}
111       */
112      progress: {
113        type: Object,
114        notify: true,
115        readOnly: true,
116        value: function() {
117          return {};
118        }
119      },
120
121      /**
122       * Aborted will be true if an abort of the request is attempted.
123       */
124      aborted: {
125        type: Boolean,
126        notify: true,
127        readOnly: true,
128        value: false,
129      },
130
131      /**
132       * Errored will be true if the browser fired an error event from the
133       * XHR object (mainly network errors).
134       */
135      errored: {
136        type: Boolean,
137        notify: true,
138        readOnly: true,
139        value: false
140      },
141
142      /**
143       * TimedOut will be true if the XHR threw a timeout event.
144       */
145      timedOut: {
146        type: Boolean,
147        notify: true,
148        readOnly: true,
149        value: false
150      }
151    },
152
153    /**
154     * Succeeded is true if the request succeeded. The request succeeded if it
155     * loaded without error, wasn't aborted, and the status code is ≥ 200, and
156     * < 300, or if the status code is 0.
157     *
158     * The status code 0 is accepted as a success because some schemes - e.g.
159     * file:// - don't provide status codes.
160     *
161     * @return {boolean}
162     */
163    get succeeded() {
164      if (this.errored || this.aborted || this.timedOut) {
165        return false;
166      }
167      var status = this.xhr.status || 0;
168
169      // Note: if we are using the file:// protocol, the status code will be 0
170      // for all outcomes (successful or otherwise).
171      return status === 0 ||
172        (status >= 200 && status < 300);
173    },
174
175    /**
176     * Sends an HTTP request to the server and returns a promise (see the `completes`
177     * property for details).
178     *
179     * The handling of the `body` parameter will vary based on the Content-Type
180     * header. See the docs for iron-ajax's `body` property for details.
181     *
182     * @param {{
183     *   url: string,
184     *   method: (string|undefined),
185     *   async: (boolean|undefined),
186     *   body: (ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string|undefined|Object),
187     *   headers: (Object|undefined),
188     *   handleAs: (string|undefined),
189     *   jsonPrefix: (string|undefined),
190     *   withCredentials: (boolean|undefined),
191     *   timeout: (Number|undefined),
192     *   rejectWithRequest: (boolean|undefined)}} options -
193     *   - url The url to which the request is sent.
194     *   - method The HTTP method to use, default is GET.
195     *   - async By default, all requests are sent asynchronously. To send synchronous requests,
196     *         set to false.
197     *   -  body The content for the request body for POST method.
198     *   -  headers HTTP request headers.
199     *   -  handleAs The response type. Default is 'text'.
200     *   -  withCredentials Whether or not to send credentials on the request. Default is false.
201     *   -  timeout - Timeout for request, in milliseconds.
202     *   -  rejectWithRequest Set to true to include the request object with promise rejections.
203     * @return {Promise}
204     */
205    send: function(options) {
206      var xhr = this.xhr;
207
208      if (xhr.readyState > 0) {
209        return null;
210      }
211
212      xhr.addEventListener('progress', function(progress) {
213        this._setProgress({
214          lengthComputable: progress.lengthComputable,
215          loaded: progress.loaded,
216          total: progress.total
217        });
218      }.bind(this));
219
220      xhr.addEventListener('error', function(error) {
221        this._setErrored(true);
222        this._updateStatus();
223        var response = options.rejectWithRequest ? {
224          error: error,
225          request: this
226        } : error;
227        this.rejectCompletes(response);
228      }.bind(this));
229
230      xhr.addEventListener('timeout', function(error) {
231        this._setTimedOut(true);
232        this._updateStatus();
233        var response = options.rejectWithRequest ? {
234          error: error,
235          request: this
236        } : error;
237        this.rejectCompletes(response);
238      }.bind(this));
239
240      xhr.addEventListener('abort', function() {
241        this._setAborted(true);
242        this._updateStatus();
243        var error = new Error('Request aborted.');
244        var response = options.rejectWithRequest ? {
245          error: error,
246          request: this
247        } : error;
248        this.rejectCompletes(response);
249      }.bind(this));
250
251      // Called after all of the above.
252      xhr.addEventListener('loadend', function() {
253        this._updateStatus();
254        this._setResponse(this.parseResponse());
255
256        if (!this.succeeded) {
257          var error = new Error('The request failed with status code: ' + this.xhr.status);
258          var response = options.rejectWithRequest ? {
259            error: error,
260            request: this
261          } : error;
262          this.rejectCompletes(response);
263          return;
264        }
265
266        this.resolveCompletes(this);
267      }.bind(this));
268
269      this.url = options.url;
270      xhr.open(
271        options.method || 'GET',
272        options.url,
273        options.async !== false
274      );
275
276      var acceptType = {
277        'json': 'application/json',
278        'text': 'text/plain',
279        'html': 'text/html',
280        'xml': 'application/xml',
281        'arraybuffer': 'application/octet-stream'
282      }[options.handleAs];
283      var headers = options.headers || Object.create(null);
284      var newHeaders = Object.create(null);
285      for (var key in headers) {
286        newHeaders[key.toLowerCase()] = headers[key];
287      }
288      headers = newHeaders;
289
290      if (acceptType && !headers['accept']) {
291        headers['accept'] = acceptType;
292      }
293      Object.keys(headers).forEach(function(requestHeader) {
294        if (/[A-Z]/.test(requestHeader)) {
295          Polymer.Base._error('Headers must be lower case, got', requestHeader);
296        }
297        xhr.setRequestHeader(
298          requestHeader,
299          headers[requestHeader]
300        );
301      }, this);
302
303      if (options.async !== false) {
304        if (options.async) {
305          xhr.timeout = options.timeout;
306        }
307
308        var handleAs = options.handleAs;
309
310        // If a JSON prefix is present, the responseType must be 'text' or the
311        // browser won’t be able to parse the response.
312        if (!!options.jsonPrefix || !handleAs) {
313          handleAs = 'text';
314        }
315
316        // In IE, `xhr.responseType` is an empty string when the response
317        // returns. Hence, caching it as `xhr._responseType`.
318        xhr.responseType = xhr._responseType = handleAs;
319
320        // Cache the JSON prefix, if it exists.
321        if (!!options.jsonPrefix) {
322          xhr._jsonPrefix = options.jsonPrefix;
323        }
324      }
325
326      xhr.withCredentials = !!options.withCredentials;
327
328
329      var body = this._encodeBodyObject(options.body, headers['content-type']);
330
331      xhr.send(
332        /** @type {ArrayBuffer|ArrayBufferView|Blob|Document|FormData|
333                   null|string|undefined} */
334        (body));
335
336      return this.completes;
337    },
338
339    /**
340     * Attempts to parse the response body of the XHR. If parsing succeeds,
341     * the value returned will be deserialized based on the `responseType`
342     * set on the XHR.
343     *
344     * @return {*} The parsed response,
345     * or undefined if there was an empty response or parsing failed.
346     */
347    parseResponse: function() {
348      var xhr = this.xhr;
349      var responseType = xhr.responseType || xhr._responseType;
350      var preferResponseText = !this.xhr.responseType;
351      var prefixLen = (xhr._jsonPrefix && xhr._jsonPrefix.length) || 0;
352
353      try {
354        switch (responseType) {
355          case 'json':
356            // If the xhr object doesn't have a natural `xhr.responseType`,
357            // we can assume that the browser hasn't parsed the response for us,
358            // and so parsing is our responsibility. Likewise if response is
359            // undefined, as there's no way to encode undefined in JSON.
360            if (preferResponseText || xhr.response === undefined) {
361              // Try to emulate the JSON section of the response body section of
362              // the spec: https://xhr.spec.whatwg.org/#response-body
363              // That is to say, we try to parse as JSON, but if anything goes
364              // wrong return null.
365              try {
366                return JSON.parse(xhr.responseText);
367              } catch (_) {
368                return null;
369              }
370            }
371
372            return xhr.response;
373          case 'xml':
374            return xhr.responseXML;
375          case 'blob':
376          case 'document':
377          case 'arraybuffer':
378            return xhr.response;
379          case 'text':
380          default: {
381            // If `prefixLen` is set, it implies the response should be parsed
382            // as JSON once the prefix of length `prefixLen` is stripped from
383            // it. Emulate the behavior above where null is returned on failure
384            // to parse.
385            if (prefixLen) {
386              try {
387                return JSON.parse(xhr.responseText.substring(prefixLen));
388              } catch (_) {
389                return null;
390              }
391            }
392            return xhr.responseText;
393          }
394        }
395      } catch (e) {
396        this.rejectCompletes(new Error('Could not parse response. ' + e.message));
397      }
398    },
399
400    /**
401     * Aborts the request.
402     */
403    abort: function() {
404      this._setAborted(true);
405      this.xhr.abort();
406    },
407
408    /**
409     * @param {*} body The given body of the request to try and encode.
410     * @param {?string} contentType The given content type, to infer an encoding
411     *     from.
412     * @return {*} Either the encoded body as a string, if successful,
413     *     or the unaltered body object if no encoding could be inferred.
414     */
415    _encodeBodyObject: function(body, contentType) {
416      if (typeof body == 'string') {
417        return body;  // Already encoded.
418      }
419      var bodyObj = /** @type {Object} */ (body);
420      switch(contentType) {
421        case('application/json'):
422          return JSON.stringify(bodyObj);
423        case('application/x-www-form-urlencoded'):
424          return this._wwwFormUrlEncode(bodyObj);
425      }
426      return body;
427    },
428
429    /**
430     * @param {Object} object The object to encode as x-www-form-urlencoded.
431     * @return {string} .
432     */
433    _wwwFormUrlEncode: function(object) {
434      if (!object) {
435        return '';
436      }
437      var pieces = [];
438      Object.keys(object).forEach(function(key) {
439        // TODO(rictic): handle array values here, in a consistent way with
440        //   iron-ajax params.
441        pieces.push(
442            this._wwwFormUrlEncodePiece(key) + '=' +
443            this._wwwFormUrlEncodePiece(object[key]));
444      }, this);
445      return pieces.join('&');
446    },
447
448    /**
449     * @param {*} str A key or value to encode as x-www-form-urlencoded.
450     * @return {string} .
451     */
452    _wwwFormUrlEncodePiece: function(str) {
453      // Spec says to normalize newlines to \r\n and replace %20 spaces with +.
454      // jQuery does this as well, so this is likely to be widely compatible.
455      if (str === null || str === undefined || !str.toString) {
456        return '';
457      }
458
459      return encodeURIComponent(str.toString().replace(/\r?\n/g, '\r\n'))
460        .replace(/%20/g, '+');
461    },
462
463    /**
464     * Updates the status code and status text.
465     */
466    _updateStatus: function() {
467      this._setStatus(this.xhr.status);
468      this._setStatusText((this.xhr.statusText === undefined) ? '' : this.xhr.statusText);
469    }
470  });
471</script>
472