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

@uppy/form: migrate to TS #4937

Merged
merged 11 commits into from
Feb 28, 2024
1 change: 1 addition & 0 deletions packages/@uppy/core/src/UIPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ class UIPlugin<
export default UIPlugin

export type PluginTarget<M extends Meta, B extends Body> =
| null
Murderlon marked this conversation as resolved.
Show resolved Hide resolved
| string
| Element
| typeof BasePlugin
Expand Down
1 change: 1 addition & 0 deletions packages/@uppy/form/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tsconfig.*
144 changes: 0 additions & 144 deletions packages/@uppy/form/src/index.js

This file was deleted.

174 changes: 174 additions & 0 deletions packages/@uppy/form/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import BasePlugin, { type DefinePluginOpts } from '@uppy/core/lib/BasePlugin.js'
import findDOMElement from '@uppy/utils/lib/findDOMElement'
import toArray from '@uppy/utils/lib/toArray'

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore untyped
import getFormData from 'get-form-data'

import type { UIPluginOptions, Uppy, UppyEventMap } from '@uppy/core'
import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
import packageJson from '../package.json'

type Result<M extends Meta, B extends Body> = Parameters<
UppyEventMap<M, B>['complete']
>[0]

export interface FormOptions extends UIPluginOptions {
resultName?: string
getMetaFromForm?: boolean
addResultToForm?: boolean
submitOnSuccess?: boolean
triggerUploadOnSubmit?: boolean
}

const defaultOptions = {
target: null,
Murderlon marked this conversation as resolved.
Show resolved Hide resolved
resultName: 'uppyResult',
getMetaFromForm: true,
addResultToForm: true,
submitOnSuccess: false,
triggerUploadOnSubmit: false,
}

type Opts = DefinePluginOpts<FormOptions, keyof typeof defaultOptions>

export default class Form<M extends Meta, B extends Body> extends BasePlugin<
Opts,
M,
B
> {
static VERSION = packageJson.version

form: HTMLFormElement // TODO: make this private (or at least, mark it as readonly)

constructor(uppy: Uppy<M, B>, opts?: FormOptions) {
super(uppy, { ...defaultOptions, ...opts })
this.type = 'acquirer'
this.id = this.opts.id || 'Form'

this.handleFormSubmit = this.handleFormSubmit.bind(this)
this.handleUploadStart = this.handleUploadStart.bind(this)
this.handleSuccess = this.handleSuccess.bind(this)
this.addResultToForm = this.addResultToForm.bind(this)
this.getMetaFromForm = this.getMetaFromForm.bind(this)
}

handleUploadStart(): void {
if (this.opts.getMetaFromForm) {
this.getMetaFromForm()
}
}

handleSuccess(result: Result<M, B>): void {
if (this.opts.addResultToForm) {
this.addResultToForm(result)
}

if (this.opts.submitOnSuccess) {
this.form.requestSubmit()
}
}

handleFormSubmit(ev: Event): void {
Murderlon marked this conversation as resolved.
Show resolved Hide resolved
if (this.opts.triggerUploadOnSubmit) {
ev.preventDefault()
const elements = toArray((ev.target as HTMLFormElement).elements)
const disabledByUppy: HTMLButtonElement[] = []
elements.forEach((el) => {
const isButton =
el.tagName === 'BUTTON' ||
(el.tagName === 'INPUT' &&
(el as HTMLButtonElement).type === 'submit')
if (isButton && !(el as HTMLButtonElement).disabled) {
;(el as HTMLButtonElement).disabled = true // eslint-disable-line no-param-reassign
disabledByUppy.push(el as HTMLButtonElement)
}
})
this.uppy
.upload()
.then(
() => {
disabledByUppy.forEach((button) => {
button.disabled = false // eslint-disable-line no-param-reassign
})
},
(err) => {
disabledByUppy.forEach((button) => {
button.disabled = false // eslint-disable-line no-param-reassign
})
return Promise.reject(err)
},
)
.catch((err) => {
this.uppy.log(err.stack || err.message || err)
})
}
}

addResultToForm(result: Result<M, B>): void {
this.uppy.log('[Form] Adding result to the original form:')
this.uppy.log(result)

let resultInput: HTMLInputElement | null = this.form.querySelector(
`[name="${this.opts.resultName}"]`,
)
if (resultInput) {
// Append new result to the previous result array.
// If the previous result is empty, or not an array,
// set it to an empty array.
let updatedResult
try {
updatedResult = JSON.parse(resultInput.value)
} catch (err) {
// Nothing, since we check for array below anyway
}

if (!Array.isArray(updatedResult)) {
updatedResult = []
}
updatedResult.push(result)
resultInput.value = JSON.stringify(updatedResult)
return
}

resultInput = document.createElement('input')
resultInput.name = this.opts.resultName
resultInput.type = 'hidden'
resultInput.value = JSON.stringify([result])

this.form.appendChild(resultInput)
}

getMetaFromForm(): void {
const formMeta = getFormData(this.form)
// We want to exclude meta the the Form plugin itself has added
// See https://github.com/transloadit/uppy/issues/1637
delete formMeta[this.opts.resultName]
this.uppy.setMeta(formMeta)
}

install(): void {
this.form = findDOMElement(this.opts.target) as HTMLFormElement
Murderlon marked this conversation as resolved.
Show resolved Hide resolved

if (!this.form || this.form.nodeName !== 'FORM') {
this.uppy.log(
'Form plugin requires a <form> target element passed in options to operate, none was found',
'error',
)
return
}
Murderlon marked this conversation as resolved.
Show resolved Hide resolved

this.form.addEventListener('submit', this.handleFormSubmit)
this.uppy.on('upload', this.handleUploadStart)
this.uppy.on('complete', this.handleSuccess)
}

uninstall(): void {
this.form.removeEventListener('submit', this.handleFormSubmit)
this.uppy.off('upload', this.handleUploadStart)
this.uppy.off('complete', this.handleSuccess)
}
}
25 changes: 25 additions & 0 deletions packages/@uppy/form/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"extends": "../../../tsconfig.shared",
"compilerOptions": {
"noImplicitAny": false,
"outDir": "./lib",
"paths": {
"@uppy/utils/lib/*": ["../utils/src/*"],
"@uppy/core": ["../core/src/index.js"],
"@uppy/core/lib/*": ["../core/src/*"]
},
"resolveJsonModule": false,
"rootDir": "./src",
"skipLibCheck": true
},
"include": ["./src/**/*.*"],
"exclude": ["./src/**/*.test.ts"],
"references": [
{
"path": "../utils/tsconfig.build.json"
},
{
"path": "../core/tsconfig.build.json"
}
]
}
21 changes: 21 additions & 0 deletions packages/@uppy/form/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"extends": "../../../tsconfig.shared",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true,
"paths": {
"@uppy/utils/lib/*": ["../utils/src/*"],
"@uppy/core": ["../core/src/index.js"],
"@uppy/core/lib/*": ["../core/src/*"],
},
},
"include": ["./package.json", "./src/**/*.*"],
"references": [
{
"path": "../utils/tsconfig.build.json",
},
{
"path": "../core/tsconfig.build.json",
},
],
}