From cd179ecad945e5a316845e8c9a0decf6f4bf1ea6 Mon Sep 17 00:00:00 2001 From: Vincent Rolea Date: Tue, 12 Oct 2021 15:34:24 +0100 Subject: [PATCH 1/2] Add data-confirm behavior to turbo (#379) * Add data-confirm behavior to turbo * Extend confirm behavior to data-turbo-method links * Add possibility to override native confirm modal --- src/core/drive/form_submission.ts | 18 +++++++ src/core/index.ts | 5 ++ src/core/session.ts | 4 ++ src/tests/fixtures/form.html | 4 ++ src/tests/functional/form_submission_tests.ts | 49 +++++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/src/core/drive/form_submission.ts b/src/core/drive/form_submission.ts index 56f57d385..d183c40e7 100644 --- a/src/core/drive/form_submission.ts +++ b/src/core/drive/form_submission.ts @@ -49,6 +49,10 @@ export class FormSubmission { state = FormSubmissionState.initialized result?: FormSubmissionResult + static confirmMethod(message: string, element: HTMLFormElement):boolean { + return confirm(message) + } + constructor(delegate: FormSubmissionDelegate, formElement: HTMLFormElement, submitter?: HTMLElement, mustRedirect = false) { this.delegate = delegate this.formElement = formElement @@ -94,10 +98,24 @@ export class FormSubmission { }, [] as [string, string][]) } + get confirmationMessage() { + return this.formElement.getAttribute("data-turbo-confirm") + } + + get needsConfirmation() { + return this.confirmationMessage !== null + } + // The submission process async start() { const { initialized, requesting } = FormSubmissionState + + if (this.needsConfirmation) { + const answer = FormSubmission.confirmMethod(this.confirmationMessage!, this.formElement) + if (!answer) { return } + } + if (this.state == initialized) { this.state = requesting return this.fetchRequest.perform() diff --git a/src/core/index.ts b/src/core/index.ts index d7910d360..40141119d 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -6,6 +6,7 @@ import { StreamSource } from "./types" import { VisitOptions } from "./drive/visit" import { PageRenderer } from "./drive/page_renderer" import { PageSnapshot } from "./drive/page_snapshot" +import { FormSubmission } from "./drive/form_submission" const session = new Session const { navigator } = session @@ -96,3 +97,7 @@ export function clearCache() { export function setProgressBarDelay(delay: number) { session.setProgressBarDelay(delay) } + +export function setConfirmMethod(confirmMethod: (message: string, element: HTMLFormElement)=>boolean) { + FormSubmission.confirmMethod = confirmMethod +} diff --git a/src/core/session.ts b/src/core/session.ts index 851bb7c9f..b9c5b597e 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -148,6 +148,10 @@ export class Session implements FormSubmitObserverDelegate, HistoryDelegate, Lin form.action = link.getAttribute("href") || "undefined" form.hidden = true + if (link.hasAttribute("data-turbo-confirm")) { + form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm")!) + } + link.parentNode?.insertBefore(form, link) return dispatch("submit", { cancelable: true, target: form }) } else { diff --git a/src/tests/fixtures/form.html b/src/tests/fixtures/form.html index 1729d7a70..3ac0aa7b6 100644 --- a/src/tests/fixtures/form.html +++ b/src/tests/fixtures/form.html @@ -53,6 +53,9 @@

Form

+
+ +

@@ -194,6 +197,7 @@

Frame: Form

Stream link inside frame + Stream link inside frame with confirmation
diff --git a/src/tests/functional/form_submission_tests.ts b/src/tests/functional/form_submission_tests.ts index 358fe5043..03551d950 100644 --- a/src/tests/functional/form_submission_tests.ts +++ b/src/tests/functional/form_submission_tests.ts @@ -21,6 +21,32 @@ export class FormSubmissionTests extends TurboDriveTestCase { this.assert.notOk(await this.hasSelector(".turbo-progress-bar"), "hides progress bar") } + async "test form submission with confirmation confirmed"() { + await this.clickSelector("#standard form.confirm input[type=submit]") + + this.assert.equal(await this.getAlertText(), "Are you sure?") + await this.acceptAlert() + this.assert.ok(await this.formSubmitted) + } + + async "test form submission with confirmation cancelled"() { + await this.clickSelector("#standard form.confirm input[type=submit]") + + this.assert.equal(await this.getAlertText(), "Are you sure?") + await this.dismissAlert() + this.assert.notOk(await this.formSubmitted) + } + + async "test from submission with confirmation overriden"() { + await this.remote.execute(() => window.Turbo.setConfirmMethod((message, element) => confirm("Overriden message"))) + + await this.clickSelector("#standard form.confirm input[type=submit]") + + this.assert.equal(await this.getAlertText(), "Overriden message") + await this.acceptAlert() + this.assert.ok(await this.formSubmitted) + } + async "test standard form submission does not render a progress bar before expiring the delay"() { await this.remote.execute(() => window.Turbo.setProgressBarDelay(500)) await this.clickSelector("#standard form.redirect input[type=submit]") @@ -449,6 +475,29 @@ export class FormSubmissionTests extends TurboDriveTestCase { this.assert.equal(await message.getVisibleText(), "Link!") } + async "test link method form submission inside frame with confirmation confirmed"() { + await this.clickSelector("#link-method-inside-frame-with-confirmation") + + this.assert.equal(await this.getAlertText(), "Are you sure?") + await this.acceptAlert() + + await this.nextBeat + + const message = await this.querySelector("#frame div.message") + this.assert.equal(await message.getVisibleText(), "Link!") + } + + async "test link method form submission inside frame with confirmation cancelled"() { + await this.clickSelector("#link-method-inside-frame-with-confirmation") + + this.assert.equal(await this.getAlertText(), "Are you sure?") + await this.dismissAlert() + + await this.nextBeat + + this.assert.notOk(await this.hasSelector("#frame div.message"), "Not confirming form submission does not submit the form") + } + async "test link method form submission outside frame"() { await this.clickSelector("#link-method-outside-frame") From 58d2261274533a80a2c5efda7da211b3f20efcbb Mon Sep 17 00:00:00 2001 From: Sam Pohlenz Date: Thu, 14 Oct 2021 22:12:38 +1030 Subject: [PATCH 2/2] Load data-turbo-method links within the correct turbo frame (#411) * append the created form for data-turbo-method at the end of body (#341) * Load data-turbo-method links within the correct turbo frame (#358) Co-authored-by: David Gil --- src/core/session.ts | 24 +++++++- src/tests/fixtures/form.html | 16 +++++- src/tests/functional/form_submission_tests.ts | 57 +++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/core/session.ts b/src/core/session.ts index b9c5b597e..03abab318 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -152,14 +152,21 @@ export class Session implements FormSubmitObserverDelegate, HistoryDelegate, Lin form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm")!) } - link.parentNode?.insertBefore(form, link) + const frame = this.getTargetFrameForLink(link) + if (frame) { + form.setAttribute("data-turbo-frame", frame) + form.addEventListener("turbo:submit-start", () => form.remove()) + } else { + form.addEventListener("submit", () => form.remove()) + } + + document.body.appendChild(form) return dispatch("submit", { cancelable: true, target: form }) } else { return false } } - // Navigator delegate allowsVisitingLocationWithAction(location: URL, action?: Action) { @@ -335,6 +342,19 @@ export class Session implements FormSubmitObserverDelegate, HistoryDelegate, Lin return isAction(action) ? action : "advance" } + getTargetFrameForLink(link: Element) { + const frame = link.getAttribute("data-turbo-frame") + + if (frame) { + return frame + } else { + const container = link.closest("turbo-frame") + if (container) { + return container.id + } + } + } + get snapshot() { return this.view.snapshot } diff --git a/src/tests/fixtures/form.html b/src/tests/fixtures/form.html index 3ac0aa7b6..0740572e8 100644 --- a/src/tests/fixtures/form.html +++ b/src/tests/fixtures/form.html @@ -196,8 +196,15 @@

Frame: Form

- Stream link inside frame + Method link inside frame
+ Break-out of frame with method link inside frame
+ Method link inside frame targeting another frame
+ Stream link inside frame Stream link inside frame with confirmation +
+ Method link within form inside frame
+ Stream link within form inside frame +
@@ -220,7 +227,12 @@

Frame: Form

- Stream link outside frame + Method link outside frame
+ Stream link outside frame + + Method link within form outside frame
+ Stream link within form outside frame +

diff --git a/src/tests/functional/form_submission_tests.ts b/src/tests/functional/form_submission_tests.ts index 03551d950..7d92ce3ee 100644 --- a/src/tests/functional/form_submission_tests.ts +++ b/src/tests/functional/form_submission_tests.ts @@ -468,7 +468,41 @@ export class FormSubmissionTests extends TurboDriveTestCase { async "test link method form submission inside frame"() { await this.clickSelector("#link-method-inside-frame") + await this.nextBeat + + const title = await this.querySelector("#frame h2") + this.assert.equal(await title.getVisibleText(), "Frame: Loaded") + this.assert.notOk(await this.hasSelector("#nested-child")) + } + + async "test link method form submission inside frame with data-turbo-frame=_top"() { + await this.clickSelector("#link-method-inside-frame-target-top") + await this.nextBody + + const title = await this.querySelector("h1") + this.assert.equal(await title.getVisibleText(), "Hello") + } + async "test link method form submission inside frame with data-turbo-frame target"() { + await this.clickSelector("#link-method-inside-frame-with-target") + await this.nextBeat + + const title = await this.querySelector("h1") + const frameTitle = await this.querySelector("#hello h2") + this.assert.equal(await frameTitle.getVisibleText(), "Hello from a frame") + this.assert.equal(await title.getVisibleText(), "Form") + } + + async "test stream link method form submission inside frame"() { + await this.clickSelector("#stream-link-method-inside-frame") + await this.nextBeat + + const message = await this.querySelector("#frame div.message") + this.assert.equal(await message.getVisibleText(), "Link!") + } + + async "test link method form submission within form inside frame"() { + await this.clickSelector("#stream-link-method-within-form-inside-frame") await this.nextBeat const message = await this.querySelector("#frame div.message") @@ -500,7 +534,30 @@ export class FormSubmissionTests extends TurboDriveTestCase { async "test link method form submission outside frame"() { await this.clickSelector("#link-method-outside-frame") + await this.nextBody + + const title = await this.querySelector("h1") + this.assert.equal(await title.getVisibleText(), "Hello") + } + + async "test stream link method form submission outside frame"() { + await this.clickSelector("#stream-link-method-outside-frame") + await this.nextBeat + + const message = await this.querySelector("#frame div.message") + this.assert.equal(await message.getVisibleText(), "Link!") + } + + async "test link method form submission within form outside frame"() { + await this.clickSelector("#link-method-within-form-outside-frame") + await this.nextBody + + const title = await this.querySelector("h1") + this.assert.equal(await title.getVisibleText(), "Hello") + } + async "test stream link method form submission within form outside frame"() { + await this.clickSelector("#stream-link-method-within-form-outside-frame") await this.nextBeat const message = await this.querySelector("#frame div.message")