• 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-ajax/iron-ajax.html">
13
14<!--
15`<iron-form>` is a wrapper around the HTML `<form>` element, that can
16validate and submit both custom and native HTML elements. Note that this
17is a breaking change from iron-form 1.0, which was a type extension.
18
19It has two modes: if `allow-redirect` is true, then after the form submission you
20will be redirected to the server response. Otherwise, if it is false, it will
21use an `iron-ajax` element to submit the form contents to the server.
22
23  Example:
24
25    <iron-form>
26      <form method="get" action="/form/handler">
27        <input type="text" name="name" value="Batman">
28        <input type="checkbox" name="donuts" checked> I like donuts<br>
29        <paper-checkbox name="cheese" value="yes" checked></paper-checkbox>
30      </form>
31    </iron-form>
32
33By default, a native `<button>` element will submit this form. However, if you
34want to submit it from a custom element's click handler, you need to explicitly
35call the `iron-form`'s `submit` method.
36
37  Example:
38
39    <paper-button raised onclick="submitForm()">Submit</paper-button>
40
41    function submitForm() {
42      document.getElementById('iron-form').submit();
43    }
44
45If you are not using the `allow-redirect` mode, then you also have the option of
46customizing the request sent to the server. To do so, you can listen to the `iron-form-presubmit`
47event, and modify the form's [`iron-ajax`](https://elements.polymer-project.org/elements/iron-ajax)
48object. However, If you want to not use `iron-ajax` at all, you can cancel the
49event and do your own custom submission:
50
51  Example of modifying the request, but still using the build-in form submission:
52
53    form.addEventListener('iron-form-presubmit', function() {
54      this.request.method = 'put';
55      this.request.params['extraParam'] = 'someValue';
56    });
57
58  Example of bypassing the build-in form submission:
59
60    form.addEventListener('iron-form-presubmit', function(event) {
61      event.preventDefault();
62      var firebase = new Firebase(form.getAttribute('action'));
63      firebase.set(form.serializeForm());
64    });
65
66Note that if you're dynamically creating this element, it's mandatory that you
67first create the contained `<form>` element and all its children, and only then
68attach it to the `<iron-form>`:
69
70  var wrapper = document.createElement('iron-form');
71  var form = document.createElement('form');
72  var input = document.createElement('input');
73  form.appendChild(input);
74  document.body.appendChild(wrapper);
75  wrapper.appendChild(form);
76
77@element iron-form
78@hero hero.svg
79@demo demo/index.html
80-->
81
82<dom-module id="iron-form">
83  <template>
84    <style>
85      :host {
86        display: block;
87      }
88    </style>
89
90    <!-- This form is used to collect the elements that should be submitted -->
91    <slot></slot>
92
93    <!-- This form is used for submission -->
94    <form id="helper" action$="[[action]]" method$="[[method]]" enctype$="[[enctype]]"></form>
95  </template>
96
97  <script>
98    Polymer({
99      is: 'iron-form',
100
101      properties: {
102        /*
103         * Set this to true if you don't want the form to be submitted through an
104         * ajax request, and you want the page to redirect to the action URL
105         * after the form has been submitted.
106         */
107        allowRedirect: {
108          type: Boolean,
109          value: false
110        },
111        /**
112        * HTTP request headers to send. See PolymerElements/iron-ajax for
113        * more details. Only works when `allowRedirect` is false.
114        */
115        headers: {
116          type: Object,
117          value: function() { return {}; }
118        },
119        /**
120        * Set the `withCredentials` flag on the request. See PolymerElements/iron-ajax for
121        * more details. Only works when `allowRedirect` is false.
122        */
123        withCredentials: {
124          type: Boolean,
125          value: false
126        },
127      },
128      /**
129       * Fired if the form cannot be submitted because it's invalid.
130       *
131       * @event iron-form-invalid
132       */
133
134      /**
135       * Fired after the form is submitted.
136       *
137       * @event iron-form-submit
138       */
139
140      /**
141       * Fired before the form is submitted.
142       *
143       * @event iron-form-presubmit
144       */
145
146      /**
147       * Fired after the form is submitted and a response is received. An
148       * IronRequestElement is included as the event.detail object.
149       *
150       * @event iron-form-response
151      */
152
153      /**
154       * Fired after the form is submitted and an error is received. An
155       * error message is included in event.detail.error and an
156       * IronRequestElement is included in event.detail.request.
157       *
158       * @event iron-form-error
159      */
160
161      attached: function() {
162        this._nodeObserver = Polymer.dom(this).observeNodes(
163          function(mutations) {
164            for (var i = 0; i < mutations.addedNodes.length; i++) {
165              if (mutations.addedNodes[i].tagName === 'FORM' && !this._alreadyCalledInit) {
166                this._alreadyCalledInit = true;
167                this._form = mutations.addedNodes[i];
168                this._init();
169              }
170            }
171          }.bind(this));
172      },
173
174      detached: function() {
175        if (this._nodeObserver) {
176          Polymer.dom(this).unobserveNodes(this._nodeObserver);
177          this._nodeObserver = null;
178        }
179      },
180
181      _init: function() {
182        this._form.addEventListener('submit', this.submit.bind(this));
183        this._form.addEventListener('reset', this.reset.bind(this));
184
185        // Save the initial values.
186        this._defaults = this._defaults || new WeakMap();
187        var nodes = this._getSubmittableElements();
188        for (var i = 0; i < nodes.length; i++) {
189          var node = nodes[i];
190          if (!this._defaults.has(node)) {
191            this._defaults.set(node, {
192              checked: node.checked,
193              value: node.value,
194            });
195          }
196        }
197      },
198
199      /**
200       * Validates all the required elements (custom and native) in the form.
201       * @return {boolean} True if all the elements are valid.
202       */
203      validate: function() {
204        if (this._form.getAttribute('novalidate') === '')
205          return true;
206
207        // Start by making the form check the native elements it knows about.
208        var valid = this._form.checkValidity();
209        var elements = this._getValidatableElements();
210
211        // Go through all the elements, and validate the custom ones.
212        for (var el, i = 0; el = elements[i], i < elements.length; i++) {
213          // This is weird to appease the compiler. We assume the custom element
214          // has a validate() method, otherwise we can't check it.
215          var validatable = /** @type {{validate: (function() : boolean)}} */ (el);
216          if (validatable.validate) {
217            valid = !!validatable.validate() && valid;
218          }
219        }
220        return valid;
221      },
222
223      /**
224       * Submits the form.
225       */
226      submit: function(event) {
227        // We are not using this form for submission, so always cancel its event.
228        if (event) {
229          event.preventDefault();
230        }
231
232        // If you've called this before distribution happened, bail out.
233        if (!this._form) {
234          return;
235        }
236
237        if (!this.validate()) {
238          this.fire('iron-form-invalid');
239          return;
240        }
241
242        // Remove any existing children in the submission form (from a previous submit).
243        this.$.helper.textContent = '';
244
245        var json = this.serializeForm();
246
247        // If we want a redirect, submit the form natively.
248        if (this.allowRedirect) {
249          // If we're submitting the form natively, then create a hidden element for
250          // each of the values.
251          for (var element in json) {
252            this.$.helper.appendChild(this._createHiddenElement(element, json[element]));
253          }
254
255          // Copy the original form attributes.
256          this.$.helper.action = this._form.getAttribute('action');
257          this.$.helper.method = this._form.getAttribute('method') || 'GET';
258          this.$.helper.contentType = this._form.getAttribute('enctype') || 'application/x-www-form-urlencoded';
259
260          this.$.helper.submit();
261          this.fire('iron-form-submit');
262        } else {
263          this._makeAjaxRequest(json);
264        }
265      },
266
267      /**
268       * Resets the form to the default values.
269       */
270      reset: function(event) {
271        // We are not using this form for submission, so always cancel its event.
272        if (event)
273          event.preventDefault();
274
275        // If you've called this before distribution happened, bail out.
276        if (!this._form) {
277          return;
278        }
279
280        // Load the initial values.
281        var nodes = this._getSubmittableElements();
282        for (var i = 0; i < nodes.length; i++) {
283          var node = nodes[i];
284          if (this._defaults.has(node)) {
285            var defaults = this._defaults.get(node);
286            node.value = defaults.value;
287            node.checked = defaults.checked;
288          }
289        }
290      },
291
292      /**
293       * Serializes the form as will be used in submission. Note that `serialize`
294       * is a Polymer reserved keyword, so calling `someIronForm`.serialize()`
295       * will give you unexpected results.
296       * @return {Object} An object containing name-value pairs for elements that
297       *                  would be submitted.
298       */
299      serializeForm: function() {
300        // Only elements that have a `name` and are not disabled are submittable.
301        var elements = this._getSubmittableElements();
302        var json = {};
303        for (var i = 0; i < elements.length; i++) {
304          var values = this._serializeElementValues(elements[i]);
305          for (var v = 0; v < values.length; v++) {
306            this._addSerializedElement(json, elements[i].name, values[v]);
307          }
308        }
309        return json;
310      },
311
312      _handleFormResponse: function (event) {
313        this.fire('iron-form-response', event.detail);
314      },
315
316      _handleFormError: function (event) {
317        this.fire('iron-form-error', event.detail);
318      },
319
320      _makeAjaxRequest: function(json) {
321        // Initialize the iron-ajax element if we haven't already.
322        if (!this.request) {
323          this.request = document.createElement('iron-ajax');
324          this.request.addEventListener('response', this._handleFormResponse.bind(this));
325          this.request.addEventListener('error', this._handleFormError.bind(this));
326        }
327
328        // Native forms can also index elements magically by their name (can't make
329        // this up if I tried) so we need to get the correct attributes, not the
330        // elements with those names.
331        this.request.url = this._form.getAttribute('action');
332        this.request.method = this._form.getAttribute('method') || 'GET';
333        this.request.contentType = this._form.getAttribute('enctype') || 'application/x-www-form-urlencoded';
334        this.request.withCredentials = this.withCredentials;
335        this.request.headers = this.headers;
336
337        if (this._form.method.toUpperCase() === 'POST') {
338          this.request.body = json;
339        } else {
340          this.request.params = json;
341        }
342
343        // Allow for a presubmit hook
344        var event = this.fire('iron-form-presubmit', {}, {cancelable: true});
345        if(!event.defaultPrevented) {
346          this.request.generateRequest();
347          this.fire('iron-form-submit', json);
348        }
349      },
350
351      _getValidatableElements: function() {
352        return this._findElements(this._form, true);
353      },
354
355      _getSubmittableElements: function() {
356        return this._findElements(this._form, false);
357      },
358
359      _findElements: function(parent, ignoreName) {
360        var nodes = Polymer.dom(parent).querySelectorAll('*');
361        var submittable = [];
362
363        for (var i = 0; i < nodes.length; i++) {
364          var node = nodes[i];
365          // An element is submittable if it is not disabled, and if it has a
366          // 'name' attribute.
367          if(!node.disabled && (ignoreName || node.name)) {
368            submittable.push(node);
369          }
370          else {
371            // This element has a root which could contain more submittable elements.
372            if(node.root) {
373              Array.prototype.push.apply(submittable, this._findElements(node.root, ignoreName));
374            }
375          }
376        }
377        return submittable;
378      },
379
380      _serializeElementValues: function(element) {
381        // We will assume that every custom element that needs to be serialized
382        // has a `value` property, and it contains the correct value.
383        // The only weird one is an element that implements IronCheckedElementBehaviour,
384        // in which case like the native checkbox/radio button, it's only used
385        // when checked.
386        // For native elements, from https://www.w3.org/TR/html5/forms.html#the-form-element.
387        // Native submittable elements: button, input, keygen, object, select, textarea;
388        // 1. We will skip `keygen and `object` for this iteration, and deal with
389        // them if they're actually required.
390        // 2. <button> and <textarea> have a `value` property, so they behave like
391        //    the custom elements.
392        // 3. <select> can have multiple options selected, in which case its
393        //    `value` is incorrect, and we must use the values of each of its
394        //    `selectedOptions`
395        // 4. <input> can have a whole bunch of behaviours, so it's handled separately.
396        // 5. Buttons are hard. The button that was clicked to submit the form
397        //    is the one who's name/value gets sent to the server.
398        var tag = element.tagName.toLowerCase();
399        if (tag === 'button' || (tag === 'input' && (element.type === 'submit' || element.type === 'reset'))) {
400          return [];
401        }
402
403        if (tag === 'select') {
404          return this._serializeSelectValues(element);
405        } else if (tag === 'input') {
406          return this._serializeInputValues(element);
407        } else {
408          if (element['_hasIronCheckedElementBehavior'] && !element.checked)
409            return [];
410          return [element.value];
411        }
412      },
413
414      _serializeSelectValues: function(element) {
415        var values = [];
416
417        // A <select multiple> has an array of options, some of which can be selected.
418        for (var i = 0; i < element.options.length; i++) {
419          if (element.options[i].selected) {
420            values.push(element.options[i].value)
421          }
422        }
423        return values;
424      },
425
426      _serializeInputValues: function(element) {
427        // Most of the inputs use their 'value' attribute, with the exception
428        // of radio buttons, checkboxes and file.
429        var type = element.type.toLowerCase();
430
431        // Don't do anything for unchecked checkboxes/radio buttons.
432        // Don't do anything for file, since that requires a different request.
433        if (((type === 'checkbox' || type === 'radio') && !element.checked) ||
434            type === 'file') {
435          return [];
436        }
437        return [element.value];
438      },
439
440      _createHiddenElement: function(name, value) {
441        var input = document.createElement("input");
442        input.setAttribute("type", "hidden");
443        input.setAttribute("name", name);
444        input.setAttribute("value", value);
445        return input;
446      },
447
448      _addSerializedElement: function(json, name, value) {
449        // If the name doesn't exist, add it. Otherwise, serialize it to
450        // an array,
451        if (json[name] === undefined) {
452          json[name] = value;
453        } else {
454          if (!Array.isArray(json[name])) {
455            json[name] = [json[name]];
456          }
457          json[name].push(value);
458        }
459      }
460    });
461  </script>
462</dom-module>
463