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

Extract HTMLFormSubmission tuple #885

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 13 additions & 86 deletions src/core/drive/form_submission.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { FetchRequest, FetchMethod, fetchMethodFromString } from "../../http/fetch_request"
import { expandURL } from "../url"
import { FetchRequest } from "../../http/fetch_request"
import { dispatch, getAttribute, getMetaContent, hasAttribute } from "../../util"
import { StreamMessage } from "../streams/stream_message"

Expand All @@ -12,80 +11,34 @@ export const FormSubmissionState = {
stopped: "stopped"
}

export const FormEnctype = {
urlEncoded: "application/x-www-form-urlencoded",
multipart: "multipart/form-data",
plain: "text/plain"
}

function formEnctypeFromString(encoding) {
switch (encoding.toLowerCase()) {
case FormEnctype.multipart:
return FormEnctype.multipart
case FormEnctype.plain:
return FormEnctype.plain
default:
return FormEnctype.urlEncoded
}
}

export class FormSubmission {
state = FormSubmissionState.initialized

static confirmMethod(message, _element, _submitter) {
return Promise.resolve(confirm(message))
}

constructor(delegate, formElement, submitter, mustRedirect = false) {
constructor(delegate, submission, mustRedirect = false) {
this.delegate = delegate
this.formElement = formElement
this.submitter = submitter
this.formData = buildFormData(formElement, submitter)
this.location = expandURL(this.action)
if (this.method == FetchMethod.get) {
mergeFormDataEntries(this.location, [...this.body.entries()])
}
this.submission = submission
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can call this argument and instance var htmlSubmission, to avoid confusion with FormSubmission.

this.formElement = submission.form
this.submitter = submission.submitter
this.formData = submission.formData
this.location = submission.location
this.method = submission.fetchMethod
this.action = submission.action
this.body = submission.body
this.enctype = submission.enctype
this.visitAction = submission.visitAction
this.frame = submission.frame
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement)
this.mustRedirect = mustRedirect
}

get method() {
const method = this.submitter?.getAttribute("formmethod") || this.formElement.getAttribute("method") || ""
return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get
}

get action() {
const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null

if (this.submitter?.hasAttribute("formaction")) {
return this.submitter.getAttribute("formaction") || ""
} else {
return this.formElement.getAttribute("action") || formElementAction || ""
}
}

get body() {
if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
return new URLSearchParams(this.stringFormData)
} else {
return this.formData
}
}

get enctype() {
return formEnctypeFromString(this.submitter?.getAttribute("formenctype") || this.formElement.enctype)
}

get isSafe() {
return this.fetchRequest.isSafe
}

get stringFormData() {
return [...this.formData].reduce((entries, [name, value]) => {
return entries.concat(typeof value == "string" ? [[name, value]] : [])
}, [])
}

// The submission process

async start() {
Expand Down Expand Up @@ -217,18 +170,6 @@ export class FormSubmission {
}
}

function buildFormData(formElement, submitter) {
const formData = new FormData(formElement)
const name = submitter?.getAttribute("name")
const value = submitter?.getAttribute("value")

if (name) {
formData.append(name, value || "")
}

return formData
}

function getCookieValue(cookieName) {
if (cookieName != null) {
const cookies = document.cookie ? document.cookie.split("; ") : []
Expand All @@ -243,17 +184,3 @@ function getCookieValue(cookieName) {
function responseSucceededWithoutRedirect(response) {
return response.statusCode == 200 && !response.redirected
}

function mergeFormDataEntries(url, entries) {
const searchParams = new URLSearchParams()

for (const [name, value] of entries) {
if (value instanceof File) continue

searchParams.append(name, value)
}

url.search = searchParams.toString()

return url
}
115 changes: 115 additions & 0 deletions src/core/drive/html_form_submission.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { expandURL } from "../url"
import { getAttribute, getVisitAction } from "../../util"
import { FetchMethod, fetchMethodFromString } from "../../http/fetch_request"

export const FormEnctype = {
urlEncoded: "application/x-www-form-urlencoded",
multipart: "multipart/form-data",
plain: "text/plain"
}

export function formEnctypeFromString(encoding) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move this function to the bottom and remove the export. It's only used in this file, further down.

switch (encoding.toLowerCase()) {
case FormEnctype.multipart:
return FormEnctype.multipart
case FormEnctype.plain:
return FormEnctype.plain
default:
return FormEnctype.urlEncoded
}
}

export class HTMLFormSubmission {
constructor(form, submitter) {
this.form = form
this.submitter = submitter

const url = expandURL(this.action)

this.location = this.isSafe ? mergeFormDataEntries(url, [...this.body.entries()]) : url
}

closest(selectors) {
return this.form.closest(selectors)
}

get method() {
return this.submitter?.getAttribute("formmethod") || this.form.getAttribute("method") || ""
}

get fetchMethod() {
return fetchMethodFromString(this.method.toLowerCase()) || FetchMethod.get
}

get target() {
if (this.submitter?.hasAttribute("formtarget") || this.form.hasAttribute("target")) {
return this.submitter?.getAttribute("formtarget") || this.form.getAttribute("target")
} else {
return null
}
Comment on lines +45 to +49
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (this.submitter?.hasAttribute("formtarget") || this.form.hasAttribute("target")) {
return this.submitter?.getAttribute("formtarget") || this.form.getAttribute("target")
} else {
return null
}
return this.submitter?.getAttribute("formtarget") || this.form.getAttribute("target") || null

}

get action() {
const formElementAction = typeof this.form.action === "string" ? this.form.action : null

if (this.submitter?.hasAttribute("formaction")) {
return this.submitter.getAttribute("formaction") || ""
} else {
return this.form.getAttribute("action") || formElementAction || ""
}
Comment on lines +55 to +59
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (this.submitter?.hasAttribute("formaction")) {
return this.submitter.getAttribute("formaction") || ""
} else {
return this.form.getAttribute("action") || formElementAction || ""
}
return this.submitter?.getAttribute("formaction") || this.form.getAttribute("action") || formElementAction || ""

}

get formData() {
const formData = new FormData(this.form)
const name = this.submitter?.getAttribute("name")
const value = this.submitter?.getAttribute("value")

if (name) {
formData.append(name, value || "")
}

return formData
}

get enctype() {
return formEnctypeFromString(this.submitter?.getAttribute("formenctype") || this.form.enctype)
}

get body() {
if (this.enctype == FormEnctype.urlEncoded || this.fetchMethod == FetchMethod.get) {
const formDataAsStrings = [...this.formData].reduce((entries, [name, value]) => {
return entries.concat(typeof value == "string" ? [[name, value]] : [])
}, [])

return new URLSearchParams(formDataAsStrings)
Comment on lines +80 to +84
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const formDataAsStrings = [...this.formData].reduce((entries, [name, value]) => {
return entries.concat(typeof value == "string" ? [[name, value]] : [])
}, [])
return new URLSearchParams(formDataAsStrings)
const params = new URLSearchParams()
for (const [name, value] of this.formData.entries()) {
if (typeof value === "string") params.append(name, value)
}

We could extract a this.formDataAsURLParams helper method from this bit, for symmetry with this.formData.

} else {
return this.formData
}
}

get visitAction() {
return getVisitAction(this.submitter, this.form)
}

get frame() {
return getAttribute("data-turbo-frame", this.submitter, this.form)
}

get isSafe() {
return this.fetchMethod === FetchMethod.get
}
}

function mergeFormDataEntries(url, entries) {
const searchParams = new URLSearchParams()

for (const [name, value] of entries) {
if (value instanceof File) continue

searchParams.append(name, value)
}

url.search = searchParams.toString()

return url
}
9 changes: 4 additions & 5 deletions src/core/drive/navigator.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getVisitAction } from "../../util"
import { FormSubmission } from "./form_submission"
import { expandURL, getAnchor, getRequestURL, locationIsVisitable } from "../url"
import { Visit } from "./visit"
Expand Down Expand Up @@ -28,9 +27,9 @@ export class Navigator {
this.currentVisit.start()
}

submitForm(form, submitter) {
submitForm(htmlFormSubmission) {
this.stop()
this.formSubmission = new FormSubmission(this, form, submitter, true)
this.formSubmission = new FormSubmission(this, htmlFormSubmission, true)

this.formSubmission.start()
}
Expand Down Expand Up @@ -151,7 +150,7 @@ export class Navigator {
return this.history.restorationIdentifier
}

getActionForFormSubmission({ submitter, formElement }) {
return getVisitAction(submitter, formElement) || "advance"
getActionForFormSubmission(htmlFormSubmission) {
return htmlFormSubmission.visitAction || "advance"
}
}
Loading