OLD | NEW |
(Empty) | |
| 1 <!-- |
| 2 @license |
| 3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved. |
| 4 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt |
| 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| 6 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt |
| 7 Code distributed by Google as part of the polymer project is also |
| 8 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt |
| 9 --> |
| 10 |
| 11 <link rel="import" href="../polymer/polymer.html"> |
| 12 <link rel="import" href="../iron-ajax/iron-ajax.html"> |
| 13 |
| 14 <script> |
| 15 /* |
| 16 `<iron-form>` is an HTML `<form>` element that can validate and submit any custo
m |
| 17 elements that implement `Polymer.IronFormElementBehavior`, as well as any |
| 18 native HTML elements. For more information on which attributes are |
| 19 available on the native form element, see https://developer.mozilla.org/en-US/do
cs/Web/HTML/Element/form |
| 20 |
| 21 It supports both `get` and `post` methods, and uses an `iron-ajax` element to |
| 22 submit the form data to the action URL. |
| 23 |
| 24 Example: |
| 25 |
| 26 <form is="iron-form" id="form" method="post" action="/form/handler"> |
| 27 <paper-input name="name" label="name"></paper-input> |
| 28 <input name="address"> |
| 29 ... |
| 30 </form> |
| 31 |
| 32 By default, a native `<button>` element will submit this form. However, if you |
| 33 want to submit it from a custom element's click handler, you need to explicitly |
| 34 call the form's `submit` method. |
| 35 |
| 36 Example: |
| 37 |
| 38 <paper-button raised onclick="submitForm()">Submit</paper-button> |
| 39 |
| 40 function submitForm() { |
| 41 document.getElementById('form').submit(); |
| 42 } |
| 43 |
| 44 To customize the request sent to the server, you can listen to the `iron-form-pr
esubmit` |
| 45 event, and modify the form's[`iron-ajax`](https://elements.polymer-project.org/e
lements/iron-ajax) |
| 46 object. However, If you want to not use `iron-ajax` at all, you can cancel the |
| 47 event and do your own custom submission: |
| 48 |
| 49 Example of modifying the request, but still using the build-in form submission
: |
| 50 |
| 51 form.addEventListener('iron-form-presubmit', function() { |
| 52 this.request.method = 'put'; |
| 53 this.request.params = someCustomParams; |
| 54 }); |
| 55 |
| 56 Example of bypassing the build-in form submission: |
| 57 |
| 58 form.addEventListener('iron-form-presubmit', function(event) { |
| 59 event.preventDefault(); |
| 60 var firebase = new Firebase(form.getAttribute('action')); |
| 61 firebase.set(form.serialize()); |
| 62 }); |
| 63 |
| 64 @demo demo/index.html |
| 65 */ |
| 66 Polymer({ |
| 67 |
| 68 is: 'iron-form', |
| 69 |
| 70 extends: 'form', |
| 71 |
| 72 properties: { |
| 73 /** |
| 74 * By default, the form will display the browser's native validation |
| 75 * UI (i.e. popup bubbles and invalid styles on invalid fields). You can |
| 76 * manually disable this; however, if you do, note that you will have to |
| 77 * manually style invalid *native* HTML fields yourself, as you are |
| 78 * explicitly preventing the native form from doing so. |
| 79 */ |
| 80 disableNativeValidationUi: { |
| 81 type: Boolean, |
| 82 value: false |
| 83 }, |
| 84 |
| 85 /** |
| 86 * Set the withCredentials flag when sending data. |
| 87 */ |
| 88 withCredentials: { |
| 89 type: Boolean, |
| 90 value: false |
| 91 }, |
| 92 |
| 93 /** |
| 94 * Content type to use when sending data. If the `contentType` property |
| 95 * is set and a `Content-Type` header is specified in the `headers` |
| 96 * property, the `headers` property value will take precedence. |
| 97 * If Content-Type is set to a value listed below, then |
| 98 * the `body` (typically used with POST requests) will be encoded accordin
gly. |
| 99 * |
| 100 * * `content-type="application/json"` |
| 101 * * body is encoded like `{"foo":"bar baz","x":1}` |
| 102 * * `content-type="application/x-www-form-urlencoded"` |
| 103 * * body is encoded like `foo=bar+baz&x=1` |
| 104 */ |
| 105 contentType: { |
| 106 type: String, |
| 107 value: "application/x-www-form-urlencoded" |
| 108 }, |
| 109 |
| 110 /** |
| 111 * HTTP request headers to send. |
| 112 * |
| 113 * Note: setting a `Content-Type` header here will override the value |
| 114 * specified by the `contentType` property of this element. |
| 115 */ |
| 116 headers: { |
| 117 type: Object, |
| 118 value: function() { |
| 119 return {}; |
| 120 } |
| 121 }, |
| 122 |
| 123 /** |
| 124 * iron-ajax request object used to submit the form. |
| 125 */ |
| 126 request: { |
| 127 type: Object, |
| 128 } |
| 129 }, |
| 130 |
| 131 /** |
| 132 * Fired if the form cannot be submitted because it's invalid. |
| 133 * |
| 134 * @event iron-form-invalid |
| 135 */ |
| 136 |
| 137 /** |
| 138 * Fired before the form is submitted. |
| 139 * |
| 140 * @event iron-form-presubmit |
| 141 */ |
| 142 |
| 143 /** |
| 144 * Fired after the form is submitted. |
| 145 * |
| 146 * @event iron-form-submit |
| 147 */ |
| 148 |
| 149 /** |
| 150 * Fired after the form is reset. |
| 151 * |
| 152 * @event iron-form-reset |
| 153 */ |
| 154 |
| 155 /** |
| 156 * Fired after the form is submitted and a response is received. An |
| 157 * IronRequestElement is included as the event.detail object. |
| 158 * |
| 159 * @event iron-form-response |
| 160 */ |
| 161 |
| 162 /** |
| 163 * Fired after the form is submitted and an error is received. An |
| 164 * IronRequestElement is included as the event.detail object. |
| 165 * |
| 166 * @event iron-form-error |
| 167 */ |
| 168 listeners: { |
| 169 'iron-form-element-register': '_registerElement', |
| 170 'iron-form-element-unregister': '_unregisterElement', |
| 171 'submit': '_onSubmit', |
| 172 'reset': '_onReset' |
| 173 }, |
| 174 |
| 175 registered: function() { |
| 176 // Dear reader: I apologize for what you're about to experience. You see, |
| 177 // Safari does not respect `required` on input elements, so it never |
| 178 // has any browser validation bubbles to show. And we have to feature |
| 179 // detect that, since we rely on the form submission to do the right thing
. |
| 180 // See http://caniuse.com/#search=required. |
| 181 |
| 182 // Create a fake form, with an invalid input. If it gets submitted, it's S
afari. |
| 183 var form = document.createElement('form'); |
| 184 var input = document.createElement('input'); |
| 185 input.setAttribute('required', 'true'); |
| 186 form.appendChild(input); |
| 187 |
| 188 // If you call submit(), the form doesn't actually fire a submit event, |
| 189 // so you can't intercept it and cancel it. The event is only fired |
| 190 // from the magical button click submission. |
| 191 // See http://wayback.archive.org/web/20090323062817/http://blogs.vertigos
oftware.com/snyholm/archive/2006/09/27/3788.aspx. |
| 192 var button = document.createElement('input'); |
| 193 button.setAttribute('type', 'submit'); |
| 194 form.appendChild(button); |
| 195 |
| 196 Polymer.clientSupportsFormValidationUI = true; |
| 197 form.addEventListener('submit', function(event) { |
| 198 // Oh good! We don't handle `required` correctly. |
| 199 Polymer.clientSupportsFormValidationUI = false; |
| 200 event.preventDefault(); |
| 201 }); |
| 202 button.click(); |
| 203 }, |
| 204 |
| 205 ready: function() { |
| 206 // Object that handles the ajax form submission request. |
| 207 this.request = document.createElement('iron-ajax'); |
| 208 this.request.addEventListener('response', this._handleFormResponse.bind(th
is)); |
| 209 this.request.addEventListener('error', this._handleFormError.bind(this)); |
| 210 |
| 211 // Holds all the custom elements registered with this form. |
| 212 this._customElements = []; |
| 213 // Holds the initial values of the custom elements registered with this fo
rm. |
| 214 this._customElementsInitialValues = []; |
| 215 }, |
| 216 |
| 217 /** |
| 218 * Submits the form. |
| 219 */ |
| 220 submit: function() { |
| 221 if (!this.noValidate && !this.validate()) { |
| 222 // In order to trigger the native browser invalid-form UI, we need |
| 223 // to do perform a fake form submit. |
| 224 if (Polymer.clientSupportsFormValidationUI && !this.disableNativeValidat
ionUi) { |
| 225 this._doFakeSubmitForValidation(); |
| 226 } |
| 227 this.fire('iron-form-invalid'); |
| 228 return; |
| 229 } |
| 230 |
| 231 var json = this.serialize(); |
| 232 |
| 233 // Native forms can also index elements magically by their name (can't mak
e |
| 234 // this up if I tried) so we need to get the correct attributes, not the |
| 235 // elements with those names. |
| 236 this.request.url = this.getAttribute('action'); |
| 237 this.request.method = this.getAttribute('method'); |
| 238 this.request.contentType = this.contentType; |
| 239 this.request.withCredentials = this.withCredentials; |
| 240 this.request.headers = this.headers; |
| 241 |
| 242 if (this.request.method.toUpperCase() === 'POST') { |
| 243 this.request.body = json; |
| 244 } else { |
| 245 this.request.params = json; |
| 246 } |
| 247 |
| 248 // Allow for a presubmit hook |
| 249 var event = this.fire('iron-form-presubmit', {}, {cancelable: true}); |
| 250 if(!event.defaultPrevented) { |
| 251 this.request.generateRequest(); |
| 252 this.fire('iron-form-submit', json); |
| 253 } |
| 254 }, |
| 255 |
| 256 /** |
| 257 * Handler that is called when the native form fires a `submit` event |
| 258 * |
| 259 * @param {Event} event A `submit` event. |
| 260 */ |
| 261 _onSubmit: function(event) { |
| 262 this.submit(); |
| 263 |
| 264 // Don't perform a page refresh. |
| 265 if (event) { |
| 266 event.preventDefault(); |
| 267 } |
| 268 |
| 269 return false; |
| 270 }, |
| 271 |
| 272 /** |
| 273 * Handler that is called when the native form fires a `reset` event |
| 274 * |
| 275 * @param {Event} event A `reset` event. |
| 276 */ |
| 277 _onReset: function(event) { |
| 278 this._resetCustomElements(); |
| 279 }, |
| 280 |
| 281 /** |
| 282 * Returns a json object containing name/value pairs for all the registered |
| 283 * custom components and native elements of the form. If there are elements |
| 284 * with duplicate names, then their values will get aggregated into an |
| 285 * array of values. |
| 286 * |
| 287 * @return {!Object} |
| 288 */ |
| 289 serialize: function() { |
| 290 var json = {}; |
| 291 |
| 292 function addSerializedElement(name, value) { |
| 293 // If the name doesn't exist, add it. Otherwise, serialize it to |
| 294 // an array, |
| 295 if (!json[name]) { |
| 296 json[name] = value; |
| 297 } else { |
| 298 if (!Array.isArray(json[name])) { |
| 299 json[name] = [json[name]]; |
| 300 } |
| 301 json[name].push(value); |
| 302 } |
| 303 } |
| 304 |
| 305 // Go through all of the registered custom components. |
| 306 for (var el, i = 0; el = this._customElements[i], i < this._customElements
.length; i++) { |
| 307 // If this custom element is inside a custom element that has already |
| 308 // registered to this form, skip it. |
| 309 if (!this._isChildOfRegisteredParent(el, true) && this._useValue(el)) { |
| 310 addSerializedElement(el.name, el.value); |
| 311 } |
| 312 } |
| 313 |
| 314 // Also go through the form's native elements. |
| 315 for (var el, i = 0; el = this.elements[i], i < this.elements.length; i++)
{ |
| 316 // If this native element is inside a custom element that has already |
| 317 // registered to this form, skip it. |
| 318 if (this._isChildOfRegisteredParent(el, true) || !this._useValue(el)) { |
| 319 continue; |
| 320 } |
| 321 |
| 322 // A <select multiple> has an array of values. |
| 323 if (el.tagName.toLowerCase() === 'select' && el.multiple) { |
| 324 for (var o = 0; o < el.options.length; o++) { |
| 325 if (el.options[o].selected) { |
| 326 addSerializedElement(el.name, el.options[o].value); |
| 327 } |
| 328 } |
| 329 } else { |
| 330 addSerializedElement(el.name, el.value); |
| 331 } |
| 332 } |
| 333 |
| 334 return json; |
| 335 }, |
| 336 |
| 337 _handleFormResponse: function (event) { |
| 338 this.fire('iron-form-response', event.detail); |
| 339 }, |
| 340 |
| 341 _handleFormError: function (event) { |
| 342 this.fire('iron-form-error', event.detail); |
| 343 }, |
| 344 |
| 345 _registerElement: function(e) { |
| 346 // Get the actual element that fired the event |
| 347 var element = Polymer.dom(e).rootTarget; |
| 348 |
| 349 element._parentForm = this; |
| 350 this._customElements.push(element); |
| 351 |
| 352 // Save the original value of this input. |
| 353 this._customElementsInitialValues.push( |
| 354 this._usesCheckedInsteadOfValue(element) ? element.checked : element.v
alue); |
| 355 }, |
| 356 |
| 357 _unregisterElement: function(e) { |
| 358 var target = e.detail.target; |
| 359 if (target) { |
| 360 var index = this._customElements.indexOf(target); |
| 361 if (index > -1) { |
| 362 this._customElements.splice(index, 1); |
| 363 this._customElementsInitialValues.splice(index, 1); |
| 364 } |
| 365 } |
| 366 }, |
| 367 |
| 368 /** |
| 369 * Validates all the required elements (custom and native) in the form. |
| 370 * @return {boolean} True if all the elements are valid. |
| 371 */ |
| 372 validate: function() { |
| 373 var valid = true; |
| 374 |
| 375 // Validate all the custom elements. |
| 376 var validatable; |
| 377 for (var el, i = 0; el = this._customElements[i], i < this._customElements
.length; i++) { |
| 378 if (!this._isChildOfRegisteredParent(el, false) && !el.disabled) { |
| 379 validatable = /** @type {{validate: (function() : boolean)}} */ (el); |
| 380 // Some elements may not have correctly defined a validate method. |
| 381 if (validatable.validate) |
| 382 valid = !!validatable.validate() && valid; |
| 383 } |
| 384 } |
| 385 |
| 386 // Validate the form's native elements. |
| 387 for (var el, i = 0; el = this.elements[i], i < this.elements.length; i++)
{ |
| 388 // If this native element is inside a custom element that has already |
| 389 // registered to this form, skip it. |
| 390 if (this._isChildOfRegisteredParent(el, false)) { |
| 391 continue; |
| 392 } |
| 393 |
| 394 // Custom elements that extend a native element will also appear in |
| 395 // this list, but they've already been validated. |
| 396 if (!el.hasAttribute('is') && el.willValidate && el.checkValidity) { |
| 397 valid = el.checkValidity() && valid; |
| 398 } |
| 399 } |
| 400 |
| 401 return valid; |
| 402 }, |
| 403 |
| 404 /** |
| 405 * Returns whether the given element is a radio-button or a checkbox. |
| 406 * @return {boolean} True if the element has a `checked` property. |
| 407 */ |
| 408 _usesCheckedInsteadOfValue: function(el) { |
| 409 if (el.type == 'checkbox' || |
| 410 el.type == 'radio' || |
| 411 el.getAttribute('role') == 'checkbox' || |
| 412 el.getAttribute('role') == 'radio' || |
| 413 el['_hasIronCheckedElementBehavior']) { |
| 414 return true; |
| 415 } |
| 416 return false; |
| 417 }, |
| 418 |
| 419 _useValue: function(el) { |
| 420 // Skip disabled elements or elements that don't have a `name` attribute. |
| 421 if (el.disabled || !el.name) { |
| 422 return false; |
| 423 } |
| 424 |
| 425 // Checkboxes and radio buttons should only use their value if they're |
| 426 // checked. Custom paper-checkbox and paper-radio-button elements |
| 427 // don't have a type, but they have the correct role set. |
| 428 if (this._usesCheckedInsteadOfValue(el)) |
| 429 return el.checked; |
| 430 return true; |
| 431 }, |
| 432 |
| 433 _doFakeSubmitForValidation: function() { |
| 434 var fakeSubmit = document.createElement('input'); |
| 435 fakeSubmit.setAttribute('type', 'submit'); |
| 436 fakeSubmit.style.display = 'none'; |
| 437 this.appendChild(fakeSubmit); |
| 438 |
| 439 fakeSubmit.click(); |
| 440 |
| 441 this.removeChild(fakeSubmit); |
| 442 }, |
| 443 |
| 444 /** |
| 445 * Resets all non-disabled form custom elements to their initial values. |
| 446 */ |
| 447 _resetCustomElements: function() { |
| 448 // Reset all the registered custom components. We need to do this after |
| 449 // the native reset, since programmatically changing the `value` of some |
| 450 // native elements (iron-input in particular) does not notify its |
| 451 // parent `paper-input`, which will now display the wrong value. |
| 452 this.async(function() { |
| 453 for (var el, i = 0; el = this._customElements[i], i < this._customElemen
ts.length; i++) { |
| 454 if (el.disabled) |
| 455 continue; |
| 456 |
| 457 if (this._usesCheckedInsteadOfValue(el)) { |
| 458 el.checked = this._customElementsInitialValues[i]; |
| 459 } else { |
| 460 // The native input/textarea displays literal "undefined" when its |
| 461 // its value is set to undefined, so default to null instead. |
| 462 var value = this._customElementsInitialValues[i]; |
| 463 if (value === undefined) { |
| 464 value = null; |
| 465 } |
| 466 el.value = value; |
| 467 |
| 468 // In the shady DOM, the native form is all-seeing, and will |
| 469 // reset the nested inputs inside <paper-input> and <paper-textarea>
. |
| 470 // In particular, it resets them to what it thinks the default value |
| 471 // is (i.e. "", before the bindings have ran), and since this is |
| 472 // a programmatic update, it also doesn't fire any events. |
| 473 // Which means we need to manually update the native element's value
. |
| 474 if (el.inputElement) { |
| 475 el.inputElement.value = el.value; |
| 476 } else if (el.textarea) { |
| 477 el.textarea.value = el.value; |
| 478 } |
| 479 } |
| 480 el.invalid = false; |
| 481 } |
| 482 |
| 483 this.fire('iron-form-reset'); |
| 484 }, 1); |
| 485 }, |
| 486 |
| 487 /** |
| 488 * Returns true if `node` is in the shadow DOM of a different element, |
| 489 * that has also implemented IronFormElementBehavior and is registered |
| 490 * to this form. The second parameter specifies if the parent must have a |
| 491 * name to be considered. |
| 492 */ |
| 493 _isChildOfRegisteredParent: function(node, checkHasName) { |
| 494 var parent = node; |
| 495 |
| 496 // At some point going up the tree we'll find either this form or the docu
ment. |
| 497 while (parent && parent !== document && parent != this) { |
| 498 // Use logical parentnode, or native ShadowRoot host. |
| 499 parent = Polymer.dom(parent).parentNode || parent.host; |
| 500 |
| 501 // Check if the parent was registered and submittable. |
| 502 if (parent && |
| 503 (!checkHasName || parent.name) && |
| 504 parent._parentForm === this) { |
| 505 return true; |
| 506 } |
| 507 } |
| 508 return false; |
| 509 } |
| 510 |
| 511 }); |
| 512 |
| 513 </script> |
OLD | NEW |