Share

Form Participation API Explained

Web applications often maintain state in JS objects that have no direct DOM representation. Such applications may want such state to be submittable.

Existing form elements map one field name to many values. People often build custom controls precisely because those controls hold more complex values that would be better represented as many names to many values. Subclassing existing form elements don’t get you this.

And inheriting from HTMLInputElement is insane (not because inheriting is insane, but because HTMLInputElement is insane), so that’s not really how we want author-defined objects to become submittable.

Given the above it is not recommended that, we should try to solve the “how authors can participate in form submission” problem by enabling the subclassing of existing form elements. Instead, we should define a protocol implementable by any JS object, which allows that JS object to expose names and values to the form validation and submission processes.

The ‘formdata’ event enables any objects to provide form data. It helps avoid creating <input type=hidden> representing application state, or making submittable custom elements.

The Form Participation API enables objects other than built-in form control elements to participate in form submission, form reset, form validation, and so on.

Goal

  • Arbitrary objects can participate in form submission
  • Autonomous custom elements can associate with a form in a way same as built-in form control elements.

Non-goal

  • Provide ability to imitate built-in form control elements perfectly. e.g. Create <input>-equivalent element with autonomous custom element.

API Proposal

There are two sets of API.

Proposal A – Generic Form Participation API which supports both of custom elements and non-elements

Proposal B – Form Participation API specific to custom elements.

API Proposal A – Event-based participation

Sample code

API Details – Addition to HTMLFormElement

‘formdata’ event This event is dispatched synchronously on a form element when ‘construct the entry list’ algorithm for it is invoked. The event bubbles, and is not cancelable. The event object has formData IDL attribute, of which interface is FormData. Event listeners may add entries to the formData attribute.

ISSUE:

The formData object must be identical in all event listeners. So an event listener can read entries set by other event listeners if we use FormData interface. If we’d like to avoid such peeping, we should introduce new interface which is a kind of write-only FormData or add write-only mode to FormData.

RESOLUTION:

We don’t think we need to introduce such write-only interface. Preventing such peeping doesn’t make much sense because anyone can access any entries through ‘new FormData(form)’

ISSUE:

Should the event be dispatch before iterating controls, or after iterating controls? If we allow the peeping, dispatching after the iteration would be useful.

RESOLUTION:

Due to the above RESOLUTION, we should dispatch it after the iteration.

Feature Detection

Check existence of HTMLFormElement.prototype.onformdata or window.FormDataEvent.

API Proposal B – For custom elements

Proposal A is enough in many cases. However it’s not easy to support ‘form’ content attribute behavior and <label>/<fieldset> association with the approach of Proposal A. Proposal B is an alternative API specific to autonomous custom elements. It provides an easy way to participate in forms.

Sample code

API Details – Define form-associated custom elements

customElements.define(name, constructor) If the prototype of the specified constructor has one of ‘formAssociatedCallback’ and ‘disabledStateChangedCallback’, the defined element is marked as a form-associated custom element, and the prototype must provide ‘value’ setter too.

ISSUE: Should we introduce new option to define() like:

customElements.define(‘my-control’, MyControl, { formAssociated: true });

The defined elements will have the following capabilities:

Parsers automatically associate them with a <form> like built-in form-associated elements, and DOM mutation functions such as appendChild() associate form-associated custom elements with a <form>, and calls formAssociatedCallback later.

form.elements, form.length, fieldset.elements contain form-associated custom elements.

In constructing the entry list algorithm, UA creates an entry with name attribute value of the form-associated custom element, and the value set by setFormControlValue() of HTMLElementPrimitives interface. Authors don’t need to register ‘formdata’ event handlers. setCustomValidity() affects form validation.

<label> can search them for a labeled control.

API Details – Functions which form-associated custom elements should provide

void createdCallback(HTMLElementPrimities primitives) If the custom element has a property named ‘createdCallback’ and it’s callable, it is called after the constructor. UA creates an HTMLElementPrimitives instance for this custom element, and passes it as an argument of createdCallback callback.

HTMLElementPrimitives interface provides API to support implementing elements.

setFormControlValue() is used to tell state of form-associated custom elements to UA. setFormControlValue() should be called whenever the value of a form-associated custom element is updated. If the element has non-empty name content attribute, the specified value is appended to entry list in form submission. The specified value is used for UA autofilling

If a form-associated custom element don’t want to use name content attribute, or want to submit multiple entries, it should specify entrySource argument. entrySource is a sequence of objects, and The length of the sequence must be multiples of 2 Objects at 2n index (0, 2, 4, 6, …) must be nullable DOMString Objects at 2n+1 index (1, 3, 5, 7, …) must be nullable FormDataEntryValue If entrySource is specified, each pairs in entrySource will be added to entry list in form submission if entrySource[2n] is not empty.

ISSUE: Should we introduce new interface for a pair of DOMString and FormDataEntryValue? ‘[name1, value1, name2, value2, …]’ is simpler than ‘[new FormDataEntry(name1, value1), new FormDataEntry(name2, valeu2), …]’

For example, a form-associated custom element contains three editable fields, and we want to submit cc-cardno=value1 cc-expire=value2 and cc-cvc=value3 if name content attribute value is ‘cc’. We should run code like the following whenever any value is updated or name content attribute is updated:

NOTE: We should not pass an HTMLELementPrimitives instance via the constructor instead of new callback createdCallback because it would make ‘new MyControl()’ not doable. Also, ‘connectedCallback’ is not suitable. ‘new FormData(form)’ for <form> in an orphan tree should collect values of form-associated custom elements.

The purpose of HTMLElementPrimitives interface is to provide API which custom element implementations can call, but it’s difficult for custom element users to call.

void formAssociatedCallback(HTMLFormElement? form)

If the custom element has a property named ‘formAssociatedCallback’ and it’s callable, it is called on CEReaction timing after UA associated the element with a form element, or disassociated the element from a form element.

void disabledStateChangedCallback(boolean disabled)

If the custom element has a property named ‘disabledStateChangedCallback’ and it’s callable, it is called on CEReaction timing after an ancestor <fieldset>’s disabled state is changed or ‘disabled’ content attribute of this element is added or removed. The argument ‘disabled’ represents new disabled state of the element.

set value(v)

A form-associated custom element implementation must provide a value setter. UA’s input-assist features such as form autofilling may call this setter.

Feature Detection

Check existence of HTMLElementPrimitives.prototype.setFormControlValue.

Changes to other API

Move setCustomValidity(), validationMessage, and reportValidity() to HTMLElement If the context object is neither a listed element nor an autonomous custom element, InvalidStateError is thrown for setCustomValidity() and reportValidity(), and validationMessage returns an empty string. For autonomous custom elements, setCustomValidity() with non-empty string makes element’s validity state ‘invalid’.

Considered alternatives

Implement one of Proposal A and Proposal B, or both?

Though Proposal A can handle both of elements and non-elements, it’s not the best API for elements, and it’s not easy to support some form-related features. For example, it’s very difficult to support <fieldset> and <label> association with the Proposal A approach.

If we don’t support non-element participants, we can drop Proposal A and implement only Proposal B with synchronous FormData callback. If we’d like to minimize complexity, we can drop Proposal B because Proposal A can support both of element and non-element participants.

Alternatives of Proposal B

Authors need to tell “this element will participate in a form” to UA before the element is connected to a document tree In order to participate in a form by markup / DOM structure. The current Proposal B is one of ways to tell it, and it uses customElements.define(). Possible alternatives would be: If an element has a ShadowRoot, and the ShadowRoot has specific callback functions, the element is a participant. Introduce specific content attribute to the element. e.g. <my-control participant=…> A custom element with name content attribute is a participant. Introduce explicit function like document.makeElementParticipatable(element) Should form-associated custom element provide ‘form’ IDL attribute? Should we introduce new base class like HTMLFormControlElement?

TODO or not TODO

Support form-related CSS selectors

This needs an API to request style re-computation explicitly as well as a callback to query element status. e.g.

sequence<DOMString> matchedPseudoClassCallback()

This callback is called just before starting style computation. The return value is a sequence of pseudo class names to be matched. e.g. [‘:invalid’, ‘:out-of-range’]

document.invalidateStyle(Element)

A custom element implementation calls this when pseudo class state is changed.