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