Introduction
This component consists in an image uploader.
Let's start by creating the structure of our web component. Create a file uploaderLab.js:
class uploaderLab extends HTMLElement {
//Here will be all the functionality.
}
window.customElements.define('uploader-lab', uploaderLab);
Step 1: Add a constructor and variables
class uploaderLab extends HTMLElement {
constructor () {
super();
this.data = [];
this.fileName = [];
this.deletedSrc = [];
this.totalLoaded = 0;
this.allowedExtension = ['image/jpeg','image/png'];
this.thumb_height = '100';
this.resize_width = '600';
this.limitImage = 5;
}
}
window.customElements.define('uploader-lab', uploaderLab);
In this case we create eight variables:
- data: Array of loaded images base64-encoded Data URI.
- fileName: Array of loaded images names.
- deletedSrc: Array of deleted images names.
- totalLoaded: Total amount of loaded images.
- allowedExtension: Array of allowed extensions.
- thumb_height: The height of thumbs (by default: 100).
- resize_width: The resized width of sent images (by default: 600).
- limitImage: Max amount of images allowed (by default: 5).
The deletedSrc array is used for edit purposes.
Step 2: Adding getters
class uploaderLab extends HTMLElement {
//Step 1
get btnSelectText() {
return this.hasAttribute('btnSelectText');
}
get btnDeleteText() {
return this.hasAttribute('btnDeleteText');
}
get thumbHeight() {
return this.hasAttribute('thumbHeight');
}
}
window.customElements.define('uploader-lab', uploaderLab);
In this case we create three attributes::
- btnSelectText: The selection button text.
- btnDeleteText: The delete button text.
- thumbHeight:The thumbnail height.
Step 3: Add a function to process the selected or dropped files
class uploaderLab extends HTMLElement {
//Step 1
//Step 2
process(e,drop){
//check if it is a selection or dropping
if(drop == true)
e.target.files = e.dataTransfer.files;
//ckeck if there is invalid files
var invalid = false;
//is a valid extension?
for (var b = 0; b < e.target.files.length; b++) {
var el = this.allowedExtension.indexOf(e.target.files[b].type);
if(el == -1){
invalid = true;
break
}
}
//it is valid
if(invalid == false){
var width = this.resize_width;
//the total lenght of the array if we add all files from input
var realLength = this.fileName.length * 1 + e.target.files.length;
var forLength = 0;
if(realLength <= this.limitImage){
//if it is valid we set the total with all files
forLength = e.target.files.length;
} else {
//if it is not valid we set the total with -1 (add none)
forLength = -1;
}
//if it is the firt time adding
if(this.fileName.length == 0){
//if it's greater than the limit we take only the allowed length
if(realLength > this.limitImage)
forLength = e.target.files.length - (e.target.files.length - this.limitImage);
}
//reading the files
for (var i = 0; i < forLength; i++) {
var reader = new FileReader();
reader.readAsDataURL(e.target.files[i]);
reader.name = e.target.files[i].name;
reader.size = e.target.files[i].size;
reader.totalToLoad = forLength;
reader.cmp = this;
reader.onload = function(event) {
event.target.cmp.shadowRoot.querySelector('.loading').style.display = 'block';
//creating a thumbnail
var imgMin = document.createElement('img');
imgMin.style.width = 'auto';
imgMin.style.height = event.target.cmp.thumb_height + 'px';
imgMin.src = event.target.result;
imgMin.totalToLoad = event.target.totalToLoad;
imgMin.cmp = event.target.cmp;
imgMin.onload = function(e){
var w = e.target.cmp.shadowRoot.querySelector('.total').innerHTML.split('/');
e.target.cmp.shadowRoot.querySelector('.total').innerHTML = w[0] * 1 + 1 + '/' + e.target.cmp.limitImage;
e.target.cmp.totalLoaded++;
if(e.target.cmp.totalLoaded == e.target.totalToLoad){
e.target.cmp.shadowRoot.querySelector('.loading').style.display = 'none';
e.target.cmp.totalLoaded = 0;
}
};
//the close button
var titMin = document.createElement('span');
titMin.setAttribute('class','closer');
titMin.innerHTML = '×';
titMin.cmp = event.target.cmp;
titMin.onclick = function(e){
var q = e.target.parentNode.parentNode.querySelectorAll('.closer');
for (var x = 0; x < q.length; x++) {
if(q[x] === e.target){
var w = e.target.cmp.shadowRoot.querySelector('.total').innerHTML.split('/');
e.target.cmp.shadowRoot.querySelector('.total').innerHTML = w[0] * 1 - 1 + '/' + e.target.cmp.limitImage;
e.target.parentNode.parentNode.removeChild(e.target.parentNode);
e.target.cmp.data.splice(x, 1);
e.target.cmp.fileName.splice(x, 1);
}
}
};
//thumbnail container
var imgCont = document.createElement('span');
imgCont.setAttribute('class','card');
imgCont.style.height = event.target.cmp.thumb_height * 1 + 10 + 'px';
imgCont.style.marginRight = '5px';
imgCont.style.cssFloat = 'left';
imgCont.appendChild(imgMin);
imgCont.appendChild(titMin);
event.target.cmp.shadowRoot.querySelector('#cont').appendChild(imgCont);
//creating image and then a canvas
var img = new Image();
img.src = event.target.result;
img.name = event.target.name;
img.size = event.target.size;
img.cmp = event.target.cmp;
img.onload = function(el) {
var elem = document.createElement('canvas');
var scaleFactor = width / el.target.width;
elem.width = width;
elem.height = el.target.height * scaleFactor;
var ctx = elem.getContext('2d');
ctx.drawImage(el.target, 0, 0, elem.width, elem.height);
var t = ctx.canvas.toDataURL('image/png', 1);
el.target.cmp.data.push(t);
el.target.cmp.fileName.push(el.target.name);
}
};
}
}
}
}
window.customElements.define('uploader-lab', uploaderLab);
Step 4: Add a function to load images inside the component (for editing purposes)
class uploaderLab extends HTMLElement {
//Step 1
//Step 2
//Step 3
loadImages(){
//clearall before load
this.clearAll();
var e = this.querySelectorAll('img');
//setting a resized width
var width = this.resize_width;
//the total lenght of the array if we add all files
var realLength = this.fileName.length * 1 + e.length;
var forLength = 0;
if(realLength <= this.limitImage){
//if it is valid we set the total with all files
forLength = e.length;
} else {
//if it is not valid we set the total with -1 (add none)
forLength = -1;
}
//if it is the firt time adding
if(this.fileName.length == 0){
//if it's greater than the limit we take only the allowed length
if(realLength > this.limitImage)
forLength = e.length - (e.length - this.limitImage);
}
//the files
for(var i = 0; i < forLength; i++) {
this.shadowRoot.querySelector('.loading').style.display = 'block';
//creating the thumbnail
var imgMin = document.createElement('img');
imgMin.style.width = 'auto';
imgMin.style.height = this.thumb_height + 'px';
imgMin.src = e[i].src;
imgMin.totalToLoad = forLength;
imgMin.cmp = this;
imgMin.onload = function(e){
var w = e.target.cmp.shadowRoot.querySelector('.total').innerHTML.split('/');
e.target.cmp.shadowRoot.querySelector('.total').innerHTML = w[0] * 1 + 1 + '/' + e.target.cmp.limitImage;
e.target.cmp.totalLoaded++;
if(e.target.cmp.totalLoaded == e.target.totalToLoad){
e.target.cmp.shadowRoot.querySelector('.loading').style.display = 'none';
e.target.cmp.totalLoaded = 0;
}
};
//the close button
var titMin = document.createElement('span');
titMin.setAttribute('class','closer');
titMin.setAttribute('data-old','true');
titMin.innerHTML = '×';
titMin.cmp = this;
titMin.onclick = function(e){
var q = e.target.parentNode.parentNode.querySelectorAll('.closer');
for (var x = 0; x < q.length; x++) {
if(q[x] === e.target){
var w = e.target.cmp.shadowRoot.querySelector('.total').innerHTML.split('/');
e.target.cmp.shadowRoot.querySelector('.total').innerHTML = w[0] * 1 - 1 + '/' + e.target.cmp.limitImage;
e.target.parentNode.parentNode.removeChild(e.target.parentNode);
e.target.cmp.data.splice(x, 1);
e.target.cmp.fileName.splice(x, 1);
e.target.cmp.deletedSrc.push(e.target.previousSibling.getAttribute('src').split('/').pop());
}
}
};
//creating the thumbnail container
var imgCont = document.createElement('span');
imgCont.setAttribute('class','card');
imgCont.style.height = this.thumb_height * 1 + 10 + 'px';
imgCont.style.marginRight = '5px';
imgCont.style.cssFloat = 'left';
imgCont.appendChild(imgMin);
imgCont.appendChild(titMin);
this.shadowRoot.querySelector('#cont').appendChild(imgCont);
//creating a image anf canvas
var img = new Image();
img.src = e[i].getAttribute('src');
img.name = e[i].src.split('/').pop();
img.cmp = this;
img.onload = function(el) {
var elem = document.createElement('canvas');
var scaleFactor = width / el.target.width;
elem.width = width;
elem.height = el.target.height * scaleFactor;
var ctx = elem.getContext('2d');
ctx.drawImage(el.target, 0, 0, elem.width, elem.height);
var t = ctx.canvas.toDataURL('image/png', 1);
el.target.cmp.data.push('null');
el.target.cmp.fileName.push('null');
}
}
}
}
window.customElements.define('uploader-lab', uploaderLab);
Step 6: Add a function connectedCallback
It fires when the component is created
class uploaderLab extends HTMLElement {
//Step 1
//Step 2
//Step 3
//Step 4
//Step 5
connectedCallback () {
//Here will be the functionality.
}
}
window.customElements.define('uploader-lab', uploaderLab);
Inside connectedCallback function we will create two things:
- shadowRoot: Is the shadowDOM of the component. It contains the styles of the component and its html definition.
- Conditions to apply certain functions to the component.
Filling the connectedCallback function:
class uploaderLab extends HTMLElement {
//Step 1
//Step 2
//Step 3
//Step 4
//Step 5
connectedCallback () {
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
.closer{
position:relative;
background-color:black;
padding: 2px 6px;
box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19);
cursor:pointer;
font-size: 13px;
font-weight: 700;
color:white;
float: right;
margin-left: -23px;
display:none;
font-family:'Verdana',sans-serif !important;
}
.btnSel{
border: none;
display: inline-block;
padding: 8px 16px;
vertical-align: middle;
overflow: hidden;
text-decoration: none;
color: inherit;
background-color: inherit;
text-align: center;
cursor: pointer;
white-space: nowrap;
color: #fff !important;
background-color: #2196F3 !important;
border-radius: 32px;
margin-top:5px;
margin-left:5px;
}
.btnClear{
border: 1px solid #e1e1e1;
background-color: white !important;
color: gray !important;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.component{
padding:5px;
border:2px dashed #c9c2c2;
text-align:center;
overflow-y:auto;
}
#cont{
margin-top: 15px;
margin-left: 5px;
}
.card:hover span{
display:block;
}
.total{
color:gray;
margin-top:10px;
margin-bottom:10px;
}
.loading {
position: relative;
margin-top:-10px;
}
.loading-bar {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 100%;
margin: 2px;
animation: loading 0.6s ease-in-out infinite;
}
.loading-bar:nth-child(1) {
background-color: #3498db;
animation-delay: 0;
}
.loading-bar:nth-child(2) {
background-color: #c0392b;
animation-delay: 0.09s;
}
.loading-bar:nth-child(3) {
background-color: #f1c40f;
animation-delay: .18s;
}
.loading-bar:nth-child(4) {
background-color: #27ae60;
animation-delay: .27s;
}
@keyframes loading {
0% {
transform: scale(1);
}
20% {
transform: scale(1.5);
}
40% {
transform: scale(1);
}
}
</style>
<div class='component'>
<div class='total'></div>
<div id='loading' class="loading" style='display:none;'>
<div class="loading-bar"></div>
<div class="loading-bar"></div>
<div class="loading-bar"></div>
<div class="loading-bar"></div>
</div>
<div style='color:gray;margin-bottom:10px;'>
<slot name='label'>Select or drop the images</slot>
</div>
<label id='btnSelect' class='btnSel' for='selectFile'>
<slot name='btnSelect'>Select images</slot>
</label>
<label id='btnDelete' class='btnSel btnClear'>
<slot name='btnDelete'>Delete all</slot>
</label>
<input style='display:none;' type="file" name='upload' id='selectFile' multiple>
<div id='cont'></div>
</div>`;
//setting attributes and events
if(this.btnSelectText)
shadowRoot.querySelector('#btnSelect').innerHTML = this.getAttribute('btnSelectText');
if(this.btnDeleteText)
shadowRoot.querySelector('#btnDelete').innerHTML = this.getAttribute('btnDeleteText');
if(this.thumbHeight)
this.thumb_height = this.getAttribute('thumbHeight');
shadowRoot.querySelector('input').addEventListener('change', e => {
this.process(e,false);
});
shadowRoot.querySelector('#btnDelete').addEventListener('click', e => {
this.deleteAll();
});
shadowRoot.addEventListener('drop', e => {
this.drop(e);
});
shadowRoot.addEventListener('dragover', e => {
this.allowDrop(e,true);
});
shadowRoot.addEventListener('dragleave', e => {
this.allowDrop(e,false);
});
}
}
window.customElements.define('switch-lab', switchLab);
Database table example
Sending data to the server (javascript).
function sendData() {
var t = document.querySelector('#uploader');
var fileName = t.fileName;
var data = t.data;
var deletedSrc = t.deletedSrc;
//creating a string for the images name
var filesArray = '';
for (var i = 0; i < fileName.length; i++) {
if (i == 0)
filesArray = fileName[i];
else
filesArray = filesArray + '**' + fileName[i];
}
//creating a string for the images data
var dataArray = '';
for (var y = 0; y < data.length; y++) {
if (y == 0)
dataArray = data[y];
else
dataArray = dataArray + '**' + data[y];
}
//creating a string for the deleted images name
var deletedArray = '';
for (var z = 0; z < deletedSrc.length; z++) {
if (z == 0)
deletedArray = deletedSrc[z];
else
deletedArray = deletedArray + '**' + deletedSrc[z];
}
//sending to the server
var fd = new FormData();
fd.append("names", filesArray);
fd.append("upload", dataArray);
//always send a verificator(0 = adding and 1 = editing)
fd.append("verificator", 1);
//deletedSrc and idrow only if you are editing
fd.append("deletedSrc", deletedArray);
fd.append("idrow", '1');//id row to edit
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
//everything is ok
if (xhr.readyState == 4 && xhr.status == 200) {
var response = JSON.parse(xhr.responseText);
if (response.success == true) {
//if it was correct i clear all
document.querySelector('#uploader').clearAll();
}
}
}
xhr.open("POST", "upload.php");
xhr.send(fd);
}
Receive the data (php)
<?php
//Creating a connection class
class connection{
protected $connection;
protected $servername = "localhost";
protected $username = "root";
protected $password = "password";
public function conect(){
$this->connection = new mysqli($this->servername,$this->username,$this->password);
$this->connection->set_charset("utf8");
return $this->connection;
}
public function disconect(){
mysqli_close($this->connection);
}
}
//end of connection class
$target_dir = "upload/";
$img = [];
$name = [];
$toDelete = [];
$conex = new connection();
$connection = $conex->conect();
//if this case we are setting a name in server but usually comes in a post
$namePerson = 'John Doe';
/* In this case we will not use the files names
if($_POST['names']!='')
$name = explode('**',$_POST['names']);
*/
//if verificator is 0 we will add
if($_POST['verificator'] == 0){
$sql = sprintf("INSERT INTO myDatabase.myTable (`name`) VALUES ('%s')",
mysqli_real_escape_string($connection,$namePerson)
);
$connection->query($sql);
$id = $connection->insert_id;
//if uploaded some images
if($_POST['upload']!=''){
$img = explode('**',$_POST['upload']);
$nameString = '';
for ($i = 0; $i < count($img); $i++) {
if($img[$i]!='null'){
$image = str_replace('data:image/png;base64,', '', $img[$i]);
$image = str_replace(' ', '+', $image);
$data = base64_decode($image);
$unique = uniqid();
$file = $target_dir.$unique.'.png';
$success = file_put_contents($file, $data);
if($success != false)
$nameString = $nameString.$unique.'.png;';
}
}
if($nameString != ''){
$sql1 = "UPDATE myDatabase.myTable SET images='".$nameString."'
WHERE id='".$id."'";
$connection->query($sql1);
}
}
} else { //so if editing
$id = $_POST['idrow'];
//select the old images field
$sql = "SELECT images FROM myDatabase.myTable WHERE id=".$id;
$result = $connection->query($sql);
$imagesOld = array();
while($row = mysqli_fetch_assoc($result)){
$imagesOld = $row['images'];
}
//if deleted some images
if($_POST['deletedSrc']!=''){
$toDelete = explode('**',$_POST['deletedSrc']);
for ($y = 0; $y < count($toDelete); $y++) {
//remove from folder
unlink($target_dir.$toDelete[$y]);
//remove from field string
$imagesOld = str_replace($toDelete[$y].';', "", $imagesOld);
}
}
//if uploaded some images
if($_POST['upload']!=''){
$img = explode('**',$_POST['upload']);
for ($i = 0; $i < count($img); $i++) {
if($img[$i]!='null'){
$image = str_replace('data:image/png;base64,', '', $img[$i]);
$image = str_replace(' ', '+', $image);
$data = base64_decode($image);
$unique = uniqid();
$file = $target_dir.$unique.'.png';
$success = file_put_contents($file, $data);
if($success != false)
$imagesOld = $imagesOld.$unique.'.png;';
}
}
}
//updating all
$sql1 = "UPDATE myDatabase.myTable SET images='".$imagesOld."'
WHERE id='".$id."'";
$connection->query($sql1);
}
$conex->disconect();
echo '{"success": true}';
?>
Check a working demo for this code: