Skip to content

Commit

Permalink
Allow changing the submitter text during form submission (#869)
Browse files Browse the repository at this point in the history
This change introduces a new optional data-turbo-submits-with text
attribute that can be set on submit elements (inputs or buttons) in
Turbo forms.

      <form action="/" method="post">
        <input type="submit" value="Save" data-turbo-submits-with="Saving...">
      </form>

When the form is in a submitting state Turbo will change the submitter
content -the input value for inputs, and the innerHTML for buttons- with
the data-turbo-submits-with value, and restore the original value when the
submission ends.

This is a common requirement in many apps, to style the form submitting
state and give feedback to the user that a click is already being
processed.
  • Loading branch information
afcapel authored Feb 23, 2023
1 parent e013072 commit 455ffe0
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 0 deletions.
31 changes: 31 additions & 0 deletions src/core/drive/form_submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class FormSubmission {
readonly mustRedirect: boolean
state = FormSubmissionState.initialized
result?: FormSubmissionResult
originalSubmitText?: string

static confirmMethod(
message: string,
Expand Down Expand Up @@ -165,6 +166,7 @@ export class FormSubmission {
requestStarted(_request: FetchRequest) {
this.state = FormSubmissionState.waiting
this.submitter?.setAttribute("disabled", "")
this.setSubmitsWith()
dispatch<TurboSubmitStartEvent>("turbo:submit-start", {
target: this.formElement,
detail: { formSubmission: this },
Expand Down Expand Up @@ -202,6 +204,7 @@ export class FormSubmission {
requestFinished(_request: FetchRequest) {
this.state = FormSubmissionState.stopped
this.submitter?.removeAttribute("disabled")
this.resetSubmitterText()
dispatch<TurboSubmitEndEvent>("turbo:submit-end", {
target: this.formElement,
detail: { formSubmission: this, ...this.result },
Expand All @@ -211,13 +214,41 @@ export class FormSubmission {

// Private

setSubmitsWith() {
if (!this.submitter || !this.submitsWith) return

if (this.submitter.matches("button")) {
this.originalSubmitText = this.submitter.innerHTML
this.submitter.innerHTML = this.submitsWith
} else if (this.submitter.matches("input")) {
const input = this.submitter as HTMLInputElement
this.originalSubmitText = input.value
input.value = this.submitsWith
}
}

resetSubmitterText() {
if (!this.submitter || !this.originalSubmitText) return

if (this.submitter.matches("button")) {
this.submitter.innerHTML = this.originalSubmitText
} else if (this.submitter.matches("input")) {
const input = this.submitter as HTMLInputElement
input.value = this.originalSubmitText
}
}

requestMustRedirect(request: FetchRequest) {
return !request.isIdempotent && this.mustRedirect
}

requestAcceptsTurboStreamResponse(request: FetchRequest) {
return !request.isIdempotent || hasAttribute("data-turbo-stream", this.submitter, this.formElement)
}

get submitsWith() {
return this.submitter?.getAttribute("data-turbo-submits-with")
}
}

function buildFormData(formElement: HTMLFormElement, submitter?: HTMLElement): FormData {
Expand Down
11 changes: 11 additions & 0 deletions src/tests/fixtures/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ <h1>Form</h1>
<input type="hidden" name="path" value="/src/tests/fixtures/form.html">
<button id="standard-get-form-with-stream-opt-in-submitter" data-turbo-stream>form[method=get] button[data-turbo-stream]</button>
</form>
<form action="/__turbo/redirect" method="post" class="redirect">
<input type="hidden" name="path" value="/src/tests/fixtures/form.html">
<input type="hidden" name="greeting" value="Hello from a redirect">
<input id="submits-with-form-input" type="submit" value="Save" data-turbo-submits-with="Saving...">
</form>
<form action="/__turbo/redirect" method="post" class="redirect">
<input type="hidden" name="path" value="/src/tests/fixtures/form.html">
<input type="hidden" name="sleep" value="200">
<input type="hidden" name="greeting" value="Hello from a redirect">
<button id="submits-with-form-button" type="submit" data-turbo-submits-with="Saving...">Save</button>
</form>
<hr>
<form>
<button id="form-action-none-q-a" name="q" value="a">Submit ?q=a to form:not([action])</button>
Expand Down
34 changes: 34 additions & 0 deletions src/tests/functional/form_submission_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,40 @@ test("test standard POST form submission toggles submitter [disabled] attribute"
)
})

test("replaces input value with data-turbo-submits-with on form submission", async ({ page }) => {
page.click("#submits-with-form-input")

assert.equal(
await nextAttributeMutationNamed(page, "submits-with-form-input", "value"),
"Saving...",
"sets data-turbo-submits-with on the submitter"
)

assert.equal(
await nextAttributeMutationNamed(page, "submits-with-form-input", "value"),
"Save",
"restores the original submitter text value"
)
})

test("replaces button innerHTML with data-turbo-submits-with on form submission", async ({ page }) => {
await page.click("#submits-with-form-button")

await nextEventNamed(page, "turbo:submit-start")
assert.equal(
await page.textContent("#submits-with-form-button"),
"Saving...",
"sets data-turbo-submits-with on the submitter"
)

await nextEventNamed(page, "turbo:submit-end")
assert.equal(
await page.textContent("#submits-with-form-button"),
"Save",
"sets data-turbo-submits-with on the submitter"
)
})

test("test standard GET form submission", async ({ page }) => {
await page.click("#standard form.greeting input[type=submit]")
await nextBody(page)
Expand Down

0 comments on commit 455ffe0

Please sign in to comment.