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.github.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/CONTRIBUTORS.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/PATENTS.txt
11
<link rel="import" href="../polymer/polymer.html">
12
<link rel="import" href="../iron-ajax/iron-ajax.html">
16
`<iron-form>` is an HTML `<form>` element that can validate and submit any custom
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/docs/Web/HTML/Element/form
21
It supports both `get` and `post` methods, and uses an `iron-ajax` element to
22
submit the form data to the action URL.
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">
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.
38
<paper-button raised onclick="submitForm()">Submit</paper-button>
40
function submitForm() {
41
document.getElementById('form').submit();
44
To customize the request sent to the server, you can listen to the `iron-form-presubmit`
45
event, and modify the form's[`iron-ajax`](https://elements.polymer-project.org/elements/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:
49
Example of modifying the request, but still using the build-in form submission:
51
form.addEventListener('iron-form-presubmit', function() {
52
this.request.method = 'put';
53
this.request.params = someCustomParams;
56
Example of bypassing the build-in form submission:
58
form.addEventListener('iron-form-presubmit', function(event) {
59
event.preventDefault();
60
var firebase = new Firebase(form.getAttribute('action'));
61
firebase.set(form.serialize());
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.
80
disableNativeValidationUi: {
86
* Set the withCredentials flag when sending data.
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 accordingly.
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`
107
value: "application/x-www-form-urlencoded"
111
* HTTP request headers to send.
113
* Note: setting a `Content-Type` header here will override the value
114
* specified by the `contentType` property of this element.
124
* iron-ajax request object used to submit the form.
132
* Fired if the form cannot be submitted because it's invalid.
134
* @event iron-form-invalid
138
* Fired before the form is submitted.
140
* @event iron-form-presubmit
144
* Fired after the form is submitted.
146
* @event iron-form-submit
150
* Fired after the form is reset.
152
* @event iron-form-reset
156
* Fired after the form is submitted and a response is received. An
157
* IronRequestElement is included as the event.detail object.
159
* @event iron-form-response
163
* Fired after the form is submitted and an error is received. An
164
* IronRequestElement is included as the event.detail object.
166
* @event iron-form-error
169
'iron-form-element-register': '_registerElement',
170
'iron-form-element-unregister': '_unregisterElement',
171
'submit': '_onSubmit',
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.
182
// Create a fake form, with an invalid input. If it gets submitted, it's Safari.
183
var form = document.createElement('form');
184
var input = document.createElement('input');
185
input.setAttribute('required', 'true');
186
form.appendChild(input);
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.vertigosoftware.com/snyholm/archive/2006/09/27/3788.aspx.
192
var button = document.createElement('input');
193
button.setAttribute('type', 'submit');
194
form.appendChild(button);
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();
206
// Object that handles the ajax form submission request.
207
this.request = document.createElement('iron-ajax');
208
this.request.addEventListener('response', this._handleFormResponse.bind(this));
209
this.request.addEventListener('error', this._handleFormError.bind(this));
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 form.
214
this._customElementsInitialValues = [];
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.disableNativeValidationUi) {
225
this._doFakeSubmitForValidation();
227
this.fire('iron-form-invalid');
231
var json = this.serialize();
233
// Native forms can also index elements magically by their name (can't make
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;
242
if (this.method.toUpperCase() === 'POST') {
243
this.request.body = json;
245
this.request.params = json;
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);
257
* Handler that is called when the native form fires a `submit` event
259
* @param {Event} event A `submit` event.
261
_onSubmit: function(event) {
264
// Don't perform a page refresh.
266
event.preventDefault();
273
* Handler that is called when the native form fires a `reset` event
275
* @param {Event} event A `reset` event.
277
_onReset: function(event) {
278
this._resetCustomElements();
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
289
serialize: function() {
292
function addSerializedElement(name, value) {
293
// If the name doesn't exist, add it. Otherwise, serialize it to
298
if (!Array.isArray(json[name])) {
299
json[name] = [json[name]];
301
json[name].push(value);
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) && this._useValue(el)) {
310
addSerializedElement(el.name, el.value);
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) || !this._useValue(el)) {
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);
330
addSerializedElement(el.name, el.value);
337
_handleFormResponse: function (event) {
338
this.fire('iron-form-response', event.detail);
341
_handleFormError: function (event) {
342
this.fire('iron-form-error', event.detail);
345
_registerElement: function(e) {
346
// Get the actual element that fired the event
347
var element = Polymer.dom(e).rootTarget;
349
element._parentForm = this;
350
this._customElements.push(element);
352
// Save the original value of this input.
353
this._customElementsInitialValues.push(
354
this._usesCheckedInsteadOfValue(element) ? element.checked : element.value);
357
_unregisterElement: function(e) {
358
var target = e.detail.target;
360
var index = this._customElements.indexOf(target);
362
this._customElements.splice(index, 1);
363
this._customElementsInitialValues.splice(index, 1);
369
* Validates all the required elements (custom and native) in the form.
370
* @return {boolean} True if all the elements are valid.
372
validate: function() {
375
// Validate all the custom elements.
377
for (var el, i = 0; el = this._customElements[i], i < this._customElements.length; i++) {
378
if (el.required && !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;
386
// Validate the form's native elements.
387
for (var el, i = 0; el = this.elements[i], i < this.elements.length; i++) {
388
// Custom elements that extend a native element will also appear in
389
// this list, but they've already been validated.
390
if (!el.hasAttribute('is') && el.willValidate && el.checkValidity) {
391
valid = el.checkValidity() && valid;
399
* Returns whether the given element is a radio-button or a checkbox.
400
* @return {boolean} True if the element has a `checked` property.
402
_usesCheckedInsteadOfValue: function(el) {
403
if (el.type == 'checkbox' ||
404
el.type == 'radio' ||
405
el.getAttribute('role') == 'checkbox' ||
406
el.getAttribute('role') == 'radio' ||
407
el['_hasIronCheckedElementBehavior']) {
413
_useValue: function(el) {
414
// Skip disabled elements or elements that don't have a `name` attribute.
415
if (el.disabled || !el.name) {
419
// Checkboxes and radio buttons should only use their value if they're
420
// checked. Custom paper-checkbox and paper-radio-button elements
421
// don't have a type, but they have the correct role set.
422
if (this._usesCheckedInsteadOfValue(el))
427
_doFakeSubmitForValidation: function() {
428
var fakeSubmit = document.createElement('input');
429
fakeSubmit.setAttribute('type', 'submit');
430
fakeSubmit.style.display = 'none';
431
this.appendChild(fakeSubmit);
435
this.removeChild(fakeSubmit);
439
* Resets all non-disabled form custom elements to their initial values.
441
_resetCustomElements: function() {
442
// Reset all the registered custom components. We need to do this after
443
// the native reset, since programmatically changing the `value` of some
444
// native elements (iron-input in particular) does not notify its
445
// parent `paper-input`, which will now display the wrong value.
446
this.async(function() {
447
for (var el, i = 0; el = this._customElements[i], i < this._customElements.length; i++) {
451
if (this._usesCheckedInsteadOfValue(el)) {
452
el.checked = this._customElementsInitialValues[i];
454
el.value = this._customElementsInitialValues[i];
456
// In the shady DOM, the native form is all-seeing, and will
457
// reset the nested inputs inside <paper-input> and <paper-textarea>.
458
// In particular, it resets them to what it thinks the default value
459
// is (i.e. "", before the bindings have ran), and since this is
460
// a programmatic update, it also doesn't fire any events.
461
// Which means we need to manually update the native element's value.
462
if (el.inputElement) {
463
el.inputElement.value = el.value;
464
} else if (el.textarea) {
465
el.textarea.value = el.value;
471
this.fire('iron-form-reset');
476
* Returns true if `node` is in the shadow DOM of a different element,
477
* that has also implemented IronFormElementBehavior and is registered
480
_isChildOfRegisteredParent: function(node) {
483
// At some point going up the tree we'll find either this form or the document.
484
while (parent && parent !== document && parent != this) {
485
// Use logical parentnode, or native ShadowRoot host.
486
parent = Polymer.dom(parent).parentNode || parent.host;
488
// Check if the parent was registered and submittable.
489
if (parent && parent.name && parent._parentForm === this) {