Extract metadata from form, closes #153

This PR adds support for extracting metadata from `<form>` elements.
This was asked #153 and came up elsewhere too, I believe. You can use
it like this:

```html
<form class="MyForm" action="/">
  <input type="file" />
  <input type="hidden" name="bla" value="12333">
  <input type="text" name="yo" value="1">
  <button type="submit">Upload</button>
</form>
```

```js
uppy.use(DragDrop, {
    target: '.MyForm',
    locale: {
      strings: {chooseFile: 'Выберите файл'}
    },
    setMetaFromTargetForm: true
  })
```

Then UI acquire type plugins, like Dashboard, FileInput and DragDrop,
before mounting themselves or doing anything else, extract FormData
from the `target` `<form>` element (it must be a form currently), and
merge the object with the global `state.meta`. This is done via
`setMeta` method on core. Then, when a file is added, `state.meta` is
merged to that file’s meta right away. The ability to add more fields
via UI plugin MetaData is also still there, though probably needs an
update.

In addition a new `meta` option is added in core:

```js
const uppy = Uppy({
  debug: true,
  autoProceed: true,
  meta: {
    username: 'Artur'
  }
})
```

This way some data can be set right away, it becomes the initial
`state.meta` object.
This commit is contained in:
Artur Paikin 2017-06-28 20:21:23 -04:00
parent 362ed5ef9b
commit 6f80ea709c
9 changed files with 128 additions and 54 deletions

View file

@ -7,12 +7,15 @@
</head>
<body>
<h1>Uppy is here</h1>
<button id="uppyModalOpener">Open Modal</button>
<button id="uppyModalOpener">Choose Files</button>
<div class="Uppy">
<form class="UppyForm" action="/">
<form class="MyForm" action="/">
<input type="file" />
<input type="hidden" name="bla" value="12333">
<input type="text" name="yo" value="1">
<button type="submit">Upload</button>
<input type="submit">
</form>
</div>

View file

@ -1,10 +1,12 @@
const Uppy = require('../../src/core/Core.js')
const Dashboard = require('../../src/plugins/Dashboard')
const GoogleDrive = require('../../src/plugins/GoogleDrive')
const Dropbox = require('../../src/plugins/Dropbox')
const Webcam = require('../../src/plugins/Webcam')
// const Dashboard = require('../../src/plugins/Dashboard')
// const GoogleDrive = require('../../src/plugins/GoogleDrive')
// const Dropbox = require('../../src/plugins/Dropbox')
// const Webcam = require('../../src/plugins/Webcam')
const Tus10 = require('../../src/plugins/Tus10')
// const Multipart = require('../../src/plugins/Multipart')
// const DragDrop = require('../../src/plugins/FileInput')
const FileInput = require('../../src/plugins/FileInput')
const MetaData = require('../../src/plugins/MetaData')
// const Informer = require('../../src/plugins/Informer')
// const StatusBar = require('../../src/plugins/StatusBar')
@ -17,30 +19,44 @@ const TUS_ENDPOINT = PROTOCOL + '://master.tus.io/files/'
// import MagicLog from '../../src/plugins/MagicLog'
// import PersistentState from '../../src/plugins/PersistentState'
const uppy = Uppy({debug: true, autoProceed: false})
.use(Dashboard, {
trigger: '#uppyModalOpener',
// maxWidth: 350,
// maxHeight: 400,
// inline: false,
// disableStatusBar: true,
// disableInformer: true,
target: 'body',
const uppy = Uppy({
debug: true,
autoProceed: true,
meta: {
username: 'Artur'
}
})
// .use(Dashboard, {
// trigger: '#uppyModalOpener',
// // maxWidth: 350,
// // maxHeight: 400,
// inline: false,
// // disableStatusBar: true,
// // disableInformer: true,
// setMetaFromTargetForm: true,
// target: '.MyForm',
// locale: {
// strings: { browse: 'wow' }
// }
// })
// .use(GoogleDrive, {target: Dashboard, host: 'http://localhost:3020'})
// .use(Dropbox, {target: Dashboard, host: 'http://localhost:3020'})
.use(FileInput, {
target: '.MyForm',
replaceTargetContent: false,
setMetaFromTargetForm: true,
locale: {
strings: {browse: 'wow'}
}
})
.use(GoogleDrive, {target: Dashboard, host: 'http://localhost:3020'})
.use(Dropbox, {target: Dashboard, host: 'http://localhost:3020'})
// .use(FileInput, {target: '.Uppy', locale: {
// strings: {selectToUpload: 'Выберите файл для загрузки'}
// }})
// .use(DragDrop, {target: 'body', locale: {
// strings: {chooseFile: 'Выберите файл'}
// }})
strings: { selectToUpload: 'Выберите файл для загрузки' }
}})
// .use(DragDrop, {
// target: '.MyForm',
// locale: {
// strings: {chooseFile: 'Выберите файл'}
// },
// setMetaFromTargetForm: true
// })
// .use(ProgressBar, {target: 'body'})
// .use(dummy)
.use(Webcam, {target: Dashboard})
// .use(Webcam, {target: Dashboard})
// .use(Multipart, {endpoint: '//api2.transloadit.com'})
.use(Tus10, {endpoint: TUS_ENDPOINT, resume: true})
// .use(Informer, {target: Dashboard})

View file

@ -53,7 +53,8 @@ class Uppy {
capabilities: {
resumableUploads: false
},
totalProgress: 0
totalProgress: 0,
meta: Object.assign({}, this.opts.meta)
}
// for debugging and testing
@ -134,6 +135,11 @@ class Uppy {
}
}
setMeta (data) {
const newMeta = Object.assign({}, this.getState().meta, data)
this.setState({meta: newMeta})
}
updateMeta (data, fileID) {
const updatedFiles = Object.assign({}, this.getState().files)
const newMeta = Object.assign({}, updatedFiles[fileID].meta, data)
@ -159,9 +165,7 @@ class Uppy {
id: fileID,
name: fileName,
extension: fileExtension || '',
meta: {
name: fileName
},
meta: Object.assign({}, this.getState().metaData),
type: {
general: fileTypeGeneral,
specific: fileTypeSpecific
@ -187,7 +191,7 @@ class Uppy {
updatedFiles[fileID] = newFile
this.setState({files: updatedFiles})
this.bus.emit('file-added', fileID)
this.bus.emit('core:file-added', fileID)
this.log(`Added file: ${fileName}, ${fileID}, mime type: ${fileType}`)
if (this.opts.autoProceed && !this.scheduledAutoProceed) {
@ -526,16 +530,18 @@ class Uppy {
if (!this.opts.debug) {
return
}
if (type === 'error') {
console.error(`LOG: ${msg}`)
return
}
if (msg === `${msg}`) {
console.log(`LOG: ${msg}`)
} else {
console.dir(msg)
}
if (type === 'error') {
console.error(`LOG: ${msg}`)
}
global.uppyLog = global.uppyLog + '\n' + 'DEBUG LOG: ' + msg
}

View file

@ -401,6 +401,27 @@ function findDOMElement (element) {
}
}
/**
* Get metadata object from a form element
*
* @param {Node|string} element
* @return {Object|null}
*/
function getMetaFromForm (element) {
if (!element || element.tagName !== 'FORM') {
console.error('Metadata can only be extracted from form elements')
return null
}
var formData = new FormData(element)
var result = {}
for (var entry of formData.entries()) {
result[entry[0]] = entry[1]
}
return result
}
function getSocketHost (url) {
// get the host domain
var regex = /^(?:https?:\/\/|\/\/)?(?:[^@\n]+@)?(?:www\.)?([^\n]+)/
@ -451,6 +472,7 @@ module.exports = {
copyToClipboard,
prettyETA,
findDOMElement,
getMetaFromForm,
getSocketHost,
emitSocketProgress
}

View file

@ -48,6 +48,7 @@ module.exports = class DashboardUI extends Plugin {
semiTransparent: false,
defaultTabIcon: defaultTabIcon(),
showProgressDetails: false,
setMetaFromTargetForm: false,
locale: defaultLocale
}
@ -88,7 +89,7 @@ module.exports = class DashboardUI extends Plugin {
if (callerPluginType !== 'acquirer' &&
callerPluginType !== 'progressindicator' &&
callerPluginType !== 'presenter') {
let msg = 'Error: Modal can only be used by plugins of types: acquirer, progressindicator, presenter'
let msg = 'Dashboard: Modal can only be used by plugins of types: acquirer, progressindicator, presenter'
this.core.log(msg)
return
}
@ -162,9 +163,9 @@ module.exports = class DashboardUI extends Plugin {
// focus on modal inner block
this.target.querySelector('.UppyDashboard-inner').focus()
this.updateDashboardElWidth()
// this.updateDashboardElWidth()
// to be sure, sometimes when the function runs, container size is still 0
setTimeout(this.updateDashboardElWidth, 300)
setTimeout(this.updateDashboardElWidth, 500)
}
// Close the Modal on esc key press
@ -175,14 +176,14 @@ module.exports = class DashboardUI extends Plugin {
}
initEvents () {
// const dashboardEl = this.target.querySelector(`${this.opts.target} .UppyDashboard`)
// Modal open button
const showModalTrigger = findDOMElement(this.opts.trigger)
if (!this.opts.inline && showModalTrigger) {
showModalTrigger.addEventListener('click', this.showModal)
} else {
this.core.log('Modal trigger wasnt found')
}
if (!this.opts.inline && !showModalTrigger) {
console.error('Dashboard modal trigger not found, you wont be able to select files. Make sure `trigger` is set correctly in Dashboard options')
}
document.body.addEventListener('keyup', this.handleEscapeKeyPress)
@ -223,7 +224,7 @@ module.exports = class DashboardUI extends Plugin {
updateDashboardElWidth () {
const dashboardEl = this.target.querySelector('.UppyDashboard-inner')
// console.log(dashboardEl.offsetWidth)
this.core.log(`Dashboard width: ${dashboardEl.offsetWidth}`)
const modal = this.core.getState().modal
this.core.setState({
@ -303,9 +304,9 @@ module.exports = class DashboardUI extends Plugin {
return target.type === 'progressindicator'
})
const addFile = (file) => {
this.core.emitter.emit('core:file-add', file)
}
// const addFile = (file) => {
// this.core.emitter.emit('core:file-add', file)
// }
const removeFile = (fileID) => {
this.core.emitter.emit('core:file-remove', fileID)
@ -364,7 +365,7 @@ module.exports = class DashboardUI extends Plugin {
i18n: this.containerWidth,
pauseAll: this.pauseAll,
resumeAll: this.resumeAll,
addFile: addFile,
addFile: this.core.addFile,
removeFile: removeFile,
info: info,
metaFields: state.metaFields,
@ -392,6 +393,10 @@ module.exports = class DashboardUI extends Plugin {
targets: []
}})
if (this.opts.setMetaFromTargetForm) {
this.setMetaFromTargetForm()
}
const target = this.opts.target
const plugin = this
this.target = this.mount(target, plugin)

View file

@ -37,6 +37,7 @@ module.exports = class DragDrop extends Plugin {
// Default options
const defaultOpts = {
target: '.UppyDragDrop',
setMetaFromTargetForm: false,
locale: defaultLocale
}
@ -143,6 +144,10 @@ module.exports = class DragDrop extends Plugin {
}
install () {
if (this.opts.setMetaFromTargetForm) {
this.setMetaFromTargetForm()
}
const target = this.opts.target
const plugin = this
this.target = this.mount(target, plugin)

View file

@ -7,7 +7,7 @@ module.exports = class FileInput extends Plugin {
constructor (core, opts) {
super(core, opts)
this.id = 'FileInput'
this.title = 'FileInput'
this.title = 'File Input'
this.type = 'acquirer'
const defaultLocale = {
@ -23,7 +23,8 @@ module.exports = class FileInput extends Plugin {
multipleFiles: true,
pretty: true,
locale: defaultLocale,
inputName: 'files[]'
inputName: 'files[]',
getMetaDataFromForm: false
}
// Merge default options with the ones set by user
@ -77,6 +78,10 @@ module.exports = class FileInput extends Plugin {
}
install () {
if (this.opts.setMetaFromTargetForm) {
this.setMetaFromTargetForm()
}
const target = this.opts.target
const plugin = this
this.target = this.mount(target, plugin)

View file

@ -38,7 +38,7 @@ module.exports = class MetaData extends Plugin {
metaFields: metaFields
})
this.core.emitter.on('file-added', this.handleFileAdded)
this.core.emitter.on('core:file-added', this.handleFileAdded)
}
install () {
@ -46,6 +46,6 @@ module.exports = class MetaData extends Plugin {
}
uninstall () {
this.core.emitter.off('file-added', this.handleFileAdded)
this.core.emitter.off('core:file-added', this.handleFileAdded)
}
}

View file

@ -1,6 +1,6 @@
const yo = require('yo-yo')
const nanoraf = require('nanoraf')
const { findDOMElement } = require('../core/Utils')
const { findDOMElement, getMetaFromForm } = require('../core/Utils')
/**
* Boilerplate that all Plugins share - and should not be used
@ -89,6 +89,18 @@ module.exports = class Plugin {
}
}
setMetaFromTargetForm () {
const el = findDOMElement(this.opts.target)
const formMetaData = getMetaFromForm(el)
if (formMetaData) {
this.core.log('Adding metadata from form:')
this.core.log(formMetaData)
this.core.setMeta(formMetaData)
} else {
this.core.log('Couldnt extract metadata from form')
}
}
// focus () {
// return
// }