Skip to main content

Upload Deprecated

The upload component is used to allow users to upload one / several files to an upload server.

This component can either be used in a nearly autonomous way by using its default "internal" operation mode, or it can keep state for you upload code via its "external" operation mode, the internal mode requires a server that can receive uploads via POST requests and DELETE requests on the same URL. Internal mode assumes that you want to upload files immediately when a user has selected one or more files and that your server is able to receive a Delete request to remove any files uploads again.

Examples #

Open in new window
<duet-layout center>
<div slot="main"> <duet-card heading="upload server status">
<style>
duet-spinner {
position: relative;
left: 20px;
}
</style>
<div id="upload-server-test">
Testing upload server availability <duet-spinner size="small" theme="default"></duet-spinner>
</div>
<script>
fetch("https://duetds-upload-server.azurewebsites.net/ping").then(response=>{
if(response.statusText==="OK"){
document.getElementById("upload-server-test").innerHTML = `server is up! `
}
}).catch(e=>{
document.getElementById("upload-server-test").innerHTML = "server is down!, refresh this page to retry"
})
</script>
</duet-card>
<duet-card heading="Upload Demonstrator">
<form action="" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*,application/*"
max-bytes="2048000000000000"
max-files="20"
required
show-links
uri="https://duetds-upload-server.azurewebsites.net/uploads"
>

<div slot="header">This text is in Header slot</div>
<div slot="fileheader">This text is in fileheader slot</div>
<div slot="filefooter">This text is in filefooter slot</div>
</duet-upload>
<duet-button disabled="true" fixed id="submit" submit> submit</duet-button>
<duet-button fixed id="cancel"> cancel</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload");
var submitButton = document.getElementById("submit");

function toggleButton(valid) {
console.log("does element contain any files?", valid);
if (valid) {
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}

// duetChange event listener
element.addEventListener("duetChange", function(e) {
console.log("duet-upload-change", e);
toggleButton(element.files.size);

});

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e);
toggleButton(element.files.size);
});


// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e);
toggleButton(element.files.size);
});

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e);
toggleButton(element.files.size);
});

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e);
toggleButton(element.files.size);
});

</script>
Open in new window
<duet-layout center>
<div slot="main"> <duet-card heading="upload server status">
<style>
duet-spinner {
position: relative;
left: 20px;
}
</style>
<div id="upload-server-test">
Testing upload server availability <duet-spinner size="small" theme="default"></duet-spinner>
</div>
<script>
fetch("https://duetds-upload-server.azurewebsites.net/ping").then(response=>{
if(response.statusText==="OK"){
document.getElementById("upload-server-test").innerHTML = `server is up! `
}
}).catch(e=>{
document.getElementById("upload-server-test").innerHTML = "server is down!, refresh this page to retry"
})
</script>
</duet-card>
<duet-card heading="Upload Demonstrator">
<form action="" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*,application/*"
max-bytes="2048000000000000"
max-files="20"
required
show-links
uri="https://duetds-upload-server.azurewebsites.net/uploads"
defer-upload
>

<div slot="header">This text is in Header slot</div>
<div slot="fileheader">This text is in fileheader slot</div>
<div slot="filefooter">This text is in filefooter slot</div>
</duet-upload>
<duet-button disabled="true" fixed id="submit"> submit</duet-button>
<duet-button fixed id="cancel"> cancel</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload");
var submitButton = document.getElementById("submit");

function toggleButton(valid) {
console.log("does element contain any files?", valid);
if (valid) {
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}

submitButton.addEventListener("click", async function(e) {
e.preventDefault()
e.cancelBubble = true;
e.stopPropagation();
console.log("upload clicked")
await element.uploadPending();

});

// duetChange event listener
element.addEventListener("duetChange", function(e) {
console.log("duet-upload-change", e);
toggleButton(element.files.size);

});

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e);
toggleButton(element.files.size);
});


// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e);
toggleButton(element.files.size);
});

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e);
toggleButton(element.files.size);
});

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e);
toggleButton(element.files.size);
});

</script>
Open in new window
<duet-layout center>
<div slot="main">
<duet-card heading="upload server status">
<style>
duet-spinner {
position: relative;
left: 20px;
}
</style>
<div id="upload-server-test">
Testing upload server availability <duet-spinner size="small" theme="default"></duet-spinner>
</div>
<script>
fetch("https://duetds-upload-server.azurewebsites.net/ping").then(response=>{
if(response.statusText==="OK"){
document.getElementById("upload-server-test").innerHTML = `server is up! `
}
}).catch(e=>{
document.getElementById("upload-server-test").innerHTML = "server is down!, refresh this page to retry"
})
</script>
</duet-card>
<duet-card heading="Upload Demonstrator internal with middleware">
<duet-paragraph>
This demonstrator shows how to use a middleware to add custom headers to the outgoing XHR request
It has a deferred upload and will show links depending on server support

</duet-paragraph>
<form action="" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*,application/*"
max-bytes="2048000000000000"
max-files="20"
required
show-links
uri="https://duetds-upload-server.azurewebsites.net/uploads"
defer-upload
>

<div slot="header">This text is in Header slot</div>
<div slot="fileheader">This text is in fileheader slot</div>
<div slot="filefooter">This text is in filefooter slot</div>
</duet-upload>
<duet-button disabled="true" fixed id="submit"> submit</duet-button>
<duet-button fixed id="cancel"> cancel</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload");
var submitButton = document.getElementById("submit");

// manipulate the internal XHR object with a middleware function
element.middleware = (xhr)=>{
console.group();

console.log("middleware was called with the following object: ", xhr);
const { options } = xhr;
console.log("options are:", {options})

const newOptions = Object.assign({}, options, {
headers: {
"x-custom-header-1": Date.now(),
"x-custom-header-2": Math.random(),
}
})
console.log("modified options are:", {newOptions})
xhr.options = newOptions
console.log("passing back xhr object :", xhr)
console.groupEnd();
return xhr;
}

function toggleButton(valid) {
console.log("does element contain any files?", valid);
if (valid) {
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}

submitButton.addEventListener("click", async function(e) {
e.preventDefault()
e.cancelBubble = true;
e.stopPropagation();
console.log("upload clicked")
await element.uploadPending();

});

// duetChange event listener
element.addEventListener("duetChange", function(e) {
console.log("duet-upload-change", e);

toggleButton(element.files.size);

});

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e);
toggleButton(element.files.size);
});


// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e);
toggleButton(element.files.size);
});

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e);
toggleButton(element.files.size);
});

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e);
toggleButton(element.files.size);
});

// add duetCancel event listener
element.addEventListener("duetReady", function(e) {
console.log("native change", e);
//customize the unknown errorcode
console.group();

element.errorCodes.forEach((item, index)=>{
if(item.type==="default"){
element.errorCodes[index].message = {
"fi": "I was modified by in-page javascript",
"sv": "I was modified by in-page javascript",
"en": "I was modified by in-page javascript"
}
element.errorCodes[index].system_message = "modified error"
}
if(item.type==="duet-upload-100"){
element.errorCodes[index].message = {
"fi": "I was modified by in-page javascript",
"sv": "I was modified by in-page javascript",
"en": "I was modified by in-page javascript"
}
element.errorCodes[index].system_message = "modified extension error"
}

})

console.log("Modifying default errorcodes", element.errorCodes)

console.groupEnd();
});



</script>
Open in new window
<duet-layout margin="none" center>
<div slot="main"> <duet-card heading="upload server status">
<style>
duet-spinner {
position: relative;
left: 20px;
}
</style>
<div id="upload-server-test">
Testing upload server availability <duet-spinner size="small" theme="default"></duet-spinner>
</div>
<script>
fetch("https://duetds-upload-server.azurewebsites.net/ping").then(response=>{
if(response.statusText==="OK"){
document.getElementById("upload-server-test").innerHTML = `server is up! `
}
}).catch(e=>{
document.getElementById("upload-server-test").innerHTML = "server is down!, refresh this page to retry"
})
</script>
</duet-card>
<duet-card heading="Upload Demonstrator">
<form method="post" action="http://www.goggle.com" id="duet-upload-form" name="duet-upload-form-name">
<duet-upload
uri="https://duetds-upload-server.azurewebsites.net/uploads"
max-files="2"
max-bytes="204800000"
required
limit-selection
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*"
>

</duet-upload>
<duet-button variation="primary" submit fixed disabled="true" id="submit"> Submit </duet-button>
<duet-button fixed id="cancel"> Cancel </duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload")
var submitButton = document.getElementById("submit")

function toggleButton( valid ){
console.log("is element valid?", valid)
if(valid){
submitButton.disabled = false
}else{
submitButton.disabled = true
}
}
// duetChange event listener
element.addEventListener("duetChange", function (e) {
console.log("duet-upload-change", e)
toggleButton(element.valid)

})

// duetDelete event listener
element.addEventListener("duetDelete", function (e) {
console.log("duet-upload-delete", e)
toggleButton(element.valid)
})


// add duetDone event listener
element.addEventListener("duetDone", function (e) {
console.log("duet-upload-done", e)
toggleButton(element.valid)
})

// add duetCancel event listener
element.addEventListener("duetCancel", function (e) {
console.log("duet-upload-cancel", e)
toggleButton(element.valid)
})

// add duetCancel event listener
element.addEventListener("duetState", function (e) {
console.log("native change", e)
toggleButton(element.valid)
})
</script>
Open in new window
<duet-layout center margin="none">

<div slot="main">
<duet-card heading="Notes on example">
<duet-paragraph>This upload demonstrator has in page source javascript that enables / disables the submit button
if the upload list contains files with errors (this behaviour is entirely up to you to controll, depending on
your UX demands)
</duet-paragraph>

</duet-card>

<duet-card heading="Upload Demonstrator">
<form action="www.duetds.com" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*"
external
limit-selection
max-bytes="204800000"
max-files="2"
required
>

<div slot="header">This upload example uses external functions to handle the upload</div>
<div slot="fileheader">This text is in the fileheader slot</div>
<div slot="filefooter">This text is in the filefooter slot</div>
</duet-upload>
<duet-button disabled="true" fixed id="submit" variation="primary"> Submit</duet-button>
<duet-button fixed id="cancel"> Cancel</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload");
var submitButton = document.getElementById("submit");

//simple debounce helper to throttle the duetChange events
function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}

submitButton.onclick = function() {
uploadFiles(element.files);
};

function toggleButton(valid) {
console.log("is element valid?", valid);
if (valid) {
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}

// listen to the events from the component
element.addEventListener("duetActionEvent", (e) => {
console.log("got action", e.detail.action);

if (e.detail.action === "edit") {
console.log("edit file details", e.detail);

console.log("use", e.detail.keyName, "and", e.detail.uid, "to control/edit item in the element.files");
}

});

// duetChange event listener
element.addEventListener("duetChange", debounce(function(e) {

console.log("duet-upload-change", element.files);
// even though the event (e) actually carries the files list inside the details object
// you should always take the properties as current state

//you can upload files right after the user has chosen them in the filebrowser (best ux)
//or you can create a scenario where files get uploaded when a button is clicked / form submitted (see startUpload function below)

// duetChange fires on each file that was added, in real life you would probably debounce the script you use to upload with

const filesUploaded = [];
const filesThatNeedsToBeUploaded = [];
element.files.forEach((file) => {
if (file.progress === 0) {
filesThatNeedsToBeUploaded.push(file);
} else {
filesUploaded.push(file);
}
// call you upload function here, use xmlHTTPrequest so that you can support Progress
// update the fileMap via .set(filename,fileItem) onProgress and OnUploadComplete
// dummy uploadFunction and deleteFunction has been added at the bottom of this file (dont use fetch as shown here :) )
// the file will NOT appear in the editable list unless progress is anything other than 0

simulateUploadOnFiles(filesThatNeedsToBeUploaded);
});
toggleButton(element.valid);

console.log("----files that should be uploaded", filesThatNeedsToBeUploaded.length, "----");
console.log("uploading:", filesThatNeedsToBeUploaded);
console.log("-------------");

}, 100));

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e);
deleteFiles(e.detail.data.deletion.uid);
toggleButton(element.valid);
});


// duetDelete event listener
element.addEventListener("duetUpload", function(e) {
console.log("duet-upload-button-pressed", e);
e.preventDefault();
});

// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e);
toggleButton(element.valid);
});

//demo upload functionality
function uploadFiles(selectedFiles) {
if (selectedFiles) {
selectedFiles.forEach(async (item) => {
let data = new FormData();
data.append("file", item);
data.append("metadata", JSON.stringify({ some: "metadata", types: "types" }));

const response = await fetch("/uploads", {
method: "POST",
body: data,
});
console.log(response);
});
}
}

//demo delete functionality
async function deleteFiles(uid) {
if (uid) {
console.log("the following files needs to be deleted", uid);
let data = new FormData();
data.append("uid", uid);
data.append("metadata", JSON.stringify({ some: "metadata", types: "types" }));

const response = await fetch("/uploads", {
method: "DELETE",
body: data,
});
console.log(response);
}
}

//simulation of an upload
function simulateUploadOnFiles(selectedFiles) {

if (selectedFiles) {
selectedFiles.forEach(async (data) => {
console.log("item passed client validation rules", data.valid);
//duet upload will handle all clientside validations for you, (types, sizes etc)
//and display them as erroneous, if you want to handle them yourself, you can do so by modifying the files map
if (data.valid) {
console.log("simulating upload of:", data.item.name);
let progress = 0;
// get the correct map item for the files Map
const filesElement = element.files.get(data.item.name);

// starts a timer with a bit of randomness to simulate an upload
const interval = setInterval(() => {
progress += Math.floor(Math.random() * 11);
if (progress > 100) {
progress = 100;
element.updateValue(data.item.name, "progress", progress);
element.updateValue(data.item.name, "uploaded", true);
clearInterval(interval);
} else {
// update the progress value on the item
element.updateValue(data.item.name, "progress", progress);
}
}, 500);
}
});
}

}
</script>
Open in new window
<duet-layout center margin="none">
<style>
duet-card duet-upload::part(duet-upload-empty-state) {
border: 2px dotted #000000;
padding: 1rem;
}

duet-card duet-upload::part(duet-upload-button-upload) {
border: 2px dotted #000000 !important;
padding: 1rem;
}
</style>
<div slot="main">
<duet-card heading="Notes on example">
<duet-paragraph>This upload demonstrator has some in page source javascript that enables / disables the submit
button if the upload
list contains files with errors (this behaviour is entirely up to you to control, depending on your UX demands)
</duet-paragraph>

</duet-card>

<duet-card heading="Upload Demonstrator">
<form action="www.duetds.com" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*"
external
limit-selection
max-bytes="204800000"
max-files="2"
required
>

<div slot="header">This upload example uses external functions to handle the upload</div>
<div slot="fileheader">This text is in the fileheader slot</div>
<div slot="filefooter">This text is in the filefooter slot</div>
</duet-upload>
<duet-button disabled="true" fixed id="submit" variation="primary"> Submit</duet-button>
<duet-button fixed id="cancel"> Cancel</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload");
var submitButton = document.getElementById("submit");

//simple debounce helper to throttle the duetChange events
function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}

submitButton.onclick = function() {
uploadFiles(element.files);
};

function toggleButton(valid) {
console.log("is element valid?", valid);
if (valid) {
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}

// listen to the events from the component
element.addEventListener("duetActionEvent", (e) => {
console.log("got action", e.detail.action);

if (e.detail.action === "edit") {
console.log("edit file details", e.detail);

console.log("use", e.detail.keyName, "and", e.detail.uid, "to control/edit item in the element.files");
}

});

// duetChange event listener
element.addEventListener("duetChange", debounce(function(e) {

console.log("duet-upload-change", element.files);
// even though the event (e) actually carries the files list inside the details object
// you should always take the properties as current state

//you can upload files right after the user has chosen them in the filebrowser (best ux)
//or you can create a scenario where files get uploaded when a button is clicked / form submitted (see startUpload function below)

// duetChange fires on each file that was added, in real life you would probably debounce the script you use to upload with

const filesUploaded = [];
const filesThatNeedsToBeUploaded = [];
element.files.forEach((file) => {
if (file.progress === 0) {
filesThatNeedsToBeUploaded.push(file);
} else {
filesUploaded.push(file);
}
// call you upload function here, use xmlHTTPrequest so that you can support Progress
// update the fileMap via .set(filename,fileItem) onProgress and OnUploadComplete
// dummy uploadFunction and deleteFunction has been added at the bottom of this file (dont use fetch as shown here :) )
// the file will NOT appear in the editable list unless progress is anything other than 0

simulateUploadOnFiles(filesThatNeedsToBeUploaded);
});
toggleButton(element.valid);

console.log("----files that should be uploaded", filesThatNeedsToBeUploaded.length, "----");
console.log("uploading:", filesThatNeedsToBeUploaded);
console.log("-------------");

}, 100));

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e);
deleteFiles(e.detail.data.deletion.uid);
toggleButton(element.valid);
});


// duetDelete event listener
element.addEventListener("duetUpload", function(e) {
console.log("duet-upload-button-pressed", e);
e.preventDefault();
});

// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e);
toggleButton(element.valid);
});

//demo upload functionality
function uploadFiles(selectedFiles) {
if (selectedFiles) {
selectedFiles.forEach(async (item) => {
let data = new FormData();
data.append("file", item);
data.append("metadata", JSON.stringify({ some: "metadata", types: "types" }));

const response = await fetch("/uploads", {
method: "POST",
body: data,
});
console.log(response);
});
}
}

//demo delete functionality
async function deleteFiles(uid) {
if (uid) {
console.log("the following files needs to be deleted", uid);
let data = new FormData();
data.append("uid", uid);
data.append("metadata", JSON.stringify({ some: "metadata", types: "types" }));

const response = await fetch("/uploads", {
method: "DELETE",
body: data,
});
console.log(response);
}
}

//simulation of an upload
function simulateUploadOnFiles(selectedFiles) {

if (selectedFiles) {
selectedFiles.forEach(async (data) => {
console.log("item passed client validation rules", data.valid);
//duet upload will handle all clientside validations for you, (types, sizes etc)
//and display them as erroneous, if you want to handle them yourself, you can do so by modifying the files map
if (data.valid) {
console.log("simulating upload of:", data.item.name);
let progress = 0;
// get the correct map item for the files Map
const filesElement = element.files.get(data.item.name);

// starts a timer with a bit of randomness to simulate an upload
const interval = setInterval(() => {
progress += Math.floor(Math.random() * 11);
if (progress > 100) {
progress = 100;
element.updateValue(data.item.name, "progress", progress);
element.updateValue(data.item.name, "uploaded", true);
clearInterval(interval);
} else {
// update the progress value on the item
element.updateValue(data.item.name, "progress", progress);
}
}, 500);
}
});
}

}
</script>
Open in new window
<duet-layout center margin="none">
<div slot="main">
<duet-card heading="Notes on example">
<duet-paragraph>
This upload demonstrator has files prefilled from the "serverside" (mocked via javascript)
</duet-paragraph>
<duet-paragraph>
It also has some in page source javascript that enables / disables the submit button if the upload list
contains files with errors (this behaviour is entirely up to you to control, depending on your UX demands)
</duet-paragraph>
<duet-paragraph>
In order to simulate an upload you can remove the erroneous files and click upload (to get the list of files
that needs uploading)
</duet-paragraph>

<duet-paragraph>
You can set the actions and groups through element.actions / element.groups instead of directly on the
element as shown here
</duet-paragraph>
</duet-card>

<duet-card heading="Upload Demonstrator">
<form action="www.duetds.com" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*"
external
hide-table-labels
hide-upload-button
max-bytes="204800000"
max-files="20"
>

<div slot="header">
<duet-select
expand
items='[
{ "label": "No category", "value": "0" },
{ "label": "Category 1", "value": "1" },
{ "label": "Category 2", "value": "2" },
{ "label": "Category 3", "value": "3" },
{ "label": "Category 4", "value": "4" }
]'

label="Kaupunki"
placeholder="Choose a category"
>
</duet-select>

<duet-button
fixed
icon="action-add-circle"
id="externalUploadButton"
size="small"
variation="secondary"
>

Choose Files
</duet-button>
</div>
<div slot="fileheader">
<duet-heading border="solid" color="gray-darker" level="h3" visual-level="h6" weight="semibold">
Uploaded files
</duet-heading>
</div>
<div slot="filefooter">This text is in filefooter slot</div>
</duet-upload>
<duet-button disabled="true" fixed id="submit" variation="primary"> Submit</duet-button>
<duet-button fixed id="cancel"> Cancel</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload");
var selector = document.querySelector("duet-select");
var submitButton = document.getElementById("submit");
var uploadBn = element.querySelector("#externalUploadButton");

//simple debounce helper to throttle the duetChange events
function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}

//add click event handler to the upload button
uploadBn.addEventListener("click", function(event) {
//prevent the default action of the button
event.preventDefault();
const metaData = [
undefined,
{
//badges is a special metaData value, and the only one that has an effect (adds badges to filelist text)
badges: ["Category 1", "other"],
randomData: Math.random(),
}, {
badges: ["Category 2"],
otherData: Math.random(),
}, {
badges: ["Category 3"],
}, {
badges: ["Category 4"],
},
];
// get duet-selector value
const meta = metaData[selector.value];
//call the upload method of the component and add metaData from selector
element.upload(meta);
});

submitButton.onclick = function() {
uploadFiles(element.files);
};

function toggleButton(valid) {
console.log("is element valid?", valid);
if (valid) {
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}

// listen to the events from the component
element.addEventListener("duetActionEvent", (e) => {
console.log("got action", e.detail.action);

if (e.detail.action === "edit") {
console.log("edit file details", e.detail);

console.log("use", e.detail.keyName, "and", e.detail.uid, "to control/edit item in the element.files");
}

});
// duetChange event listener
element.addEventListener("duetChange", debounce(function(e) {

console.log("duet-upload-change", element.files);
// even though the event (e) actually carries the files list inside the details object
// you should always take the properties as current state

//you can upload files right after the user has chosen them in the filebrowser (best ux)
//or you can create a scenario where files get uploaded when a button is clicked / form submitted (see startUpload function below)

// duetChange fires on each file that was added, in real life you would probably debounce the script you use to upload with

const filesUploaded = [];
const filesThatNeedsToBeUploaded = [];
element.files.forEach((file) => {
if (file.progress === 0) {
filesThatNeedsToBeUploaded.push(file);
} else {
filesUploaded.push(file);
}
// call you upload function here, use xmlHTTPrequest so that you can support Progress
// update the fileMap via .set(filename,fileItem) onProgress and OnUploadComplete
// dummy uploadFunction and deleteFunction has been added at the bottom of this file (dont use fetch as shown here :) )
// the file will NOT appear in the editable list unless progress is anything other than 0

simulateUploadOnFiles(filesThatNeedsToBeUploaded);
});
toggleButton(element.valid);

console.log("----files that should be uploaded", filesThatNeedsToBeUploaded.length, "----");
console.log("uploading:", filesThatNeedsToBeUploaded);
console.log("-------------");

}, 100));

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e);
deleteFiles(e.detail.data.deletion.uid);
toggleButton(element.valid);
});


// duetDelete event listener
element.addEventListener("duetUpload", function(e) {
console.log("duet-upload-button-pressed", e);
e.preventDefault();
});

// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e);
toggleButton(element.valid);
});

//demo upload functionality
function uploadFiles(selectedFiles) {
if (selectedFiles) {
selectedFiles.forEach(async (item) => {
let data = new FormData();
data.append("file", item);
data.append("metadata", JSON.stringify({
some: "metadata",
types: "types",
}));

const response = await fetch("/uploads", {
method: "POST",
body: data,
});
console.log(response);
});
}
}

//demo delete functionality
async function deleteFiles(uid) {
if (uid) {
console.log("the following files needs to be deleted", uid);
let data = new FormData();
data.append("uid", uid);
data.append("metadata", JSON.stringify({
some: "metadata",
types: "types",
}));

const response = await fetch("/uploads", {
method: "DELETE",
body: data,
});
console.log(response);
}
}

//simulation of an upload
function simulateUploadOnFiles(selectedFiles) {

if (selectedFiles) {
selectedFiles.forEach(async (data) => {
console.log("item passed client validation rules", data.valid);
//duet upload will handle all clientside validations for you, (types, sizes etc)
//and display them as erroneous, if you want to handle them yourself, you can do so by modifying the files map
if (data.valid) {
console.log("simulating upload of:", data.item.name);
let progress = 0;
// get the correct map item for the files Map
const filesElement = element.files.get(data.item.name);

// starts a timer with a bit of randomness to simulate an upload
const interval = setInterval(() => {
progress += Math.floor(Math.random() * 11);
if (progress > 100) {
progress = 100;
element.updateValue(data.item.name, "progress", progress);
element.updateValue(data.item.name, "uploaded", true);
clearInterval(interval);
} else {
// update the progress value on the item
element.updateValue(data.item.name, "progress", progress);
}
}, 500);
}
});
}

}
</script>
Open in new window
<duet-layout center margin="none">
<div slot="main">
<duet-card heading="Notes on example">
<duet-paragraph>
This upload demonstrator has files prefilled from the "serverside" (mocked via javascript)
</duet-paragraph>
<duet-paragraph>
It also has some in page source javascript that enables / disables the submit button if the upload list
contains files with errors (this behaviour is entirely up to you to control, depending on your UX demands)
</duet-paragraph>
<duet-paragraph>
In order to simulate an upload you can remove the erroneous files and click upload (to get the list of files
that needs uploading)
</duet-paragraph>

<duet-paragraph>
You can set the actions and groups through element.actions / element.groups instead of directly on the
element as shown here
</duet-paragraph>
</duet-card>

<duet-card heading="Upload Demonstrator">
<form action="www.duetds.com" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*"
external
max-bytes="2048000"
max-files="20"
hide-upload-button
show-links
external-upload-button-id="externalUploadButton"
caption-on-bottom
hide-table-labels
show-uploaded-items-header
hide-header
link-click-event
action-button-titles='{
"cancel":{"fi":"[fi] cancel","sv":"[sv] cancel","en":"[en] cancel"},
"delete":{"fi":"[fi] delete","sv":"[sv] delete","en":"[en] delete"}
}'

rename-duplicates
>

<div slot="filefooter">This text is in filefooter slot</div>

<div slot="uploadfooter">
<duet-select
expand
items='[
{ "label": "Category 1", "value": "1" },
{ "label": "Category 2", "value": "2" },
{ "label": "Category 3", "value": "3" },
{ "label": "Category 4", "value": "4" }
]'

label="Kaupunki"
placeholder="Choose a category"
>
</duet-select>
<duet-paragraph role="alert" id="empty-category-error" size="small" margin="none" color="danger">
You must select category first.
</duet-paragraph>
</div>

<div slot="afterfooter">
<duet-alert id="category-alert" variation="warning" icon="messaging-alert">
Bbb
</duet-alert>

<duet-button
fixed
icon="action-add-circle"
id="externalUploadButton"
size="small"
variation="secondary"
>

Choose Files
</duet-button>
</div>

</duet-upload>
<duet-button disabled="true" fixed id="submit" variation="primary"> Submit</duet-button>
<duet-button fixed id="cancel"> Cancel</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload");
var selector = document.querySelector("duet-select");
var submitButton = document.getElementById("submit");
var uploadBn = element.querySelector("#externalUploadButton");
var categoryAlert = document.querySelector("#category-alert");
var emptyCategoryError = document.querySelector("#empty-category-error");
categoryAlert.style.display = "none";
emptyCategoryError.style.display = "none"

//simple debounce helper to throttle the duetChange events
function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}

//add click event handler to the upload button
uploadBn.addEventListener("click", function(event) {
//prevent the default action of the button
event.preventDefault();

if(!selector.value){
emptyCategoryError.style.display = "block";
return;
} else {
emptyCategoryError.style.display = "none";
}

const metaData = [
undefined,
{
//badges is a special metaData value, and the only one that has an effect (adds badges to filelist text)
badges: ["Category 1", "other"],
randomData: Math.random(),
}, {
badges: ["Different label"],
otherData: Math.random(),
}, {
badges: ["Category 3"],
}, {
badges: ["Category 4"],
},
];
// get duet-selector value
const meta = metaData[selector.value];
//call the upload method of the component and add metaData from selector
element.upload(meta);
});

submitButton.onclick = function() {
uploadFiles(element.files);
};

function toggleButton(valid) {
console.log("is element valid?", valid);
if (valid) {
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}

selector.addEventListener("change", (e) => {
const alerts = [
undefined,
undefined,
"Category 2 alert",
undefined,
"Category 4 alert"
]

if(e.target.value) {
emptyCategoryError.style.display = "none"
}

const alert = alerts[+e.target.value]

if(!alert){
categoryAlert.style.display = "none"
} else {
categoryAlert.innerHTML = alert;
categoryAlert.style.display = "block";

}
})

// listen to the events from the component
element.addEventListener("duetActionEvent", (e) => {
console.log("got action", e.detail.action);

if (e.detail.action === "edit") {
console.log("edit file details", e.detail);

console.log("use", e.detail.keyName, "and", e.detail.uid, "to control/edit item in the element.files");
}

});
// duetChange event listener
element.addEventListener("duetChange", debounce(function(e) {

console.log("duet-upload-change", element.files);
// even though the event (e) actually carries the files list inside the details object
// you should always take the properties as current state

//you can upload files right after the user has chosen them in the filebrowser (best ux)
//or you can create a scenario where files get uploaded when a button is clicked / form submitted (see startUpload function below)

// duetChange fires on each file that was added, in real life you would probably debounce the script you use to upload with

const filesUploaded = [];
const filesThatNeedsToBeUploaded = [];
element.files.forEach((file) => {
if (file.progress === 0) {
filesThatNeedsToBeUploaded.push(file);
} else {
filesUploaded.push(file);
}
// call you upload function here, use xmlHTTPrequest so that you can support Progress
// update the fileMap via .set(filename,fileItem) onProgress and OnUploadComplete
// dummy uploadFunction and deleteFunction has been added at the bottom of this file (dont use fetch as shown here :) )
// the file will NOT appear in the editable list unless progress is anything other than 0

simulateUploadOnFiles(filesThatNeedsToBeUploaded);
});
toggleButton(element.valid);

console.log("----files that should be uploaded", filesThatNeedsToBeUploaded.length, "----");
console.log("uploading:", filesThatNeedsToBeUploaded);
console.log("-------------");

}, 100));

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e);
deleteFiles(e.detail.data.deletion.uid);
toggleButton(element.valid);
});


// duetDelete event listener
element.addEventListener("duetUpload", function(e) {
console.log("duet-upload-button-pressed", e);
e.preventDefault();
});

// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e);
toggleButton(element.valid);
});

// add linkClick event listener
element.addEventListener("linkClick", function(e) {
console.log("lick clicked!", e.detail);
});

//demo upload functionality
function uploadFiles(selectedFiles) {
if (selectedFiles) {
selectedFiles.forEach(async (item) => {
let data = new FormData();
data.append("file", item);
data.append("metadata", JSON.stringify({
some: "metadata",
types: "types",
}));

const response = await fetch("/uploads", {
method: "POST",
body: data,
});
console.log(response);
});
}
}

//demo delete functionality
async function deleteFiles(uid) {
if (uid) {
console.log("the following files needs to be deleted", uid);
let data = new FormData();
data.append("uid", uid);
data.append("metadata", JSON.stringify({
some: "metadata",
types: "types",
}));

const response = await fetch("/uploads", {
method: "DELETE",
body: data,
});
console.log(response);
}
}

//simulation of an upload
function simulateUploadOnFiles(selectedFiles) {

if (selectedFiles) {
selectedFiles.forEach(async (data) => {
console.log("item passed client validation rules", data.valid);
//duet upload will handle all clientside validations for you, (types, sizes etc)
//and display them as erroneous, if you want to handle them yourself, you can do so by modifying the files map
if (data.valid) {
console.log("simulating upload of:", data.item.name);
let progress = 0;
// get the correct map item for the files Map
const filesElement = element.files.get(data.item.name);

// starts a timer with a bit of randomness to simulate an upload
const interval = setInterval(() => {
progress += Math.floor(Math.random() * 11);
if (progress > 100) {
progress = 100;
element.updateValue(data.item.name, "progress", progress);
element.updateValue(data.item.name, "uploaded", true);
element.updateValue(data.item.name, "url", "fakeUrl");
clearInterval(interval);
} else {
// update the progress value on the item
element.updateValue(data.item.name, "progress", progress);
}
}, 250);
}
});
}

}
</script>
Open in new window
<duet-layout center margin="none">
<div slot="main">
<duet-card heading="Notes on example">
<duet-paragraph>
This upload demonstrator has files prefilled from the "serverside" (mocked via javascript)
</duet-paragraph>
<duet-paragraph>
It also has some in page source javascript that enables / disables the submit button if the upload list
contains files with errors (this behaviour is entirely up to you to control, depending on your UX demands)
</duet-paragraph>
<duet-paragraph>
In order to simulate an upload you can remove the erroneous files and click upload (to get the list of files
that needs uploading)
</duet-paragraph>

<duet-paragraph>
You can set the actions and groups through element.actions / element.groups instead of directly on the
element as shown here
</duet-paragraph>
</duet-card>

<duet-card heading="Upload Demonstrator">
<form action="www.duetds.com" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*"
external
hide-upload-button
max-bytes="204800000"
max-files="20"
defer-upload
>

<div slot="header">
<duet-select
expand
items='[
{ "label": "No category", "value": "0" },
{ "label": "Category 1", "value": "1" },
{ "label": "Category 2", "value": "2" },
{ "label": "Category 3", "value": "3" },
{ "label": "Category 4", "value": "4" }
]'

label="Kaupunki"
placeholder="Choose a category"
>
</duet-select>

<duet-button
fixed
icon="action-add-circle"
id="externalUploadButton"
size="small"
variation="secondary"
>

Choose Files
</duet-button>
</div>
<div slot="fileheader">
<duet-heading border="solid" color="gray-darker" level="h3" visual-level="h6" weight="semibold">
Demonstrating pending files (click submit to start upload)
</duet-heading>
</div>
<div slot="filefooter">This text is in filefooter slot</div>
</duet-upload>
<duet-button disabled="true" fixed id="submit" variation="primary"> Submit</duet-button>
<duet-button fixed id="cancel"> Cancel</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload");
var selector = document.querySelector("duet-select");
var submitButton = document.getElementById("submit");
var uploadBn = element.querySelector("#externalUploadButton");

//simple debounce helper to throttle the duetChange events
function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}

//add click event handler to the upload button
uploadBn.addEventListener("click", function(event) {
//prevent the default action of the button
event.preventDefault();
const metaData = [
undefined,
{
//badges is a special metaData value, and the only one that has an effect (adds badges to filelist text)
badges: ["Category 1", "other"],
randomData: Math.random(),
}, {
badges: ["Category 2"],
otherData: Math.random(),
}, {
badges: ["Category 3"],
}, {
badges: ["Category 4"],
},
];
// get duet-selector value
const meta = metaData[selector.value];
//call the upload method of the component and add metaData from selector
element.upload(meta);
});

submitButton.onclick = function() {
uploadFiles(element.files);
};

function toggleButton(valid) {
console.log("is element valid?", valid);
if (valid) {
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}

// listen to the events from the component
element.addEventListener("duetActionEvent", (e) => {
console.log("got action", e.detail.action);

if (e.detail.action === "edit") {
console.log("edit file details", e.detail);

console.log("use", e.detail.keyName, "and", e.detail.uid, "to control/edit item in the element.files");
}

});
// duetChange event listener
element.addEventListener("duetChange", debounce(function(e) {

console.log("duet-upload-change", element.files);
// even though the event (e) actually carries the files list inside the details object
// you should always take the properties as current state

//you can upload files right after the user has chosen them in the filebrowser (best ux)
//or you can create a scenario where files get uploaded when a button is clicked / form submitted (see startUpload function below)

// duetChange fires on each file that was added, in real life you would probably debounce the script you use to upload with

const filesUploaded = [];
const filesThatNeedsToBeUploaded = [];
element.files.forEach((file) => {
if (file.progress === 0) {
filesThatNeedsToBeUploaded.push(file);
} else {
filesUploaded.push(file);
}
// call you upload function here, use xmlHTTPrequest so that you can support Progress
// update the fileMap via .set(filename,fileItem) onProgress and OnUploadComplete
// dummy uploadFunction and deleteFunction has been added at the bottom of this file (dont use fetch as shown here :) )
// the file will NOT appear in the editable list unless progress is anything other than 0

//simulateUploadOnFiles(filesThatNeedsToBeUploaded);
simulatePendingFiles(filesThatNeedsToBeUploaded);
});
toggleButton(element.valid);

console.log("----files that should be uploaded", filesThatNeedsToBeUploaded.length, "----");
console.log("uploading:", filesThatNeedsToBeUploaded);
console.log("-------------");

}, 100));

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e);
deleteFiles(e.detail.data.deletion.uid);
toggleButton(element.valid);
});


// duetDelete event listener
element.addEventListener("duetUpload", function(e) {
console.log("duet-upload-button-pressed", e);
e.preventDefault();
});

// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e);
toggleButton(element.valid);
});

//demo upload functionality
function uploadFiles(selectedFiles) {
simulateUploadOnFiles()
if (selectedFiles) {
selectedFiles.forEach(async (item) => {
let data = new FormData();
data.append("file", item);
data.append("metadata", JSON.stringify({
some: "metadata",
types: "types",
}));

const response = await fetch("/uploads", {
method: "POST",
body: data,
});
console.log(response);
});
}
}

//demo delete functionality
async function deleteFiles(uid) {
if (uid) {
console.log("the following files needs to be deleted", uid);
let data = new FormData();
data.append("uid", uid);
data.append("metadata", JSON.stringify({
some: "metadata",
types: "types",
}));

const response = await fetch("/uploads", {
method: "DELETE",
body: data,
});
console.log(response);
}
}

//simulation of an upload
function simulateUploadOnFiles() {
element.files.forEach((file) => {
if (file.progress === 0 && file.pending===true && file.valid===true) {
console.log("simulating upload of:", file.item.name);
element.updateValue(file.item.name, "pending", undefined, true);

let progress = 0;

// starts a timer with a bit of randomness to simulate an upload
const interval = setInterval(() => {
progress += Math.floor(Math.random() * 11);
if (progress > 100) {
progress = 100;
element.updateValue(file.item.name, "progress", progress);
element.updateValue(file.item.name, "uploaded", true);
clearInterval(interval);
} else {
// update the progress value on the item
element.updateValue(file.item.name, "progress", progress);
}

}, 500);
}
}
);

}

//simulation of an upload
function simulatePendingFiles(selectedFiles) {
// there's no need to do this at all, but it has been included here for demo purposes
if (selectedFiles) {
selectedFiles.forEach( (data) => {
console.log("item passed client validation rules", data.valid);
//duet upload will handle all clientside validations for you, (types, sizes etc)
//and display them as erroneous, if you want to handle them yourself, you can do so by modifying the files map
if (data.valid) {
console.log("simulating pending of:", data.item.name);
// update the status of files to be pending
element.updateValue(data.item.name, "pending", true, true);
element.updateValue(data.item.name, "group", "pending", true);
}
});
}

}
</script>
Open in new window
<duet-layout center margin="none">

<div slot="main">
<duet-card heading="Notes on example">
<duet-paragraph>This demonstrator has some in page source javascript that enables / disables the submit button if
the upload list contains files with errors (this behaviour is entirely up to you to control, depending on your
UX demands)
</duet-paragraph>
<duet-paragraph>Here the group labels have been hidden in the duet-editable-table</duet-paragraph>

</duet-card>

<duet-card heading="Upload Demonstrator">
<form action="www.duetds.com" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*"
external
hide-table-labels
limit-selection
max-bytes="204800000"
max-files="4"
required

>

<div slot="header">This upload example uses external functions to handle the upload</div>
<div slot="fileheader">This text is in the fileheader slot</div>
<div slot="filefooter">This text is in the filefooter slot</div>
</duet-upload>
<duet-button disabled="true" fixed id="submit" variation="primary"> Submit</duet-button>
<duet-button fixed id="cancel"> Cancel</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload");
var submitButton = document.getElementById("submit");

//simple debounce helper to throttle the duetChange events
function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}

submitButton.onclick = function() {
uploadFiles(element.files);
};

function toggleButton(valid) {
console.log("is element valid?", valid);
if (valid) {
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}

// listen to the events from the component
element.addEventListener("duetActionEvent", (e) => {
console.log("got action", e.detail.action);

if (e.detail.action === "edit") {
console.log("edit file details", e.detail);

console.log("use", e.detail.keyName, "and", e.detail.uid, "to control/edit item in the element.files");
}

});

// duetChange event listener
element.addEventListener("duetChange", debounce(function(e) {

console.log("duet-upload-change", element.files);
// even though the event (e) actually carries the files list inside the details object
// you should always take the properties as current state

//you can upload files right after the user has chosen them in the filebrowser (best ux)
//or you can create a scenario where files get uploaded when a button is clicked / form submitted (see startUpload function below)

// duetChange fires on each file that was added, in real life you would probably debounce the script you use to upload with

const filesUploaded = [];
const filesThatNeedsToBeUploaded = [];
element.files.forEach((file) => {
if (file.progress === 0) {
filesThatNeedsToBeUploaded.push(file);
} else {
filesUploaded.push(file);
}
// call you upload function here, use xmlHTTPrequest so that you can support Progress
// update the fileMap via .set(filename,fileItem) onProgress and OnUploadComplete
// dummy uploadFunction and deleteFunction has been added at the bottom of this file (dont use fetch as shown here :) )
// the file will NOT appear in the editable list unless progress is anything other than 0

simulateUploadOnFiles(filesThatNeedsToBeUploaded);
});
toggleButton(element.valid);

console.log("----files that should be uploaded", filesThatNeedsToBeUploaded.length, "----");
console.log("uploading:", filesThatNeedsToBeUploaded);
console.log("-------------");

}, 100));

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e);
deleteFiles(e.detail.data.deletion.uid);
toggleButton(element.valid);
});


// duetDelete event listener
element.addEventListener("duetUpload", function(e) {
console.log("duet-upload-button-pressed", e);
e.preventDefault();
});

// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e);
toggleButton(element.valid);
});

//demo upload functionality
function uploadFiles(selectedFiles) {
if (selectedFiles) {
selectedFiles.forEach(async (item) => {
let data = new FormData();
data.append("file", item);
data.append("metadata", JSON.stringify({ some: "metadata", types: "types" }));

const response = await fetch("/uploads", {
method: "POST",
body: data,
});
console.log(response);
});
}
}

//demo delete functionality
async function deleteFiles(uid) {
if (uid) {
console.log("the following files needs to be deleted", uid);
let data = new FormData();
data.append("uid", uid);
data.append("metadata", JSON.stringify({ some: "metadata", types: "types" }));

const response = await fetch("/uploads", {
method: "DELETE",
body: data,
});
console.log(response);
}
}

//simulation of an upload
function simulateUploadOnFiles(selectedFiles) {

if (selectedFiles) {
selectedFiles.forEach(async (data) => {
console.log("item passed client validation rules", data.valid);
//duet upload will handle all clientside validations for you, (types, sizes etc)
//and display them as erroneous, if you want to handle them yourself, you can do so by modifying the files map
if (data.valid) {
console.log("simulating upload of:", data.item.name);
let progress = 0;
// get the correct map item for the files Map
const filesElement = element.files.get(data.item.name);

// starts a timer with a bit of randomness to simulate an upload
const interval = setInterval(() => {
progress += Math.floor(Math.random() * 11);
if (progress > 100) {
progress = 100;
element.updateValue(data.item.name, "progress", progress);
element.updateValue(data.item.name, "uploaded", true);
clearInterval(interval);
} else {
// update the progress value on the item
element.updateValue(data.item.name, "progress", progress);
}

}, 500);
}
});
}

}
</script>
Open in new window
<duet-layout center margin="none">

<div slot="main">
<duet-card heading="Notes on example">
<duet-paragraph>This upload demonstrator has files prefilled from the "serverside" (mocked via javascript)
</duet-paragraph>
<duet-paragraph>It also has some in page source javascript that enables / disables the submit button if the upload
list contains files with errors (this behaviour is entirely up to you to controll, depending on your UX demands)
</duet-paragraph>
<duet-paragraph>In order to simulate an upload you can remove the erroneous files and click upload (to get the
list of files that needs uploading)
</duet-paragraph>

</duet-card>

<duet-card heading="Upload Demonstrator">
<form action="www.duetds.com" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*"
external
limit-selection
max-bytes="204800000"
max-files="4"
required
>

<div slot="header">This upload example uses external functions to handle the upload</div>
<div slot="fileheader">This text is in the fileheader slot</div>
<div slot="filefooter">This text is in the filefooter slot</div>
</duet-upload>
<duet-button disabled="true" fixed id="submit" variation="primary"> Submit</duet-button>
<duet-button fixed id="cancel"> Cancel</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload");
var submitButton = document.getElementById("submit");

//simple debounce helper to throttle the duetChange events
function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}

/*
* Mock functions to simulate a list of items allready uploaded prevously
* the list doesn't really need the actual blob or fileItem inside it to work, but it is shown here for completeness sake
* */


//mock functions to show uploaded file state
function getBrowsedFiles() {
return fileListFromArray([
mockFileCreator({ name: "action-add-circle.svg", type: "image/svg", size: 234 * 1000, lastModified: new Date() }),
mockFileCreator({ name: "action-arrow-down.svg", type: "image/svg", size: 56 * 10000, lastModified: new Date() }),
mockFileCreator({
name: "test-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.svg",
type: "image/svg",
size: 56 * 100,
lastModified: new Date(),
})]);
}

function mockFileCreator({ name, size, type, lastModified, error }) {
let file;
try {
const blob = new Blob(["a".repeat(size)], { type });
blob["lastModifiedDate"] = lastModified;
file = new File([blob], name, { type });
} catch (e) {
file = new File(["foo"], name, { type });
}
return { file, error };
}

function fileListFromArray(files) {
const fileMap = new Map();

files.forEach((obj) => {

const isValid = !obj.error;

const fileItem = {
uid: `${Date.now()}-${Math.random()}`,
item: obj.file,
size: obj.file.size,
valid: isValid,
error: {
type: isValid ? undefined : obj.error,
message: isValid ? undefined : "Random error message",
system_message: isValid ? undefined : "Random system error message",
},
//we're setting progress to 100 in order to communicate to the component that the file has allready been upload
uploaded: true,
progress: 100,
deleted: false,
xhr: false,
url: `https://cdn.duetds.com/api/icons/2.1.12/lib/assets/${obj.file.name}`,
};
fileMap.set(obj.file.name, fileItem);
},
);

return fileMap;

}

// set files
element.files = getBrowsedFiles();

submitButton.onclick = function() {
uploadFiles(element.files);
};

function toggleButton(valid) {
console.log("is element valid?", valid);
if (valid) {
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}

// listen to the events from the component
element.addEventListener("duetActionEvent", (e) => {
console.log("got action", e.detail.action);

if (e.detail.action === "edit") {
console.log("edit file details", e.detail);

console.log("use", e.detail.keyName, "and", e.detail.uid, "to control/edit item in the element.files");
}

});

// duetChange event listener
element.addEventListener("duetChange", debounce(function(e) {

console.log("duet-upload-change", element.files);
// even though the event (e) actually carries the files list inside the details object
// you should always take the properties as current state

//you can upload files right after the user has chosen them in the filebrowser (best ux)
//or you can create a scenario where files get uploaded when a button is clicked / form submitted (see startUpload function below)

// duetChange fires on each file that was added, in real life you would probably debounce the script you use to upload with

const filesUploaded = [];
const filesThatNeedsToBeUploaded = [];
element.files.forEach((file) => {
if (file.progress === 0) {
filesThatNeedsToBeUploaded.push(file);
} else {
filesUploaded.push(file);
}
// call you upload function here, use xmlHTTPrequest so that you can support Progress
// update the fileMap via .set(filename,fileItem) onProgress and OnUploadComplete
// dummy uploadFunction and deleteFunction has been added at the bottom of this file (dont use fetch as shown here :) )
// the file will NOT appear in the editable list unless progress is anything other than 0

simulateUploadOnFiles(filesThatNeedsToBeUploaded);
});
toggleButton(element.valid);

console.log("----files that should be uploaded", filesThatNeedsToBeUploaded.length, "----");
console.log("uploading:", filesThatNeedsToBeUploaded);
console.log("-------------");

}, 100));

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e);
deleteFiles(e.detail.data.deletion.uid);
toggleButton(element.valid);
});


// duetDelete event listener
element.addEventListener("duetUpload", function(e) {
console.log("duet-upload-button-pressed", e);
e.preventDefault();
});

// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e);
toggleButton(element.valid);
});

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e);
toggleButton(element.valid);
});


//demo upload functionality
function uploadFiles(selectedFiles) {
if (selectedFiles) {
selectedFiles.forEach(async (item) => {
let data = new FormData();
data.append("file", item);
data.append("metadata", JSON.stringify({ some: "metadata", types: "types" }));

const response = await fetch("/uploads", {
method: "POST",
body: data,
});
console.log(response);
});
}
}

//demo delete functionality
async function deleteFiles(uid) {
if (uid) {
console.log("the following files needs to be deleted", uid);
let data = new FormData();
data.append("uid", uid);
data.append("metadata", JSON.stringify({ some: "metadata", types: "types" }));

const response = await fetch("/uploads", {
method: "DELETE",
body: data,
});
console.log(response);
}
}

//simulation of an upload
function simulateUploadOnFiles(selectedFiles) {

if (selectedFiles) {
selectedFiles.forEach(async (data) => {
console.log("item passed client validation rules", data.valid);
//duet upload will handle all clientside validations for you, (types, sizes etc)
//and display them as erroneous, if you want to handle them yourself, you can do so by modifying the files map
if (data.valid) {
console.log("simulating upload of:", data.item.name);
let progress = 0;
// get the correct map item for the files Map
const filesElement = element.files.get(data.item.name);

// starts a timer with a bit of randomness to simulate an upload
const interval = setInterval(() => {
progress += Math.floor(Math.random() * 11);
if (progress > 100) {
progress = 100;
element.updateValue(data.item.name, "progress", progress);
element.updateValue(data.item.name, "uploaded", true);
clearInterval(interval);
} else {
// update the progress value on the item
element.updateValue(data.item.name, "progress", progress);
}


}, 500);
}
});
}

}
</script>
Open in new window
<duet-layout center margin="none">
<div slot="main">
<duet-grid alignment="stretch" breakpoint="medium" responsive>
<duet-grid-item margin="none" max-width="66.6%">
<duet-card heading="Uploader">
<form action="#" id="duet-upload-form" method="post" name="duet-upload-form-name">
<duet-upload
allowed-extensions="png,gif,jpg,jpeg,svg,pdf"
allowed-mimetypes="image/*,video/*"
external
limit-selection
max-bytes="204800000"
max-files="4"
required

>

<div slot="header">This upload example uses external functions to handle the upload</div>
<div slot="fileheader">This text is in the fileheader slot</div>
<div slot="filefooter">This text is in the filefooter slot</div>
</duet-upload>
<duet-button disabled="true" fixed id="submit" variation="primary"> Submit</duet-button>
<duet-button fixed id="cancel"> Cancel</duet-button>
</form>
</duet-card>
</duet-grid-item>
<duet-grid-item max-width="33.3%">
<duet-card heading="Methods">
<duet-button
fixed
icon="navigation-finance-transfer"
id="getfiles"
margin="none"
size="small"
variation="plain"
>

getFiles()
</duet-button>
<duet-button
fixed
icon="navigation-finance-transfer"
id="upload"
margin="none"
size="small"
variation="plain"
>

upload()
</duet-button>
<duet-button
fixed
icon="navigation-finance-transfer"
id="updatevalue"
margin="none"
size="small"
variation="plain"
>

updateValue()
</duet-button>
<duet-button
fixed
icon="navigation-finance-transfer"
id="refresh"
margin="none"
size="small"
variation="plain"
>

refresh()
</duet-button>
<duet-button
fixed
icon="navigation-finance-transfer"
id="setfocus"
margin="none"
size="small"
variation="plain"
>

setFocus()
</duet-button>
<duet-textarea expand id="status" label="Method Results"></duet-textarea>
</duet-card>
</duet-grid-item>
</duet-grid>
</div>
</duet-layout>
<script>

// Select the above duet-upload component
var element = document.querySelector("duet-upload")
// methods buttons
var getfilesBn = document.getElementById("getfiles")
var updatevalueBn = document.getElementById("updatevalue")
var uploadBn = document.getElementById("upload")
var refreshBn = document.getElementById("refresh")
var setfocusBn = document.getElementById("setfocus")
var textArea = document.querySelector("duet-textarea")


// Listen for clicks in the button
getfilesBn.addEventListener("click", async function(e) {
const files = await element.getFiles()

textArea.value = `getFiles:
---valid---
${JSON.stringify(files.valid, null, 2)}

---invalid---
${JSON.stringify(files.invalid, null, 2)}
`

})

uploadBn.addEventListener("click", function(e) {
console.log("clicked")
element.upload({
badges: ["Category 2"],
otherData: Math.random(),
evenMoreData: [{ some: "thing", other: "thing" }]
})
})

updatevalueBn.addEventListener("click", function(e) {
element.updateValue("action-add-circle.svg", "valid", false)
getfilesBn.click()
})
refreshBn.addEventListener("click", async function(e) {
element.files.clear()
await element.refresh()
getfilesBn.click()
})
setfocusBn.addEventListener("click", async function(e) {
document.body.focus()
textArea.value = document.activeElement.localName
setTimeout(async () => {
await element.setFocus()
textArea.value = document.activeElement.localName
}, 1000)

})
</script>
<script>
// Select the above duet-upload component
var element = document.querySelector("duet-upload")
var submitButton = document.getElementById("submit")

//simple debounce helper to throttle the duetChange events
function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}

/*
* Mock functions to simulate a list of items allready uploaded prevously
* the list doesn't really need the actual blob or fileItem inside it to work, but it is shown here for completeness sake
* */


//mock functions to show uploaded file state
function getBrowsedFiles() {
return fileListFromArray([
mockFileCreator({ name: "action-add-circle.svg", type: "image/svg", size: 234 * 1000, lastModified: new Date() }),
mockFileCreator({ name: "action-arrow-down.svg", type: "image/svg", size: 56 * 10000, lastModified: new Date() }),
mockFileCreator({
name: "action-car-transfer.svg",
type: "image/svg",
size: 56 * 100,
lastModified: new Date()
})])
}

function mockFileCreator({ name, size, type, lastModified, error }) {
let file
try {
const blob = new Blob(["a".repeat(size)], { type })
blob["lastModifiedDate"] = lastModified
file = new File([blob], name, { type })
} catch (e) {
file = new File(["foo"], name, { type })
}
return { file, error }
}

function fileListFromArray(files) {
const fileMap = new Map();

files.forEach((obj) => {

const isValid = !obj.error;

const fileItem = {
uid: `${Date.now()}-${Math.random()}`,
item: obj.file,
size: obj.file.size,
valid: isValid,
error: {
type: isValid ? undefined : obj.error,
message: isValid ? undefined : "Random error message",
system_message: isValid ? undefined : "Random system error message",
},
//we're setting progress to 100 in order to communicate to the component that the file has allready been upload
uploaded: true,
progress: 100,
deleted: false,
xhr: false,
url: `https://cdn.duetds.com/api/icons/2.1.12/lib/assets/${obj.file.name}`,
}
fileMap.set(obj.file.name, fileItem);
}
)

return fileMap

}

// set files
element.files = getBrowsedFiles()

submitButton.onclick = function() {
uploadFiles(element.files)
}

function toggleButton(valid) {
console.log("is element valid?", valid)
submitButton.disabled = !valid;
}

// listen to the events from the component
element.addEventListener("duetActionEvent", (e) => {
console.log("got action", e.detail.action)

if (e.detail.action === "edit") {
console.log("edit file details", e.detail)

console.log("use", e.detail.keyName, "and", e.detail.uid, "to control/edit item in the element.files")
}

})

// duetChange event listener
element.addEventListener("duetChange", debounce(function(e) {

console.log("duet-upload-change", element.files)
// even though the event (e) actually carries the files list inside the details object
// you should always take the properties as current state

//you can upload files right after the user has chosen them in the filebrowser (best ux)
//or you can create a scenario where files get uploaded when a button is clicked / form submitted (see startUpload function below)

// duetChange fires on each file that was added, in real life you would probably debounce the script you use to upload with

const filesUploaded = []
const filesThatNeedsToBeUploaded = []
element.files.forEach((file) => {
if (file.progress === 0) {
filesThatNeedsToBeUploaded.push(file)
} else {
filesUploaded.push(file)
}
// call you upload function here, use xmlHTTPrequest so that you can support Progress
// update the fileMap via .set(filename,fileItem) onProgress and OnUploadComplete
// dummy uploadFunction and deleteFunction has been added at the bottom of this file (dont use fetch as shown here :) )
// the file will NOT appear in the editable list unless progress is anything other than 0

simulateUploadOnFiles(filesThatNeedsToBeUploaded)
})
toggleButton(element.valid)

console.log("----files that should be uploaded", filesThatNeedsToBeUploaded.length, "----")
console.log("uploading:", filesThatNeedsToBeUploaded);
console.log("-------------")

}, 100))

// duetDelete event listener
element.addEventListener("duetDelete", function(e) {
console.log("duet-upload-delete", e)
deleteFiles(e.detail.data.deletion.uid)
toggleButton(element.valid)
})


// duetDelete event listener
element.addEventListener("duetUpload", function(e) {
console.log("duet-upload-button-pressed", e)
e.preventDefault()
})

// add duetDone event listener
element.addEventListener("duetDone", function(e) {
console.log("duet-upload-done", e)
toggleButton(element.valid)
})

// add duetCancel event listener
element.addEventListener("duetCancel", function(e) {
console.log("duet-upload-cancel", e)
toggleButton(element.valid)
})

// add duetCancel event listener
element.addEventListener("duetState", function(e) {
console.log("native change", e)
toggleButton(element.valid)
})


//demo upload functionality
function uploadFiles(selectedFiles) {
if (selectedFiles) {
selectedFiles.forEach(async (item) => {
let data = new FormData();
data.append("file", item);
data.append("metadata", JSON.stringify({ some: "metadata", types: "types" }));

const response = await fetch("/uploads", {
method: "POST",
body: data,
});
console.log(response)
})
}
}

//demo delete functionality
async function deleteFiles(uid) {
if (uid) {
console.log("the following files needs to be deleted", uid)
let data = new FormData();
data.append("uid", uid);
data.append("metadata", JSON.stringify({ some: "metadata", types: "types" }));

const response = await fetch("/uploads", {
method: "DELETE",
body: data,
});
console.log(response)
}
}

//simulation of an upload
function simulateUploadOnFiles(selectedFiles) {

if (selectedFiles) {
selectedFiles.forEach(async (data) => {
console.log("item passed client validation rules", data.valid)
//duet upload will handle all clientside validations for you, (types, sizes etc)
//and display them as erroneous, if you want to handle them yourself, you can do so by modifying the files map
if (data.valid) {
console.log("simulating upload of:", data.item.name)
let progress = 0;
// get the correct map item for the files Map
const filesElement = element.files.get(data.item.name);

// starts a timer with a bit of randomness to simulate an upload
const interval = setInterval(() => {
progress += Math.floor(Math.random() * 11);
if (progress > 100) {
progress = 100;
element.updateValue(data.item.name, "progress", progress)
element.updateValue(data.item.name, "uploaded", true)
clearInterval(interval);
} else {
// update the progress value on the item
element.updateValue(data.item.name, "progress", progress)
}

}, 500);
}
})
}

}
</script>

Properties #

Property Attribute Description Type Default
accessibleActiveDescendant accessible-active-descendant Indicates the id of a related component’s visually focused element. string undefined
accessibleButtonLabel accessible-button-label accessible aria-Label of button string undefined
accessibleControls accessible-controls Use this prop to add an aria-controls attribute. Use the attribute to indicate the id of a component controlled by this component. string undefined
accessibleDescribedBy accessible-described-by Indicates the id of a component that describes the upload component. string undefined
accessibleOwns accessible-owns Indicates the id of a component owned by the input. string undefined
actionButtonTitles action-button-titles Map action name to DuetLangObject or boolean. Sets upload item action buttons titles (show as DOM tooltip when hovering). string | { [actionName: string]: boolean | DuetLangObject; } { cancel: false, delete: false, }
actions -- Default actions added to the internally used duet-editable-table { icon: string; color: string; background: string; size: DuetActionButtonIconSize; name: string; map?: string[]; label?: DuetLangObject; }[] [ { icon: "action-delete", color: "color-danger", size: "x-small", background: "gray-lightest", name: "delete", map: ["success", "failure"], label: { fi: "Poista tiedosto", en: "Delete the file", sv: "Ta bort filen", }, }, { icon: "navigation-close", color: "primary", size: "x-small", background: "gray-lightest", name: "cancel", map: ["inprogress", "pending"], label: { fi: "Keskeytä lähetys", en: "Cancel the upload", sv: "Stop överföringen", }, }, ]
alignment alignment Key used to set vertical alignment of action buttons string "middle"
allowedExtensions allowed-extensions A string of commaseperated file type values that are allowed string "all"
allowedMimetypes allowed-mimetypes A string of commaseperated mime type values that are allowed string ""
buttonLabel button-label Label of button string getLocaleString(this.buttonLabelDefaults)
buttonLabelDefaults button-label-default Property to change button label defaults on the component. DuetLangObject | string { fi: "Lisää liite", sv: "Lägg till en bilaga", en: "Add an attachment", }
caption caption Caption (underneath label) that can be set as a way of adding extra information string undefined
captionOnBottom caption-on-bottom If true the input caption will be placed below file list and footer boolean false
deferUpload defer-upload If defer-upload is true, duet-upload will not (as recommended) instantly upload files but await a call to uploadPending() boolean false
description description Description for the upload component. string getLocaleString(this.descriptionDefaults)
descriptionDefaults description-default Property to change descriptionDefaults defaults on the component. DuetLangObject | string { en: "You may attach the following filetypes: {filetypes} - as well as the most common video files. You can upload {maxbytestotal} of files at a time, and add up to {maxfiles} attachments at a time each no larger than {maxbytes}.", sv: "Du kan bifoga följande filtyper: {filetypes} - samt de vanligaste videofilerna. Du kan ladda upp {maxbytestotal} av filer åt gången, och lägga till upp till {maxfiles} bilagor åt gången varje inte större än {maxbytes}.", fi: "Voit liittää seuraavat tiedostotyypit: {filetypes} - sekä yleisimmät videotiedostot. Voit lähettää {maxbytestotal} tiedostoa kerrallaan, ja lisätä enintään {maxfiles} liitettä kerrallaan, jokainen enintään {maxbytes} kokoisena.", }
disabled disabled Makes the input component disabled. This prevents users from being able to interact with the upload component, and conveys its inactive state to assistive technologies. boolean false
emitEvent link-click-event If link-click-event is set to true then upload component will emit an event on uploaded file link click (check linkClick event). boolean false
error error Display the input in error state along with an error message. string ""
errorCodes -- Default errorcodes used by the component, modifiable via javascript DuetUploadErrorCode[] errorCodes
external external If external is set to true, the upload component will not actually upload the files, but only keep states it will be up to you to handle the upload and return progress information to the upload-component boolean false
externalUploadButtonId external-upload-button-id Id of external uploadButton of the input used for setting accessibility attributes. string undefined
fileListEmpty file-list-empty Label for the filelist's empty state. string getLocaleString(this.fileListEmptyDefaults)
fileListEmptyDefaults list-empty-default Defaults for the filelist's empty state. DuetLangObject | string { fi: "Ei vielä lisättyjä tiedostoja.", sv: "Inga filer har lagts till ännu.", en: "No files added yet.", }
files -- Map of string that contain list of uploaded files. StringMap new Map()
groups -- Array of group names that you want the editable table to use to display files DuetUploadTableGroupName[] [ { id: this.DefaultGroups.success, label: { fi: "Valmiit tiedostot", sv: "Files success", en: "Files success", }, }, { id: this.DefaultGroups.failure, label: { fi: "Tiedostot, joissa on virheitä", sv: "Filer med fel", en: "Files with errors", }, }, { id: this.DefaultGroups.inprogress, label: { fi: "Kesken olevat tiedostot", sv: "Filer inprogress", en: "Files inprogress", }, }, { id: this.DefaultGroups.pending, label: { en: "Files to upload", sv: "Filer att ladda", fi: "Ladattavat tiedostot", }, }, ]
headerHeadingLevel header-heading-level Heading level for the label in the legend element. This is only used to give screen readers better logical structure. This does not affect visual appearance. "h1" | "h2" | "h3" | "h4" | "h5" | "h6" "h3"
hideButton hide-upload-button If set the upload component will not display an upload button, you will have to create one yourself and call the exposed method startUpload to open the upload dialog boolean false
hideCancelButton hide-cancel-button Use hideCancelButton to hide cancel button for pending and in progress uploads boolean false
hideGroups hide-table-labels Visually hides the groups labels in the editable table list used to display the list of files boolean false
hideHeader hide-header Use hide-header to hide upload component header and caption (if caption is not on bottom) boolean false
identifier identifier Adds a unique identifier for the upload component. string undefined
label label Label for the input. string getLocaleString(this.labelDefaults)
labelDefaults label-default Property to change labelDefaults defaults on the component. normally you would handle these strings on an application level and override label when needed DuetLangObject | string { fi: "Lisää liite", sv: "Lägg till en bilaga", en: "Add attachments", }
limitSelection limit-selection Use limitSelection to enforce the value in allowedExtension & allowedMimetypes when selecting files, by default this is off, setting this to true will limit the users choices to what has been explicitly set boolean false
margin margin Controls the margin of the component. "auto" | "none" "auto"
maxBytes max-bytes Use maxBytes to specify the maximum size in Bytes of a file that can be uploaded. number 200000000
maxBytesTotal max-bytes-total Use maxBytesTotal to specify the maximum size in Bytes of All files combined that can be uploaded. number undefined
maxFiles max-files Use maxFiles to specify the maximum amount of files that can be uploaded number 99
middleware -- If internal upload method is used, and this has been set to a function - it will be called with the XHR options before the reqeust is sent, return an updated XHR options object in order to manipulate the request (XHRMiddlewareOptions: any) => XHRMiddlewareOptions null
multiple multiple Use multiple to allow the user to select multiple files when uploading boolean true
name name Name of the upload component. string undefined
renameDuplicates rename-duplicates Use rename-duplicates to auto rename files with the same name instead of replacing them. boolean false
required required Set whether the input is required or not. Please note that this is necessary for accessible inputs when the user is required to fill them. When using this property you need to also set “novalidate” attribute to your form element to prevent browser from displaying its own validation errors. boolean false
showLinks show-links If enabled the editable-table will display links on successfully uploaded items, this requires the server can respond with link URIs in the correct format and that the files are accessible to the user boolean false
showUploadedItemsHeader show-uploaded-items-header Use show-uploaded-items-header to show single, generic, header for uploaded items boolean false
statusLabelDefaults status-label-default Property to change the aria upload progress text read aloud by screenreaders DuetLangObject | string { fi: { inProgress: "Lähetetään {filesUploaded}, yhteensä lähetettävänä {filesTotal}.", inProgressWithErrors: "Lähetetään {filesInProgress}, lähetetty {filesUploaded}, yhteensä lähetettävänä {filesTotal}, {filesWithErrors} epäonnistui", done: "Lähetys valmis, {filesTotal} lisätty onnistuneesti", doneWithErrors: "Lähetys valmis, {filesUploaded} lisätty onnistuneesti, {filesWithErrors} epäonnistui", files: "tiedostoa", file: "tiedosto", }, sv: { inProgress: "Laddar upp {filesUploaded} av {filesTotal}", inProgressWithErrors: "Laddar upp {filesInProgress}, {filesUploaded} uppladdad av {filesTotal}, {filesWithErrors} misslyckades", done: "Uppladdningen slutförd, {filesTotal} har lagts till", doneWithErrors: "Uppladdningen slutförd, {filesUploaded} har lagts till, {filesWithErrors} misslyckades", files: "filer", file: "fil", }, en: { inProgress: "Uploading {filesUploaded} of {filesTotal}", inProgressWithErrors: "Uploading {filesInProgress}, {filesUploaded} uploaded of {filesTotal}, {filesWithErrors} failed", done: "Upload completed, {filesTotal} added successfully", doneWithErrors: "Upload completed, {filesUploaded} added successfully, {filesWithErrors} failed.", files: "files", file: "file", }, }
statusMessageLabel status-message-label Strings used for the status aria-label string | { inProgress: string; inProgressWithErrors: string; done: string; doneWithErrors: string; file: string; files: string; } getLocaleString( this.statusLabelDefaults )
theme theme Theme of the input. "" | "default" | "turva" ""
uploadedItemsHeaderLabel -- Property to change single uploaded items header label DuetLangObject { fi: "Ladattu tiedosto", sv: "Lägg till en bilaga", en: "Uploaded file", }
uri uri Endpoint URI that is capable of receiving the files string undefined
valid valid Property to read if the internally used editable-table contains errors or not boolean !this.required
value value Value of the input. string undefined

Events #

Event Description Type
duetBlur Emitted when the input loses focus. CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>
duetCancel Emitted when a user clicks cancel on an upload in progress CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>
duetChange Emitted when the value has changed. CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>
duetDelete Emitted when a user clicks delete to delete an uploaded file, or a file entry with error CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>
duetDone Emitted when the current upload batch finishes CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>
duetFocus Emitted when the input has focus. CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>
duetProgress Emitted when the file progress is updated. CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>
duetReady Emitted when the component is finished initializing CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>
duetState Emitted when the current validation state changes internally CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>
duetUpload Emitted when the user clicks the upload button CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>
linkClick Emitted when the user clicks the uploaded file link and link-click-event prop is set to true CustomEvent<{ originalEvent?: Event; data?: Record<string, any>; metaData?: Record<string, any>; component: "duet-upload"; }>

Methods #

focusActionButton(uid: string) => Promise<void> #

focusActionButton.

Parameters #

Name Type Description
uid string

Returns #

Type: Promise<void>

getFiles() => Promise<false | { valid: any[]; invalid: any[]; }> #

Get list of files, divided in errors and valid sections

Returns #

Type: Promise<false | { valid: any[]; invalid: any[]; }>

refresh() => Promise<void> #

Method for forcing a render of the upload list, element.files can be changed externally
But it will only rerender on a new Map or a top Level change - this can be used to update
the tabular data if the automatic re-render is no sufficient

Returns #

Type: Promise<void>

setFocus(options?: FocusOptions) => Promise<void> #

Sets focus on the specified duet-input. Use this method instead of the global
input.focus().

Parameters #

Name Type Description
options FocusOptions

Returns #

Type: Promise<void>

updateValue(item: string, key: string, value: any) => Promise<void> #

Convenience method for updating the value of a key:value inside an item in the files attribute

Parameters #

Name Type Description
item string
key string
value any

Returns #

Type: Promise<void>

upload(metaData?: any | undefined) => Promise<void> #

Method for invoking the upload sequence

Parameters #

Name Type Description
metaData any

Returns #

Type: Promise<void>

uploadPending() => Promise<void> #

Method for uploading pending files

Returns #

Type: Promise<void>

Slots #

Slot Description
"afterfooter" named slot - to place content below "footer" / caption (if caption is on bottom)
"filefooter" named slot - to place content below "filelist" (only displayed when "filelist" contains items)
"fileheader" named slot - to place content above "filelist" (only displayed when "filelist" contains items)
"header" named slot - to place content after description / caption (if caption is on top)
"uploadfooter" named slot - to place content below filefooter

Shadow Parts #

Part Description
"${this.identifier}-editable-table : duet-upload-editable-table" named part - can be used to style the editable-table
"${this.identifier}-empty-state : duet-upload-empty-state" named part - can be used to style the empty notification area or hide it completely
"${this.identifier}-error-notification : duet-upload-error-notification" named part - can be used to style any error notifications occurring internally

Usage #

This section includes guidelines for designers and developers about the usage of this component in different contexts.

Duet-Upload component #

The upload component is a flexible and easy to use encapsulation of duet-editable-table, that enables you to easily create complex upload scenarios using a simple element.

When to use #

  • Use this when you want to allow users to upload files to a server,
    for enhancing user perception of UX on this component the Internally uploading variant of this component will upload files the user has selected immediately, and give them the possibility to remove them if desired.
    The uploader will handle most upload restrictions locally in user-land, which means that the server mostly only needs to provide an endpoint to upload to.

Notes on usage #

  • In internal upload mode all the capabilities of the upload component are automated for you, you will just need to provide an api endpoint thaat adheres to the contract mentioned below
  • The internal upload versions are a lot simpler to use, they do however requires a server (as noted below) that can receive uploads via POST requests and DELETE requests on the same URL, a sample server can be examined here https://github.com/duetds/duetds-upload-server and it has been deployed here for convenience: https://duetds-upload-server.azurewebsites.net/
  • In external upload mode, the component will only provide you with an easy way to maintain state of you upload and files, it is up to you to both upload the files and update the state of the component using the exposed methods and attributes
  • The upload component will always allow a user to re-upload a file, even if they uploaded it before - this is to ensure that we are never in doubt about which file is the correct one, and if the file got uploaded or not. (by design)

Variations #

This section describes the different component variations, their purpose, and when to use each variation.

Name Purpose
external Component will not upload but only show state
internal (default state) Components handles all user interactions and upload

Accessibility #

This component has been validated to meet the WCAG 2.1 AA accessibility guidelines. You can find additional information regarding accessibility of this component below.

Notes on internal file representation #

The components exposes a files variable that can be queried and written to as you will via javascript, it is a Javascript Map object, with the following structure:

{
  uid: Uniquely identifies the file for instance: `${Date.now()}-${Math.random()}`
  item: HTMLformFileObject,
  size: The size of the file in question,
  valid: Boolean that can be set / queried to indicate if the file is valid or not
         - this is set by the duet-upload component based on the clientside validations
         (governed by attributes such as max-size, max-files, etc)
  error: {
    type: Undefined or errorObject
    message: Undefined or user visible string such as "Random error message"
    system_message: Undefined or deeveloper friendly message such as
                    "Object Error, some parameters missing in upload format"
  }
  uploaded: Boolean that can be set /
            queried to indicate if the file has been uploaded or not
  progress: Int 0-100 that is used to indicate the progress of the upload
            a uploaded: true file must allways have progress: 100
             progress: 0 is used for files that have not started uploading yet, but are queued
  deleted: Can be used to query the map to find deleted files
           this is useful for cleaning up the map after uploads
           have been completed or files have been removed from
           the server but UI is not yet updated
  xhr: XmlHttpRequest object that is exposed in its raw form for
       additional javascript hooks to be applied,
  url: If a file was uploaded and the server returns a url parameter
       as part of the reponse, then that will be stored here
  }

Information regarding upload targets #

When using the internal variation the upload target (defined via the uri attribute) has to provide the following interfaces on the same URL

Accepts POST & DELETE requests on the same url

Request & Reponse api #

POST: #

request: ( FormData )

Name Type Description
file binary Contains the actual binary data of the file uploaded
uid string Guaranteed unique string that can be used for identification
name string Userland name of the file being uploaded
metadata json Collection of useful metadata, currently contains: {uid, url, size, meta}

response:

Name Type Description
key string Unique key from the server identifying file (not used for anything internally, but is passed around in case you need it in your code
url string uri where the file can be accessed by the user - used to create links to uploaded files

DELETE: #

The delete request sends an empty DELETE to the same URI as used in the POST, it adds the following headers to the request
request: ( Empty )

Name Type Description
x-fileuid string Unique string that can be used for identification (same sent in POST request as uid)
x-filename string Guaranteed unique string that can be used for identification

response: ( 200 )

Empty body, or "ok" in body

HTTP response codes & mappings (i18n translated) #

Code Type Description
200-299 OK Everything went well, file was uploaded
300-999 Failure Uploading failed, display error message

HTTP Error codes the component understands in details

Code Type Description i18n?
>400 Failure Show default error message unknown error Yes
401 Failure Not Authenticated Yes
403 Failure No Access Yes
413 Failure Payload too large Yes
415 Failure Unsupported media type Yes
429 Failure Too many request from same address Yes
500 Failure Internal Server Error Yes
500< Failure Show default error message unknown error Yes

Internally used error codes #

These error codes are used when any of the userland restrictions causes a failure (allowed-mimetypes, allowed-extensions, max-files, max-bytes)

Code Type Description i18n?
duet-upload-100 Failure File extension not allowed Yes
duet-upload-001 Failure File transfer failed Yes
duet-upload-101 Failure File mimetype not allowed Yes
duet-upload-201 Failure File is too large Yes
duet-upload-202 Failure The combined size of all files is too large" Yes
duet-upload-301 Failure The maximum file limit has been reached" Yes

Integration

For integration, event and theming guidelines, please see Using Components. This documentation explains how to implement and use Duet’s components across different technologies like Angular, React or Vanilla JavaScript.

Integration guidelines


Tutorials

Follow these practical tutorials to learn how to build simple page layouts using Duet’s CSS Framework, Web Components and other features:

Tutorials

Building Layouts

Tutorials

Using CLI Tools

Tutorials

Creating Custom Patterns

Tutorials

Server Side Rendering

Tutorials

Sharing Prototypes

Tutorials

Usage With Markdown


Troubleshooting

If you experience any issues while using a component, please head over to the Support page for more guidelines and help.