| <!-- |
| @license |
| 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-ajax/iron-ajax.html"> |
| |
| <!-- |
| `<iron-form>` is a wrapper around the HTML `<form>` element, that can |
| validate and submit both custom and native HTML elements. Note that this |
| is a breaking change from iron-form 1.0, which was a type extension. |
| |
| It has two modes: if `allow-redirect` is true, then after the form submission you |
| will be redirected to the server response. Otherwise, if it is false, it will |
| use an `iron-ajax` element to submit the form contents to the server. |
| |
| Example: |
| |
| <iron-form> |
| <form method="get" action="/form/handler"> |
| <input type="text" name="name" value="Batman"> |
| <input type="checkbox" name="donuts" checked> I like donuts<br> |
| <paper-checkbox name="cheese" value="yes" checked></paper-checkbox> |
| </form> |
| </iron-form> |
| |
| By default, a native `<button>` element will submit this form. However, if you |
| want to submit it from a custom element's click handler, you need to explicitly |
| call the `iron-form`'s `submit` method. |
| |
| Example: |
| |
| <paper-button raised onclick="submitForm()">Submit</paper-button> |
| |
| function submitForm() { |
| document.getElementById('iron-form').submit(); |
| } |
| |
| If you are not using the `allow-redirect` mode, then you also have the option of |
| customizing the request sent to the server. To do so, you can listen to the `iron-form-presubmit` |
| event, and modify the form's [`iron-ajax`](https://elements.polymer-project.org/elements/iron-ajax) |
| object. However, If you want to not use `iron-ajax` at all, you can cancel the |
| event and do your own custom submission: |
| |
| Example of modifying the request, but still using the build-in form submission: |
| |
| form.addEventListener('iron-form-presubmit', function() { |
| this.request.method = 'put'; |
| this.request.params['extraParam'] = 'someValue'; |
| }); |
| |
| Example of bypassing the build-in form submission: |
| |
| form.addEventListener('iron-form-presubmit', function(event) { |
| event.preventDefault(); |
| var firebase = new Firebase(form.getAttribute('action')); |
| firebase.set(form.serializeForm()); |
| }); |
| |
| Note that if you're dynamically creating this element, it's mandatory that you |
| first create the contained `<form>` element and all its children, and only then |
| attach it to the `<iron-form>`: |
| |
| var wrapper = document.createElement('iron-form'); |
| var form = document.createElement('form'); |
| var input = document.createElement('input'); |
| form.appendChild(input); |
| document.body.appendChild(wrapper); |
| wrapper.appendChild(form); |
| |
| @element iron-form |
| @hero hero.svg |
| @demo demo/index.html |
| --> |
| |
| <dom-module id="iron-form"> |
| <template> |
| <style> |
| :host { |
| display: block; |
| } |
| </style> |
| |
| <!-- This form is used to collect the elements that should be submitted --> |
| <slot></slot> |
| |
| <!-- This form is used for submission --> |
| <form id="helper" action$="[[action]]" method$="[[method]]" enctype$="[[enctype]]"></form> |
| </template> |
| |
| <script> |
| Polymer({ |
| is: 'iron-form', |
| |
| properties: { |
| /* |
| * Set this to true if you don't want the form to be submitted through an |
| * ajax request, and you want the page to redirect to the action URL |
| * after the form has been submitted. |
| */ |
| allowRedirect: { |
| type: Boolean, |
| value: false |
| }, |
| /** |
| * HTTP request headers to send. See PolymerElements/iron-ajax for |
| * more details. Only works when `allowRedirect` is false. |
| */ |
| headers: { |
| type: Object, |
| value: function() { return {}; } |
| }, |
| /** |
| * Set the `withCredentials` flag on the request. See PolymerElements/iron-ajax for |
| * more details. Only works when `allowRedirect` is false. |
| */ |
| withCredentials: { |
| type: Boolean, |
| value: false |
| }, |
| }, |
| /** |
| * Fired if the form cannot be submitted because it's invalid. |
| * |
| * @event iron-form-invalid |
| */ |
| |
| /** |
| * Fired after the form is submitted. |
| * |
| * @event iron-form-submit |
| */ |
| |
| /** |
| * Fired before the form is submitted. |
| * |
| * @event iron-form-presubmit |
| */ |
| |
| /** |
| * Fired after the form is submitted and a response is received. An |
| * IronRequestElement is included as the event.detail object. |
| * |
| * @event iron-form-response |
| */ |
| |
| /** |
| * Fired after the form is submitted and an error is received. An |
| * error message is included in event.detail.error and an |
| * IronRequestElement is included in event.detail.request. |
| * |
| * @event iron-form-error |
| */ |
| |
| attached: function() { |
| this._nodeObserver = Polymer.dom(this).observeNodes( |
| function(mutations) { |
| for (var i = 0; i < mutations.addedNodes.length; i++) { |
| if (mutations.addedNodes[i].tagName === 'FORM' && !this._alreadyCalledInit) { |
| this._alreadyCalledInit = true; |
| this._form = mutations.addedNodes[i]; |
| this._init(); |
| } |
| } |
| }.bind(this)); |
| }, |
| |
| detached: function() { |
| if (this._nodeObserver) { |
| Polymer.dom(this).unobserveNodes(this._nodeObserver); |
| this._nodeObserver = null; |
| } |
| }, |
| |
| _init: function() { |
| this._form.addEventListener('submit', this.submit.bind(this)); |
| this._form.addEventListener('reset', this.reset.bind(this)); |
| |
| // Save the initial values. |
| this._defaults = this._defaults || new WeakMap(); |
| var nodes = this._getSubmittableElements(); |
| for (var i = 0; i < nodes.length; i++) { |
| var node = nodes[i]; |
| if (!this._defaults.has(node)) { |
| this._defaults.set(node, { |
| checked: node.checked, |
| value: node.value, |
| }); |
| } |
| } |
| }, |
| |
| /** |
| * Validates all the required elements (custom and native) in the form. |
| * @return {boolean} True if all the elements are valid. |
| */ |
| validate: function() { |
| if (this._form.getAttribute('novalidate') === '') |
| return true; |
| |
| // Start by making the form check the native elements it knows about. |
| var valid = this._form.checkValidity(); |
| var elements = this._getValidatableElements(); |
| |
| // Go through all the elements, and validate the custom ones. |
| for (var el, i = 0; el = elements[i], i < elements.length; i++) { |
| // This is weird to appease the compiler. We assume the custom element |
| // has a validate() method, otherwise we can't check it. |
| var validatable = /** @type {{validate: (function() : boolean)}} */ (el); |
| if (validatable.validate) { |
| valid = !!validatable.validate() && valid; |
| } |
| } |
| return valid; |
| }, |
| |
| /** |
| * Submits the form. |
| */ |
| submit: function(event) { |
| // We are not using this form for submission, so always cancel its event. |
| if (event) { |
| event.preventDefault(); |
| } |
| |
| // If you've called this before distribution happened, bail out. |
| if (!this._form) { |
| return; |
| } |
| |
| if (!this.validate()) { |
| this.fire('iron-form-invalid'); |
| return; |
| } |
| |
| // Remove any existing children in the submission form (from a previous submit). |
| this.$.helper.textContent = ''; |
| |
| var json = this.serializeForm(); |
| |
| // If we want a redirect, submit the form natively. |
| if (this.allowRedirect) { |
| // If we're submitting the form natively, then create a hidden element for |
| // each of the values. |
| for (var element in json) { |
| this.$.helper.appendChild(this._createHiddenElement(element, json[element])); |
| } |
| |
| // Copy the original form attributes. |
| this.$.helper.action = this._form.getAttribute('action'); |
| this.$.helper.method = this._form.getAttribute('method') || 'GET'; |
| this.$.helper.contentType = this._form.getAttribute('enctype') || 'application/x-www-form-urlencoded'; |
| |
| this.$.helper.submit(); |
| this.fire('iron-form-submit'); |
| } else { |
| this._makeAjaxRequest(json); |
| } |
| }, |
| |
| /** |
| * Resets the form to the default values. |
| */ |
| reset: function(event) { |
| // We are not using this form for submission, so always cancel its event. |
| if (event) |
| event.preventDefault(); |
| |
| // If you've called this before distribution happened, bail out. |
| if (!this._form) { |
| return; |
| } |
| |
| // Load the initial values. |
| var nodes = this._getSubmittableElements(); |
| for (var i = 0; i < nodes.length; i++) { |
| var node = nodes[i]; |
| if (this._defaults.has(node)) { |
| var defaults = this._defaults.get(node); |
| node.value = defaults.value; |
| node.checked = defaults.checked; |
| } |
| } |
| }, |
| |
| /** |
| * Serializes the form as will be used in submission. Note that `serialize` |
| * is a Polymer reserved keyword, so calling `someIronForm`.serialize()` |
| * will give you unexpected results. |
| * @return {Object} An object containing name-value pairs for elements that |
| * would be submitted. |
| */ |
| serializeForm: function() { |
| // Only elements that have a `name` and are not disabled are submittable. |
| var elements = this._getSubmittableElements(); |
| var json = {}; |
| for (var i = 0; i < elements.length; i++) { |
| var values = this._serializeElementValues(elements[i]); |
| for (var v = 0; v < values.length; v++) { |
| this._addSerializedElement(json, elements[i].name, values[v]); |
| } |
| } |
| return json; |
| }, |
| |
| _handleFormResponse: function (event) { |
| this.fire('iron-form-response', event.detail); |
| }, |
| |
| _handleFormError: function (event) { |
| this.fire('iron-form-error', event.detail); |
| }, |
| |
| _makeAjaxRequest: function(json) { |
| // Initialize the iron-ajax element if we haven't already. |
| if (!this.request) { |
| this.request = document.createElement('iron-ajax'); |
| this.request.addEventListener('response', this._handleFormResponse.bind(this)); |
| this.request.addEventListener('error', this._handleFormError.bind(this)); |
| } |
| |
| // Native forms can also index elements magically by their name (can't make |
| // this up if I tried) so we need to get the correct attributes, not the |
| // elements with those names. |
| this.request.url = this._form.getAttribute('action'); |
| this.request.method = this._form.getAttribute('method') || 'GET'; |
| this.request.contentType = this._form.getAttribute('enctype') || 'application/x-www-form-urlencoded'; |
| this.request.withCredentials = this.withCredentials; |
| this.request.headers = this.headers; |
| |
| if (this._form.method.toUpperCase() === 'POST') { |
| this.request.body = json; |
| } else { |
| this.request.params = json; |
| } |
| |
| // Allow for a presubmit hook |
| var event = this.fire('iron-form-presubmit', {}, {cancelable: true}); |
| if(!event.defaultPrevented) { |
| this.request.generateRequest(); |
| this.fire('iron-form-submit', json); |
| } |
| }, |
| |
| _getValidatableElements: function() { |
| return this._findElements(this._form, true); |
| }, |
| |
| _getSubmittableElements: function() { |
| return this._findElements(this._form, false); |
| }, |
| |
| _findElements: function(parent, ignoreName) { |
| var nodes = Polymer.dom(parent).querySelectorAll('*'); |
| var submittable = []; |
| |
| for (var i = 0; i < nodes.length; i++) { |
| var node = nodes[i]; |
| // An element is submittable if it is not disabled, and if it has a |
| // 'name' attribute. |
| if(!node.disabled && (ignoreName || node.name)) { |
| submittable.push(node); |
| } |
| else { |
| // This element has a root which could contain more submittable elements. |
| if(node.root) { |
| Array.prototype.push.apply(submittable, this._findElements(node.root, ignoreName)); |
| } |
| } |
| } |
| return submittable; |
| }, |
| |
| _serializeElementValues: function(element) { |
| // We will assume that every custom element that needs to be serialized |
| // has a `value` property, and it contains the correct value. |
| // The only weird one is an element that implements IronCheckedElementBehaviour, |
| // in which case like the native checkbox/radio button, it's only used |
| // when checked. |
| // For native elements, from https://www.w3.org/TR/html5/forms.html#the-form-element. |
| // Native submittable elements: button, input, keygen, object, select, textarea; |
| // 1. We will skip `keygen and `object` for this iteration, and deal with |
| // them if they're actually required. |
| // 2. <button> and <textarea> have a `value` property, so they behave like |
| // the custom elements. |
| // 3. <select> can have multiple options selected, in which case its |
| // `value` is incorrect, and we must use the values of each of its |
| // `selectedOptions` |
| // 4. <input> can have a whole bunch of behaviours, so it's handled separately. |
| // 5. Buttons are hard. The button that was clicked to submit the form |
| // is the one who's name/value gets sent to the server. |
| var tag = element.tagName.toLowerCase(); |
| if (tag === 'button' || (tag === 'input' && (element.type === 'submit' || element.type === 'reset'))) { |
| return []; |
| } |
| |
| if (tag === 'select') { |
| return this._serializeSelectValues(element); |
| } else if (tag === 'input') { |
| return this._serializeInputValues(element); |
| } else { |
| if (element['_hasIronCheckedElementBehavior'] && !element.checked) |
| return []; |
| return [element.value]; |
| } |
| }, |
| |
| _serializeSelectValues: function(element) { |
| var values = []; |
| |
| // A <select multiple> has an array of options, some of which can be selected. |
| for (var i = 0; i < element.options.length; i++) { |
| if (element.options[i].selected) { |
| values.push(element.options[i].value) |
| } |
| } |
| return values; |
| }, |
| |
| _serializeInputValues: function(element) { |
| // Most of the inputs use their 'value' attribute, with the exception |
| // of radio buttons, checkboxes and file. |
| var type = element.type.toLowerCase(); |
| |
| // Don't do anything for unchecked checkboxes/radio buttons. |
| // Don't do anything for file, since that requires a different request. |
| if (((type === 'checkbox' || type === 'radio') && !element.checked) || |
| type === 'file') { |
| return []; |
| } |
| return [element.value]; |
| }, |
| |
| _createHiddenElement: function(name, value) { |
| var input = document.createElement("input"); |
| input.setAttribute("type", "hidden"); |
| input.setAttribute("name", name); |
| input.setAttribute("value", value); |
| return input; |
| }, |
| |
| _addSerializedElement: function(json, name, value) { |
| // If the name doesn't exist, add it. Otherwise, serialize it to |
| // an array, |
| if (json[name] === undefined) { |
| json[name] = value; |
| } else { |
| if (!Array.isArray(json[name])) { |
| json[name] = [json[name]]; |
| } |
| json[name].push(value); |
| } |
| } |
| }); |
| </script> |
| </dom-module> |