A file upload area, as a button or a drop zone.
Usage
el_upload(
id = NULL,
button_label = "Upload",
drag = FALSE,
multiple = FALSE,
accept = NULL,
limit = NULL,
show_file_list = TRUE,
list_type = "text",
auto_upload = TRUE,
disabled = FALSE,
name = NULL,
tip = NULL,
action = NULL,
headers = NULL,
extra_data = NULL,
file_list = NULL,
with_credentials = NULL,
before_upload = NULL,
before_remove = NULL,
on_change = NULL,
on_progress = NULL,
on_preview = NULL,
on_remove = NULL,
on_exceed = NULL,
label = NULL,
label_position = c("top", "left", "right"),
label_width = NULL,
label_suffix = NULL,
required = FALSE,
error = NULL,
show_message = TRUE,
inline_message = FALSE,
crossorigin = NULL,
directory = NULL,
width = NULL,
slots = NULL,
session = NULL
)
update_el_upload(
session = shiny::getDefaultReactiveDomain(),
id,
disabled = NULL,
limit = NULL,
label = NULL,
error = NULL,
button_label = NULL,
drag = NULL,
multiple = NULL,
accept = NULL,
show_file_list = NULL,
list_type = NULL,
auto_upload = NULL,
headers = NULL,
extra_data = NULL,
file_list = NULL,
with_credentials = NULL,
before_upload = NULL,
before_remove = NULL,
on_change = NULL,
on_progress = NULL,
on_preview = NULL,
on_remove = NULL,
on_exceed = NULL,
crossorigin = NULL,
directory = NULL
)Arguments
- id
Upload ID (auto-generated if NULL).
Text for the trigger, as
buttonLabelis forshiny::fileInput(): on the button whendrag = FALSE, inside the drop zone otherwise.- drag
Render a drop zone rather than a button.
- multiple
Allow selecting several files at once.
- accept
File types to accept, as an
acceptattribute would have them, e.g.".csv,.tsv"or"image/*".- limit
Maximum number of files.
- show_file_list
Show the list of chosen files.
- list_type
"text"(default),"picture"or"picture-card".- auto_upload
Start uploading as soon as files are chosen.
- disabled
Disable the control.
- name
Field name Element posts the file under. Only meaningful with
action, where it names the multipart field; it defaults to"file"there. Withoutactionthe field has no name: Shiny's own file-input binding claims everyinput[type=file]on the page that has an id or a name, and would add aninput$<name>of its own besideinput$<id>.- tip
Help text shown under the control.
- action
Post to this URL using Element's own upload instead of Shiny's channel. See details.
- headers
Request headers, as a named list.
- extra_data
Extra fields sent alongside the file, as a named list.
- file_list
Files shown initially, each
list(name=, url=).- with_credentials
Whether to send cookies with the request.
- before_upload
JS()function called before a file is sent; returningfalsecancels it.- before_remove
JS()function called before a file is removed; returningfalsecancels it.- on_change
JS()function called when a file is added, or finishes.- on_progress
JS()function called as a file uploads.- on_preview
JS()function called when an uploaded file is clicked.- on_remove
JS()function called after a file is removed.- on_exceed
JS()function called when more files are picked thanlimit.- label
A label shown with the component, as Shiny's inputs have: text or a tag.
NULL, the default, shows none. It is the component's accessible name too – tied to it withforwhere the component has a native input that takes the id<id>-input, else witharia-labelledby.- label_position
Where the label sits, as
el_form()'slabel_position:"top"(the default, as Shiny's labels sit), or beside the component, its text aligned"left"or"right"– which shows oncelabel_widthgives the labels a common width.- label_width
Width of a label beside the component, as a CSS unit, so that several line up. Element's
label-width.- label_suffix
Text after the label, such as
":". Element'slabel-suffix.- required
Draw Element's red asterisk before the label. It marks the field; it does not check it – shinyvalidate or
el_form()does that.- error
An error message shown under the component in Element's style, the field framed in red. Element's
error.- show_message, inline_message
Whether
error's message is shown, and whether beside the component rather than under it. Element'sshow-messageandinline-message.- crossorigin
Native attribute crossorigin. Element Plus's
crossorigin(” | 'anonymous' | 'use-credentials').- directory
Whether to support uploading directory. After enabling it, only folders can be selected, and after selecting a folder, the files within the folder will be flattened. Element Plus's
directory(boolean).- width
Component width, as a CSS unit –
"200px","50%", or a number taken as pixels. Element's own markup carries it, so it behaves like thewidthargument of a Shiny input.- slots
Named list of Element slot contents:
default(the trigger, in place of the button),tip,file(each file of the list, scoped withfile),trigger. A shiny.element component given here is absorbed rather than nested. For a scoped slot, write the template withtemplate().- session
In
el_upload(), deprecated: inside a module, wrapidinns(), as for any Shiny input; a session given here namespacesidonce more, with a warning. Inupdate_el_upload(), the Shiny session, the current one by default, as forshiny::updateTextInput().
Details
By default the files travel through Shiny's own upload channel, so
input$<id> is the same data frame shiny::fileInput() produces, complete
with a datapath pointing at a temporary file. Element's file list,
progress bars and hooks all keep working: only the transport is replaced.
Give action instead to use Element's own upload, posting straight to that
URL. Shiny then plays no part – useful for a pre-signed S3 URL or an
existing file service, but the server sees no datapath.
Shiny inputs
Without action, input$<id> is a data frame of name, size, type
and datapath, one row per file in the last batch, exactly as
shiny::fileInput() reports it. Uploads inherit Shiny's
shiny.maxRequestSize limit and its temporary-file cleanup.
With action, Shiny never sees the files; input$<id>_success lists the
names of files Element uploaded successfully.
Either way, input$<id>_error is the name of a file that failed, as an
event. Without action, the rest of its batch is sent again without it,
so input$<id> holds the files that arrived; a file stopped with
abort() is left out the same way.
Files go up one at a time, as shiny::fileInput() sends them, and each
is marked done once the whole batch has reached the server.
Element methods
Callable with call_el():
abort()– Cancel upload request: one file, given as itsuid, or every file in flightclearFiles()– Clear the uploaded file list (this method is not supported in the before-upload hook)submit()– Upload the file list manually
Updating from the server
update_el_upload() changes the component from the server.
Every other argument of el_upload() that can change once it is
drawn is an argument here too, under the same name. One left NULL
stays as it is; NA returns it to Element's default.
update_el_upload() is called for its side effect and returns NULL invisibly.
Examples
# A drop zone taking several CSVs, read on the server like fileInput()
el_upload(
"files",
drag = TRUE,
multiple = TRUE,
accept = ".csv",
tip = "CSV files only"
)
#> <div id="files" data-shiny-vue style="display: contents">
#> <script type="text/x-template" data-shiny-vue-template><div id="files_container" style="display: contents">
#> <el-upload ref="upload" name="" action="#" :multiple="multiple" :show-file-list="showFileList" :list-type="listType" :auto-upload="autoUpload" :disabled="disabled" :accept="accept === null ? undefined : accept" :limit="limit === null ? undefined : limit" :on-success="handleSuccess" :on-error="handleError" :drag="drag" :headers="headers === null ? undefined : headers" :data="extraData === null ? undefined : extraData" v-model:file-list="fileList" :with-credentials="withCredentials === null ? undefined : withCredentials" :before-upload="beforeUpload === null ? undefined : beforeUpload" :before-remove="beforeRemove === null ? undefined : beforeRemove" :on-change="onChange === null ? undefined : onChange" :on-progress="onProgress === null ? undefined : onProgress" :on-preview="onPreview === null ? undefined : onPreview" :on-remove="onRemove === null ? undefined : onRemove" :on-exceed="onExceed === null ? undefined : onExceed" :http-request="shinyUpload" :crossorigin="crossorigin === null ? undefined : crossorigin" :directory="directory === null ? undefined : directory">
#> <el-icon class="el-icon--upload" v-if="drag">
#> <upload-filled></upload-filled>
#> </el-icon>
#> <div class="el-upload__text" v-if="drag">{{buttonLabel}}</div>
#> <el-icon v-else-if="listType === 'picture-card'">
#> <plus></plus>
#> </el-icon>
#> <el-button v-else size="small" type="primary">{{buttonLabel}}</el-button>
#> <template v-slot:tip>
#> <div class="el-upload__tip">CSV files only</div>
#> </template>
#> </el-upload>
#> </div></script>
#> <script type="application/json" data-shiny-vue-options>{"options":{"data":{"drag":true,"buttonLabel":"Upload","multiple":true,"showFileList":true,"listType":"text","autoUpload":true,"disabled":false,"accept":".csv","limit":null,"succeeded":[],"failed":"","headers":null,"extraData":null,"fileList":[],"withCredentials":null,"beforeUpload":null,"beforeRemove":null,"onChange":null,"onProgress":null,"onPreview":null,"onRemove":null,"onExceed":null,"crossorigin":null,"directory":null},"methods":{"handleSuccess":"function(response, file, fileList) { this.succeeded = fileList.filter(function(f) { return f.status === 'success'; }).map(function(f) { return f.name; }); window.Shiny && Shiny.setInputValue && Shiny.setInputValue('files_success', this.succeeded); }","handleError":"function(err, file) { this.failed = file.name; window.Shiny && Shiny.setInputValue && Shiny.setInputValue('files_error', file.name, {priority: 'event'}); }","shinyUpload":"function(options) {\n var self = this, inputId = \"files\";\n if (!window.Shiny || !Shiny.shinyapp) {\n options.onError(new Error('no Shiny session to upload to'));\n return Object.defineProperty(Object.create(XMLHttpRequest.prototype), 'abort', { value: function() {} });\n }\n var entry = { o: options, aborted: false, failed: false, xhr: null };\n self._queue = self._queue || [];\n self._queue.push(entry);\n if (!self._flushing) {\n self._flushing = true;\n Promise.resolve().then(function() {\n self._flushing = false;\n runJob(self._queue.splice(0));\n });\n }\n function warn(m) { if (window.console) console.warn('[shiny.element] upload: ' + m); }\n function abandon(res) {\n window.Shiny && Shiny.setInputValue && Shiny.setInputValue('.shiny_element_upload_abandon:shiny.element.upload_abandon',\n res.jobId, { priority: 'event' });\n }\n function runJob(batch) {\n batch = batch.filter(function(e) { return !e.aborted && !e.failed; });\n if (!batch.length) return;\n var info = batch.map(function(e) {\n return { name: e.o.file.name, size: e.o.file.size, type: e.o.file.type };\n });\n Shiny.shinyapp.makeRequest('uploadInit', [info], function(res) {\n postNext(batch, 0, res);\n }, function(err) {\n warn('uploadInit: ' + err);\n batch.forEach(function(e) { e.failed = true; e.o.onError(new Error(String(err))); });\n });\n }\n function postNext(batch, i, res) {\n if (i === batch.length) return finish(batch, res);\n var e = batch[i];\n if (e.aborted) { abandon(res); return runJob(batch); }\n e.xhr = $.ajax(res.uploadUrl, {\n type: 'POST', cache: false, data: e.o.file,\n processData: false, contentType: 'application/octet-stream',\n xhr: function() {\n var x = new window.XMLHttpRequest();\n x.upload.addEventListener('progress', function(ev) {\n if (ev.lengthComputable) {\n e.o.onProgress({ percent: Math.min(99, Math.round(ev.loaded / ev.total * 100)) });\n }\n });\n return x;\n },\n success: function() { e.xhr = null; postNext(batch, i + 1, res); },\n error: function(x, status) {\n e.xhr = null;\n if (!e.aborted) {\n e.failed = true;\n e.o.onError(new Error('Upload failed for ' + e.o.file.name + ': ' + (status || 'error')));\n }\n abandon(res);\n runJob(batch);\n }\n });\n }\n function finish(batch, res) {\n if (batch.some(function(e) { return e.aborted; })) { abandon(res); return runJob(batch); }\n Shiny.shinyapp.makeRequest('uploadEnd', [res.jobId, inputId], function() {\n batch.forEach(function(e) { if (!e.aborted) e.o.onSuccess({ ok: true }); });\n }, function(err) {\n warn('uploadEnd: ' + err);\n abandon(res);\n batch.forEach(function(e) { e.failed = true; e.o.onError(new Error(String(err))); });\n });\n }\n var handle = Object.create(XMLHttpRequest.prototype);\n Object.defineProperty(handle, 'abort', { value: function() {\n entry.aborted = true;\n if (entry.xhr) entry.xhr.abort();\n } });\n return handle;\n}","shinyVueReceive":"function(d) { if (d['.action'] === 'clear') { if (this.$refs.upload) this.$refs.upload.clearFiles(); this.succeeded = []; window.Shiny && Shiny.setInputValue && Shiny.setInputValue('files_success', []); } delete d['.action']; return d; }"}},"input":null,"rate":null,"type":null,"use":["shinyElement.plugin"],"evals":["options.methods.handleSuccess","options.methods.handleError","options.methods.shinyUpload","options.methods.shinyVueReceive"]}</script>
#> </div>
# A plain button
el_upload("avatar", button_label = "Choose a picture", accept = "image/*")
#> <div id="avatar" data-shiny-vue style="display: contents">
#> <script type="text/x-template" data-shiny-vue-template><div id="avatar_container" style="display: contents">
#> <el-upload ref="upload" name="" action="#" :multiple="multiple" :show-file-list="showFileList" :list-type="listType" :auto-upload="autoUpload" :disabled="disabled" :accept="accept === null ? undefined : accept" :limit="limit === null ? undefined : limit" :on-success="handleSuccess" :on-error="handleError" :drag="drag" :headers="headers === null ? undefined : headers" :data="extraData === null ? undefined : extraData" v-model:file-list="fileList" :with-credentials="withCredentials === null ? undefined : withCredentials" :before-upload="beforeUpload === null ? undefined : beforeUpload" :before-remove="beforeRemove === null ? undefined : beforeRemove" :on-change="onChange === null ? undefined : onChange" :on-progress="onProgress === null ? undefined : onProgress" :on-preview="onPreview === null ? undefined : onPreview" :on-remove="onRemove === null ? undefined : onRemove" :on-exceed="onExceed === null ? undefined : onExceed" :http-request="shinyUpload" :crossorigin="crossorigin === null ? undefined : crossorigin" :directory="directory === null ? undefined : directory">
#> <el-icon class="el-icon--upload" v-if="drag">
#> <upload-filled></upload-filled>
#> </el-icon>
#> <div class="el-upload__text" v-if="drag">{{buttonLabel}}</div>
#> <el-icon v-else-if="listType === 'picture-card'">
#> <plus></plus>
#> </el-icon>
#> <el-button v-else size="small" type="primary">{{buttonLabel}}</el-button>
#> </el-upload>
#> </div></script>
#> <script type="application/json" data-shiny-vue-options>{"options":{"data":{"drag":false,"buttonLabel":"Choose a picture","multiple":false,"showFileList":true,"listType":"text","autoUpload":true,"disabled":false,"accept":"image/*","limit":null,"succeeded":[],"failed":"","headers":null,"extraData":null,"fileList":[],"withCredentials":null,"beforeUpload":null,"beforeRemove":null,"onChange":null,"onProgress":null,"onPreview":null,"onRemove":null,"onExceed":null,"crossorigin":null,"directory":null},"methods":{"handleSuccess":"function(response, file, fileList) { this.succeeded = fileList.filter(function(f) { return f.status === 'success'; }).map(function(f) { return f.name; }); window.Shiny && Shiny.setInputValue && Shiny.setInputValue('avatar_success', this.succeeded); }","handleError":"function(err, file) { this.failed = file.name; window.Shiny && Shiny.setInputValue && Shiny.setInputValue('avatar_error', file.name, {priority: 'event'}); }","shinyUpload":"function(options) {\n var self = this, inputId = \"avatar\";\n if (!window.Shiny || !Shiny.shinyapp) {\n options.onError(new Error('no Shiny session to upload to'));\n return Object.defineProperty(Object.create(XMLHttpRequest.prototype), 'abort', { value: function() {} });\n }\n var entry = { o: options, aborted: false, failed: false, xhr: null };\n self._queue = self._queue || [];\n self._queue.push(entry);\n if (!self._flushing) {\n self._flushing = true;\n Promise.resolve().then(function() {\n self._flushing = false;\n runJob(self._queue.splice(0));\n });\n }\n function warn(m) { if (window.console) console.warn('[shiny.element] upload: ' + m); }\n function abandon(res) {\n window.Shiny && Shiny.setInputValue && Shiny.setInputValue('.shiny_element_upload_abandon:shiny.element.upload_abandon',\n res.jobId, { priority: 'event' });\n }\n function runJob(batch) {\n batch = batch.filter(function(e) { return !e.aborted && !e.failed; });\n if (!batch.length) return;\n var info = batch.map(function(e) {\n return { name: e.o.file.name, size: e.o.file.size, type: e.o.file.type };\n });\n Shiny.shinyapp.makeRequest('uploadInit', [info], function(res) {\n postNext(batch, 0, res);\n }, function(err) {\n warn('uploadInit: ' + err);\n batch.forEach(function(e) { e.failed = true; e.o.onError(new Error(String(err))); });\n });\n }\n function postNext(batch, i, res) {\n if (i === batch.length) return finish(batch, res);\n var e = batch[i];\n if (e.aborted) { abandon(res); return runJob(batch); }\n e.xhr = $.ajax(res.uploadUrl, {\n type: 'POST', cache: false, data: e.o.file,\n processData: false, contentType: 'application/octet-stream',\n xhr: function() {\n var x = new window.XMLHttpRequest();\n x.upload.addEventListener('progress', function(ev) {\n if (ev.lengthComputable) {\n e.o.onProgress({ percent: Math.min(99, Math.round(ev.loaded / ev.total * 100)) });\n }\n });\n return x;\n },\n success: function() { e.xhr = null; postNext(batch, i + 1, res); },\n error: function(x, status) {\n e.xhr = null;\n if (!e.aborted) {\n e.failed = true;\n e.o.onError(new Error('Upload failed for ' + e.o.file.name + ': ' + (status || 'error')));\n }\n abandon(res);\n runJob(batch);\n }\n });\n }\n function finish(batch, res) {\n if (batch.some(function(e) { return e.aborted; })) { abandon(res); return runJob(batch); }\n Shiny.shinyapp.makeRequest('uploadEnd', [res.jobId, inputId], function() {\n batch.forEach(function(e) { if (!e.aborted) e.o.onSuccess({ ok: true }); });\n }, function(err) {\n warn('uploadEnd: ' + err);\n abandon(res);\n batch.forEach(function(e) { e.failed = true; e.o.onError(new Error(String(err))); });\n });\n }\n var handle = Object.create(XMLHttpRequest.prototype);\n Object.defineProperty(handle, 'abort', { value: function() {\n entry.aborted = true;\n if (entry.xhr) entry.xhr.abort();\n } });\n return handle;\n}","shinyVueReceive":"function(d) { if (d['.action'] === 'clear') { if (this.$refs.upload) this.$refs.upload.clearFiles(); this.succeeded = []; window.Shiny && Shiny.setInputValue && Shiny.setInputValue('avatar_success', []); } delete d['.action']; return d; }"}},"input":null,"rate":null,"type":null,"use":["shinyElement.plugin"],"evals":["options.methods.handleSuccess","options.methods.handleError","options.methods.shinyUpload","options.methods.shinyVueReceive"]}</script>
#> </div>
# Element's own upload, straight to a pre-signed URL
el_upload("direct", action = "https://example.invalid/presigned")
#> <div id="direct" data-shiny-vue style="display: contents">
#> <script type="text/x-template" data-shiny-vue-template><div id="direct_container" style="display: contents">
#> <el-upload ref="upload" name="file" action="https://example.invalid/presigned" :multiple="multiple" :show-file-list="showFileList" :list-type="listType" :auto-upload="autoUpload" :disabled="disabled" :accept="accept === null ? undefined : accept" :limit="limit === null ? undefined : limit" :on-success="handleSuccess" :on-error="handleError" :drag="drag" :headers="headers === null ? undefined : headers" :data="extraData === null ? undefined : extraData" v-model:file-list="fileList" :with-credentials="withCredentials === null ? undefined : withCredentials" :before-upload="beforeUpload === null ? undefined : beforeUpload" :before-remove="beforeRemove === null ? undefined : beforeRemove" :on-change="onChange === null ? undefined : onChange" :on-progress="onProgress === null ? undefined : onProgress" :on-preview="onPreview === null ? undefined : onPreview" :on-remove="onRemove === null ? undefined : onRemove" :on-exceed="onExceed === null ? undefined : onExceed" :crossorigin="crossorigin === null ? undefined : crossorigin" :directory="directory === null ? undefined : directory">
#> <el-icon class="el-icon--upload" v-if="drag">
#> <upload-filled></upload-filled>
#> </el-icon>
#> <div class="el-upload__text" v-if="drag">{{buttonLabel}}</div>
#> <el-icon v-else-if="listType === 'picture-card'">
#> <plus></plus>
#> </el-icon>
#> <el-button v-else size="small" type="primary">{{buttonLabel}}</el-button>
#> </el-upload>
#> </div></script>
#> <script type="application/json" data-shiny-vue-options>{"options":{"data":{"drag":false,"buttonLabel":"Upload","multiple":false,"showFileList":true,"listType":"text","autoUpload":true,"disabled":false,"accept":null,"limit":null,"succeeded":[],"failed":"","headers":null,"extraData":null,"fileList":[],"withCredentials":null,"beforeUpload":null,"beforeRemove":null,"onChange":null,"onProgress":null,"onPreview":null,"onRemove":null,"onExceed":null,"crossorigin":null,"directory":null},"methods":{"handleSuccess":"function(response, file, fileList) { this.succeeded = fileList.filter(function(f) { return f.status === 'success'; }).map(function(f) { return f.name; }); window.Shiny && Shiny.setInputValue && Shiny.setInputValue('direct_success', this.succeeded); }","handleError":"function(err, file) { this.failed = file.name; window.Shiny && Shiny.setInputValue && Shiny.setInputValue('direct_error', file.name, {priority: 'event'}); }","shinyVueReceive":"function(d) { if (d['.action'] === 'clear') { if (this.$refs.upload) this.$refs.upload.clearFiles(); this.succeeded = []; window.Shiny && Shiny.setInputValue && Shiny.setInputValue('direct_success', []); } delete d['.action']; return d; }"}},"input":null,"rate":null,"type":null,"use":["shinyElement.plugin"],"evals":["options.methods.handleSuccess","options.methods.handleError","options.methods.shinyVueReceive"]}</script>
#> </div>
if (interactive()) {
library(shiny)
library(shiny.element)
ui <- el_page(
el_upload("files", drag = TRUE, multiple = TRUE),
tableOutput("info")
)
server <- function(input, output, session) {
output$info <- renderTable({
req(input$files)
input$files[, c("name", "size", "type")]
})
}
shinyApp(ui, server)
}
if (interactive()) {
# inside a server function
observeEvent(input$go, {
update_el_upload(session, "files", disabled = TRUE)
})
}