Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Form-associated custom elements: being a submit button #814

Open
domenic opened this issue May 16, 2019 · 40 comments
Open

Form-associated custom elements: being a submit button #814

domenic opened this issue May 16, 2019 · 40 comments

Comments

@domenic
Copy link
Collaborator

domenic commented May 16, 2019

See discussion starting around #187 (comment).

If you want your FACE to be a submit button, which means:

  • Can be selected by :default
  • Can be automatically clicked by the implicit submission of other form elements

you currently cannot.

A strawperson proposal would be to add a submitButton boolean to ElementInternals. Then custom submit buttons would just do this.#internals.submitButton = true.

@annevk
Copy link
Collaborator

annevk commented May 17, 2019

And it would throw if this's form-associated is not true?

Do you have to setup your own activation behavior (by invoking this.#internals.form.requestSubmit(this) presumably)?

@domenic
Copy link
Collaborator Author

domenic commented May 17, 2019

And it would throw if this's form-associated is not true?

For sure.

Do you have to setup your own activation behavior (by invoking this.#internals.form.requestSubmit(this) presumably)?

Good question. I had envisioned yes, but I don't have strong feelings. I can't immediately think of any good use cases that would be prevented if we automatically added such activation behavior.

@elycruz
Copy link

elycruz commented Sep 21, 2019

Shouldn't we just mimmick the way buttons currently work in forms? I.e.: Click any button or an input[type="submit"] element and the form gets submitted?

Also, for bullet point 2: You can call form.submit() // or form.requestSubmit() if you have access to the form via internals no?

Never mind my comments above (just read the comments in issue #187 and now understand what you're mentioning here 👍). (excuse my ignorance)
(thanks)

@Westbrook
Copy link
Collaborator

Are there any blockers to this sort of addition or places that the community could specifically support moving this forward?

@calebdwilliams
Copy link

I really like the HTMLFormElement.prototype.requestSubmit idea. Would be easy enough to polyfill as well.

@annevk
Copy link
Collaborator

annevk commented May 10, 2021

I think the main blocker is having someone willing to work out the details. I.e., a more concrete form of OP that can be turned into a PR against the HTML Standard. And then have someone write web-platform-tests to accompany that.

@calebdwilliams
Copy link

calebdwilliams commented May 13, 2021

So I've been playing with this in my project at work and this seems like it mimics what I'd like to see on this front:

HTMLFormElement.prototype.requestSubmit = function() {
  if (this.reportValidity() === false) {
    return;
  }
  const submitEvent = new Event('submit', { cancelable: true });
  this.dispatchEvent(submitEvent);
  if (!submitEvent.defaultPrevented) {
    this.submit();
  }
}

While this could live on ElementInternals, it doesn't quite make sense to make it ElementInternals.prototype.submitForm because form is already exposed by ElementInternals and other APIs might wish to make use of this API as well (other than submit buttons) like watching for an Enter keydown event:

class SomeCustomElement extends HTMLElement {
  #internals = this.attachInternals();

  /** Interesting custom element things */

  connectedCallback() {
    this.someTarget.addEventListener('keypress', event => {
      if (this.#internals.form && event.code === 'Enter') {
        this.#internals.form.requestSubmit();
      }
    }
  }
}

Since ElementInternals.prototype.setFormValue will accept a FormData object, this could also enable logical submissions from within nested forms:

<form id="rootForm">
  <x-personal-info-form></x-personal-info-form>
  <x-address-form></x-address-form>
  <x-ds-button-row></x-ds-button-row>
<form>

Where any Enter keypress from x-personal-info-form or x-address-form could submit the form. The x-ds-button-row element could include styled reset and submit buttons. Both of which will work for all the forms.

@domenic
Copy link
Collaborator Author

domenic commented May 13, 2021

requestSubmit() already exists and is implemented: https://html.spec.whatwg.org/#dom-form-requestsubmit https://wpt.fyi/results/html/semantics/forms/the-form-element/form-requestsubmit.html?label=master&label=experimental&aligned&q=requestsubmit

@calebdwilliams
Copy link

Welp, learn something new every day. Thanks for the link.

@elycruz
Copy link

elycruz commented Jun 28, 2021

@domenic Quick question: What if we, instead of adding a submitButton: boolean property, we added a type: string property, which would accept one of the already existing form control element types; E.g., 'submit', 'reset', 'select-one', etc.. - This approach would allow for any of the existing form control functionalities to be injected into the ElementInternals interface (as required) allowing component authors to just set this.#internals.type in order to switch on which ever available functionality they require, no?

@WickyNilliams
Copy link

WickyNilliams commented Jun 21, 2022

requestSubmit is not sufficient to cover all needs, right? It would not handle e.g. hitting Enter inside an <input /> to trigger a submit. So I think the browser needs to add activation behaviour automatically.

@domenic
Copy link
Collaborator Author

domenic commented Jun 21, 2022

It is not automatic; indeed you need to add code such as that in the comment I was replying to: #814 (comment)

@WickyNilliams
Copy link

When designating a CE as a button via internals it should behave as a native button out of the box. I think it's too easy to get wrong otherwise, especially when you consider more esoteric cases like using a form attribute on the button or an input.

Just wanted to voice a vote in favour of automatically getting that behaviour :)

@clshortfuse
Copy link

clshortfuse commented Aug 17, 2022

Be warned, it's not enough to just form.submit(). We also need a way to signal which button invoked the submit in order for <form method=dialog> to work properly.

I'm currently working around that. requestSubmit(this) won't work on Autonomous HTMLElement:

Uncaught TypeError: Failed to execute 'requestSubmit' on 'HTMLFormElement': The specified element is not a submit button.
at HTMLButtonElement.

And requestSubmit(this.buttonElement) won't work because buttonElement is part of a nest shadow DOM.

Uncaught DOMException: Failed to execute 'requestSubmit' on 'HTMLFormElement': The specified element is not owned by this form element. at HTMLButtonElement.

I can't use Customized HTML Elements because Safari. That just leaves creating a clone of the submit button as a child of the form that's hidden with identical values and then assigning it to that.

Very hacky solution:

const duplicatedButton = this.buttonElement.cloneNode();
duplicatedButton.hidden = true;
form.append(duplicatedButton);
form.requestSubmit(duplicatedButton);
duplicatedButton.remove();

The <dialog> that's associated with the form will now properly reflect the value within HTMLDialogElement.returnValue.

@WickyNilliams
Copy link

Yeah creating a "proxy" button in the light dom is the approach I've been using. But it's definitely hacky. And still not perfect, since you get a seemingly incorrect element (of course it's technically correct, but for a dev using your custom button it seems wrong) for event.submitter on a form's submit event.

I personally leave the proxy button in the light DOM always, so that hitting enter in a text input will work.

@annevk
Copy link
Collaborator

annevk commented Feb 13, 2023

@clshortfuse requestSubmit() should work if this proposal is adopted though as that allows a custom element to become a submit button.

@clshortfuse
Copy link

clshortfuse commented Feb 13, 2023

@annevk Thanks. My comment before was more for posterity's sake for people searching for a workaround. I haven't fully delved what I'd like to see a proposal consist of. Right now I have my own code in my FACE InputMixin:

   /**
     * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission
     * @param {Event} event
     * @return {void}
     */
    performImplicitSubmission(event) {
      const form = this.form;
      if (!form) return;
      /** @type {HTMLInputElement} */
      let defaultButton;
      const submissionBlockers = new Set();
      for (const element of /** @type {HTMLCollectionOf<HTMLInputElement>} */ (form.elements)) {
        // Spec doesn't specify disabled, but browsers do skip them.
        if (element.type === 'submit' && !element.disabled && !element.matches(':disabled')) {
          defaultButton ??= element;
          break;
        }

        if (IMPLICIT_SUBMISSION_BLOCKING_TYPES.has(element.type)) {
          submissionBlockers.add(element);
        }
      }
      if (defaultButton) {
        defaultButton.click();
        return;
      }
      if (submissionBlockers.size > 1) return;
      this.form.submit();
    }

This is to handle if users press 'Enter' on a FACE element, like <x-checkbox> or <x-text-input>. The spec asks to fire a click event on the button. This seems like enough to satisfy that requirement. (side-note: :disabled isn't ready on Safari yet.)

The question would really be, would a FACE listed in form.elements with [type="submit"] be satisfactory? Right now it's a Submit-Like button because I don't bother to check if it's an HTMLButtonElement or HTMLInputElement.

The other question is, why wouldn't it be enough for any FACE button that has .type === 'submit' to just be considered a submit button. Would that not be enough without requiring extra (and possibly redundant) JS code? Are there situations where type is not submit and it can be a submit button? Are there situations where type is submit and it is not a submit button?

Another point, form.requestSubmit spec says:

>form.requestSubmit([ submitter ])
Requests to submit the form. Unlike submit(), this method includes interactive constraint validation and firing a submit event, either of which can cancel submission.

>The submitter argument can be used to point to a specific submit button, whose formaction, formenctype, formmethod, formnovalidate, and formtarget attributes can impact submission. Additionally, the submitter will be included when constructing the entry list for submission; normally, buttons are excluded.

With IDL showing:

>undefined requestSubmit(optional HTMLElement? submitter = null);

My interpretation that the submitter in requestSubmit can be a submit button, but does not need to be and it maybe a browser error to check against HTMLInputElement or HTMLButtonElement. Chrome should not be firing:

>Uncaught TypeError: Failed to execute 'requestSubmit' on 'HTMLFormElement': The specified element is not a submit button. at HTMLButtonElement.

Nothing in the steps say to perform this check. It may be a necessary step to build some Web Platform Tests to target this "mistake".

I'm of the opinion that

  1. FACE authors should opt-in to perform implicit submission with their own code. Perhaps a may include a shortcut later called form.performImplicitSubmission(), but that's out of scope for now. (If it's automatic, opt-out would be preventDefault() on Enter keydown, though that would probably break compatibility with current FACE.)
  2. Any FACE with .type === 'submit' is a submit button
  3. Browsers should not perform instance/type checks on the element passed to form.requestSubmit(submitter).

Edit: Misread the spec. "If submitter is not a submit button, then throw a TypeError." Still, would be satisfied by point 2.

@annevk
Copy link
Collaborator

annevk commented Feb 14, 2023

I don't understand what .type === 'submit' means. I think we need what OP suggests and have something be set on ElementInternals. We cannot invoke JavaScript to determine something is a submit button.

As for implicit submission, if the only FACE you have is a submit button, I think you'd want implicit submission from a non-FACE to do something with it.

It's a good question what should happen inside a FACE with regards to implicit submission. I guess that cannot deviate from what happens today, though perhaps we should create an affordance for that as well?

@clshortfuse
Copy link

clshortfuse commented Feb 14, 2023

@annevk Forgive my ignorance. I see now what you mean about invoking JS to get a state/property. I was thinking about how the disabled state works. In my use case I bind [disabled] to XElement.prototype.boolean so I had improperly assumed it was the JS element that was at work here.

The reality is [disabled] (the attribute) is tracked:

The element is a button, input, select, textarea, or form-associated custom element, and the disabled attribute is specified on this element (regardless of its value).

Along the same vein a [type=submit] check could allow a FACE to be form-associated submit button. It sounds like you can avoid JS that way. Though, I'm not sure how useful in practice it really is.

As for implicit submission, if the only FACE you have is a submit button, I think you'd want implicit submission from a non-FACE to do something with it.

The steps for implicit submission should cover it, IMO. Once the browser finds the submit button, FACE or native, it should perform a click() action.

It's a good question what should happen inside a FACE with regards to implicit submission. I guess that cannot deviate from what happens today, though perhaps we should create an affordance for that as well?

At first I was worried about authors who have built combo-boxes that expect ENTER to do something (eg: select from a dropdown list) and would inadvertently find themselves having their forms being submitted in an incompletely state because they now would have to ensure .preventDefault() was called. But on second thought, how would an author who is manually performing implicit submission steps (see my code above) know that a FACE is a submit button? I'm checking form.elements for (element.type === 'button'). But since XButton.prototype.type isn't mandatory, and neither is <x-button type="submit"> we either need a way to either allow authors to script their own implicit submission, or provide a function to invoke.

Example:

<x-checkbox>Vendor-1 Checkbox</x-checkbox>
<y-submit-button>Vendor-2 Submit button</y-submit-button>

If FACE XCheckbox wants to perform implicit submission, the steps I had of iterating (form.elements) is not sufficient because FACE Submit does not expose itself as submit button in any normalized way, even if YSubmitButton._elementInternals.submitButton === true . One solution is to require [type=submit]. Another would be a function that XCheckbox can invoke where the browser can perform all the implicit submission steps (eg: this._elementInternals.performImplicitSubmission()).

Side note: I did now notice that we don't have the ability to block implicit submission from FACE elements. Now that I understand [type] isn't tracked with FACE, but only with <input>, we don't have a way for FACE to satify:

[...] an element is a field that blocks implicit submission of a form element if it is an input element whose form owner is that form element and whose type attribute is in one of the following states: Text, Search, URL, Telephone, Email, Password, Date, Month, Week, Time, Local Date and Time, Number

That means we also need this._elementInternals.blocksImplicitSubmit === true. I don't personally like the concept of having a [type] attribute that is referred against a list. Something set internally just seems cleaner.


Summed up:

  1. this.#elementInternals.submitButton === true;
  2. this.#elementInternals.implicitSubmit();
  3. this.#elementInternals.blocksImplicitSubmit === true;

@annevk
Copy link
Collaborator

annevk commented Feb 14, 2023

Agreed on continuing to use the internal equivalent of click().

I'm not convinced we need to support the case of a form without a submit button that essentially has a single control. I suspect that behavior is there largely for isindex. Do you have a use case?

Implicit submission can be implemented as follows, though this is not very ergonomic:

const submitButton = control.form.querySelector(":default");
if (submitButton) {
  submitButton.click();
}

Offering a shortcut for that might be reasonable, though it should probably go on <form>. It would still be good to have some tests around this to see what implementations do here today.

@clshortfuse
Copy link

clshortfuse commented Feb 14, 2023

Unfortunately :default wouldn't be enough because native checkboxes and radio can also have it. The code could click a native checkbox. MDN's sample is a good example of this. It comes back to authors being able to know the element they are selecting for implicit submission is a button "type" and not just :default. Of course, having both :default and :default-button could solve this and keep code clean. Though, that doesn't account for submission-blocking form elements (native or FACE).

Or, we ask the browser to perform the steps. I thought of it being on <form>. It's just a matter of, do we want that function available anywhere, or scoped to just Custom Elements (which we can do with ElementInternals)?

@mfreed7
Copy link

mfreed7 commented Feb 24, 2023

@mfreed7 right, that's still the proposed API. The additional case discussed in the thread that I think has merit is supporting implicit submission better. There's no way currently to a submit a form using its default submit button and as @clshortfuse and I have shown the code for that is somewhat error-prone to write.

And then finally I suggested that maybe we should have a getter that returns the default submit button of a form.

Perhaps that getter and requestSubmit() could also suffice as supporting implicit submission.

Thanks. I like the idea of keeping the changes to the minimum set that meets the use case, if possible. Would just form.defaultSubmitButton be enough? Then form.defaultSubmitButton.click() is enough to submit it, right?

@clshortfuse
Copy link

clshortfuse commented Feb 24, 2023

Would just form.defaultSubmitButton be enough? Then form.defaultSubmitButton.click() is enough to submit it, right?

Seems like yes according to https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission

A form element's default button is the first submit button in tree order whose form owner is that form element.

const { form } = this.elementInternals;
const { defaultSubmitButton } = form;

If the user agent supports letting the user submit a form implicitly (for example, on some platforms hitting the "enter" key while a text control is focused implicitly submits the form), then doing so for a form, whose default button has activation behavior and is not disabled, must cause the user agent to fire a click event at that default button.

if (defaultSubmitButton) {
  if (!defaultSubmitButton.matches(':disabled')) { // edit: use :disabled instead of [disabled]
    defaultSubmitButton.click();
  }
  return;
}

There are pages on the web that are only usable if there is a way to implicitly submit forms, so user agents are strongly encouraged to support this.

If the form has no submit button, then the implicit submission mechanism must do nothing if the form has more than one field that blocks implicit submission, and must submit the form element from the form element itself otherwise.

if (getSubmissionBlockers(form).size > 1) return; // extraneous
form.requestSubmit();

That's how I'd implement it, given access to defaultSubmitButton.

@mfreed7
Copy link

mfreed7 commented Feb 25, 2023

Point taken - it’s still complicated. So we do need a way to do the submission also. Ok. I’m supportive of the concept.

@calebdwilliams
Copy link

I could be wrong but since the default submit button for a form is typically the first in source order wouldn’t it make more sense to have an API on either HTMLFormElement.prototype or on internals that could be called whenever an element becomes associated with a form

class CustomSubmitButton extends HTMLElement {
  static formAssociated = true;
  #internals = this.attachInternals();

  formAssociatedCallback(form) {
    this.#internals.registerSubmitButtonFor(form);
  }
}

@clshortfuse
Copy link

clshortfuse commented Feb 27, 2023

@calebdwilliams I think the difference would lie with it being implicit registration (.submitButton = boolean) vs explicit (register/unregister).

If it's a boolean it's contained in and of itself. The steps of implicit form submission will still find that element and check if it has that boolean set while it's running the traditional steps of walking the DOM tree. Attaching itself directly to the form (via the register/unregister functions) could change the steps required for finding the submit button during implicit submission. It implies there's a registration list that the form maintains. The form, internally would have to check if anything has attached itself as a submission (and it would probably be a list since there can be multiple), and then check each of them. Then it's a question of, does that occur before or after the DOM walking?

An example:

<form id=form>
  <input> <!-- Receives ENTER press -->
  <button type=submit>1</button>
  <x-submit>2</x-submit>
  <button type=submit>3</button>
<form>
<button type=submit for=form>4</button>
<x-submit for=form>5</x-submit>

Button 1 should take preference. If 1 is not available (disabled or removed) then 2. If not 2, then 3 (and so on). We don't want 4 to jump before 3, which could be the case if we bypass the DOM order steps.


Also, if a DOM is removed/moved, you have to unregister as well. And you need the form with which you were registered to keep proper logic symmetry (if there's a register, you assume an unregister). If you want to keep it implicit and "auto-unregister", then the form itself has to double check that the FACE is still associated with itself, or perform the unregister steps automatically. I feel like that's extra steps for the browser implementers. (And I guess maybe even yourself as a polyfill writer 😆 .)


There's also a lifecycle/state question. Is a FACE a submit button only if associated with a form? Can it just be a detached submit button? Can I register a button with a form manually? Can I use a detached form and register the submit before adding it to the DOM? Can a button be associated with one form, but the submit for another? If not, will it throw an error?

But I could be wrong of course, but as a component author, I don't see a problem with scripting my FACE submit buttons to flag themselves with a boolean, and scripting my FACE input fields to call the implicit submit function. In my logic, I would set a boolean as true if the [type] attribute is set to "submit" regardless if it's associated or not.


As for part of HTMLFormElement or internal, it's a question of scope. Non-boundary crossing native elements (aka LightDOM) don't need to attach ever, AFAIK. They have the [for] attribute and should be using that. Maybe we'd be facilitating some implementation for design that don't easily set element IDs (frameworks), but they still technically have one option. Giving them two may be needlessly confusing.


PS: Love your polyfill work and use them all the time :)

@calebdwilliams
Copy link

I’m not crazy about the assumption that the entire element itself should trigger the implicit submit process by default. What if there are other, non-actionable elements in that node’s shadow root (or even a reset or back button). I’d like to amend my suggestion above to have the internals method return a function that could be called to trigger submission from the custom element though.

Having a single flag and assuming that’s sufficient doesn’t always make sense in the use cases I’ve seen.

@annevk
Copy link
Collaborator

annevk commented Feb 27, 2023

@calebdwilliams I'm not sure what you're saying. The capabilities we're discussing would make the custom element submit button no different from <input type=submit>. The implicit submit process we're discussing is a separate independent feature that allows non-submit buttons to trigger form submission in the same way that the implicit form submission algorithm does.

@smaug----
Copy link

There is also the possibility to use <button type=submit is="my-custom-element">, no?
That isn't FACE though.

I don't quite understand what behavior
this.#internals.submitButton = true would give? Would that change the default activation behavior for the element?

@annevk
Copy link
Collaborator

annevk commented Feb 28, 2023

It would give the elements the semantics of a lowercase "submit button". So that means:

  • You can invoke requestSubmit() with it as argument.
  • It supports the formaction, formenctype, formmethod, formnovalidate, and formtarget attributes.
  • It partakes in implicit submission.
  • It can be returned from SubmitEvent.
  • :default can match it.
  • It cannot be a popover target element.

And yeah, it should have activation behavior that matches that of other submit buttons.

@smaug----
Copy link

I think I'm not keen on having magical states which change activation behavior. I'd rather expose some sort of activation callback to custom elements. That is roughly how XBL did it.

@clshortfuse
Copy link

clshortfuse commented Feb 28, 2023

I do question if FACE with submitButton === true should have that activation behavior done so by the browser (just because it's flagged). I think custom element author should do it.

As of now, we expect authors to call setFormValue(). I was expecting that type of opt-in interaction. In other words:

<!-- had set .submitButton during construction -->
<x-button onclick="this._elementInternals.form?.requestSubmit(this)">FACE Button</x-button>

<x-input onkeydown="event.key === 'Enter' ? this._elementInternals.implicitSubmit() : null">FACE Input</x-input>

:disabled FACE elements don't fire click events, so it shouldn't be too complicated for authors. <x-input> probably needs a disabled check.

Customized built-in elements ([is] attribute) don't need to use this. That should be handled by the browser because they're native HTMLButtonElement or native HTMLInputElement with [type=submit] set.

@annevk
Copy link
Collaborator

annevk commented Feb 28, 2023

I guess that could work as well. The lines are a bit blurred here and there, also with respect to attributes that carry certain semantics.

@keithamus
Copy link
Collaborator

WCCG had their spring F2F in which this was discussed. You can read the full notes of the discussion (#978 (comment)), heading entitled "Enhancements to Form-Associated Custom Elements".

@jpzwarte
Copy link

jpzwarte commented Jun 9, 2023

I just want to chime in that this could be related to supporting the popovertarget on custom element buttons (whatwg/html#9110). I think @annevk makes a point in that issue that perhaps the API here could be generalised to this.#elementInternals.button = 'button' | 'submit';, where 'button' would mean that the custom element can support the popovertarget attribute behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests