Criando um CRUD com Javascript Vanilla
Vamos lá… primeiramente, o que é “Vanilla”? Simples… Baunilha em Inglês. Mas o que isso tem a ver com Javascript? Honestamente não sei, mas depois de algumas pesquisas, não encontrei significado algum do porque “Vanilla”, porém, nada mais é do que o Javascript puro, ou seja, livre de frameworks e bibliotecas.
E quanto a “CRUD”? Bom, essa é fácil, é um acrônimo para Create, Read, Update, Delete, ou seja, uma interface ou serviço que realizará operações de Criação, Leitura, Atualização e Deleção.
Então vamos lá, mãos a obra.
Primeiramente criamos o arquivo html com o nome index. Este é o arquivo principal de nosso CRUD e é ele que é chamado quando digitamos o endereço virtual de nosso site.
na estrutura de pasta temos então:
- vanilla-js-crud
- index.html
Código:
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Products</title>
</head>
<body>
<div id="container" class="container"></div>
<script src="js/app/helpers/ArrayUtils.js"></script>
<script src="js/app/helpers/DateUtils.js"></script>
<script src="js/app/models/Product.js"></script>
<script src="js/app/models/ProductList.js"></script>
<script src="js/app/view/ProductView.js"></script>
<script src="js/app/controllers/ProductController.js"></script>
<script>
let productController = new ProductController();
</script>
</body>
</html>
Agora vamos dar vida ao html com o código JavaScript.
Primeiramente vamos escrever o arquivo que representará nosso Produto:
na estrutura de pasta temos então:
- vanilla-js-crud
- index.html
- js
- app
- models
- Product.js
Código:
class Product {
constructor(id, description, price, quantity,tax) {
this._id = id;
this._description = description;
this._price = price;
this._quantity = quantity;
this._tax = tax;
this._total = this.calculateTotal();
}
get id() {
return this._id;
}
get description() {
return this._description;
}
get price() {
return this._price;
}
get quantity() {
return this._quantity;
}
get tax() {
return this._tax;
}
get total() {
return this._total;
}
calculateTotal() {
return (this._price*this._quantity)*((this._tax/100)+1);
}
}
E então o arquivo que representará nossa Lista de Produtos.
na estrutura de pasta temos então:
- vanilla-js-crud
- index.html
- js
- app
- models
- Product.js
- ProductList.js
Código:
class ProductList {
constructor() {
this._products = [];
}
add(product) {
this._products.push(product);
}
get products() {
//Hack to makes _products imutable;
// return [].concat(this._products);
return this._products;
}
set products(products) {
this._products = products;
}
findById(id) {
let productToReturn;
this._products.forEach(product => {
if (product.id === id) {
productToReturn = product;
}
});
return productToReturn;
}
}
Agora a interface que será adicionada por código javascript em nosso index.html contendo o formulário e botões e interações.
na estrutura de pasta temos então:
- vanilla-js-crud
- index.html
- js
- app
- models
- Product.js
- ProductList.js
- view
- ProductView.js
Código:
class ProductView {
constructor(container) {
this._container = container;
}
update(productList) {
this._container.innerHTML = this.template(productList);
}
template(productList) {
return `<div>
<h2>Products</h2>
</div>
<div>
<form id="productForm" onsubmit="productController.save(event)">
<label for="idInput">Code</label>
<input type="number" name="idInput" placeholder="Item code">
<label for="descriptionInput">Description</label>
<input type="text" name="descriptionInput" placeholder="Item description">
<label for="priceInput">Price</label>
<input type="number" name="priceInput" placeholder="Item Price">
<label for="quantityInput">Quantity</label>
<input type="number" name="quantityInput" placeholder="Quantity">
<label for="taxInput">Tax</label>
<input type="number" name="taxInput" placeholder="Tax">
<button id="btnSave">Save</button>
</form>
</div>
<div>
<hr/>
<table id="productTable" class="table table-striped" >
<thead>
<tr>
<th scope="col">Code</th>
<th scope="col">Description</th>
<th scope="col">Quantity</th>
<th scope="col">Price</th>
<th scope="col">Tax</th>
<th scope="col">Montant</th>
<th scope="col">Edit</th>
<th scope="col">Remove</th>
</tr>
</thead>
<tbody>
${
productList.products.map(product => `
<tr>
<td id="productId" >${product.id}</td>
<td id="productDescription" >${product.description}</td>
<td id="price" >${product.price}</td>
<td id="quantity" >${product.quantity}</td>
<td id="tax" >${product.tax}</td>
<td>${product.total}</td>
<td><img src="/img/edit.svg" width="16px" height="16px" onclick="productController.edit(event)" /></td>
<td><img src="/img/remove.svg" width="16px" height="16px" onclick="productController.remove(event)" /></td>
</tr>
`).join('')
}
</tbody>
<tfoot>
</tfoot>
</table>
</div>`
}
}
E finalmente o arquivo que será o controlador de toda interação entre nossa interface html e as funcionalidades escritas em javascripts.
na estrutura de pasta temos então:
- vanilla-js-crud
- index.html
- js
- app
- models
- Product.js
- ProductList.js
- view
- ProductView.js
- controllers
- ProductController.js
Código:
class ProductController {
constructor() {
//setting querySelector function to $
let $ = document.querySelector.bind(document);
let container = $("#container");
this._productView = new ProductView(container);
this._productList = new ProductList();
this._productView.update(this._productList);
}
save(event) {
event.preventDefault();
let editedProduct = this._createProduct();
let productOld = this._productList.findById(editedProduct.id);
if (productOld != null) {
let index = this._findProductById(productOld.id);
this._productList.products = ArrayUtils.removeIndex(this._productList.products,index);
}
this._productList.add(editedProduct);
this._productView.update(this._productList);
}
_findProductById(productId) {
let indexToRemove;
this._productList.products.forEach((element, index) => {
if (element.id === productId) {
indexToRemove = index;
}
});
return indexToRemove;
}
edit(event) {
console.log(event);
let form = document.querySelector("#productForm");
let row = event.target.parentNode.parentNode;
form.idInput.value = row.querySelector("#productId").textContent;
form.descriptionInput.value = row.querySelector("#productDescription").textContent;
form.priceInput.value = row.querySelector("#price").textContent;
form.quantityInput.value = row.querySelector("#quantity").textContent;
form.taxInput.value = row.querySelector("#tax").textContent;
}
remove(event) {
let row = event.target.parentNode.parentNode;
let productId = row.querySelector("#productId").textContent;
let indexToRemove;
this._productList.products.forEach((element, index) => {
if (element.id === productId) {
indexToRemove = index;
}
});
event.target.parentNode.parentNode.remove();
this._productList.products = ArrayUtils.removeIndex(this._productList.products, indexToRemove);
}
_createProduct() {
let form = document.querySelector("#productForm");
let product = new Product(form.idInput.value,
form.descriptionInput.value,
form.priceInput.value,
form.quantityInput.value,
form.taxInput.value);
form.reset();
return product;
}
}
Foram utilizados 2 arquivos a mais com a funcionalidade de Formatar Datas e outro de Controlar conjuntos de itens.
na estrutura de pasta temos então:
- vanilla-js-crud
- index.html
- js
- app
- models
- Product.js
- ProductList.js
- view
- ProductView.js
- controllers
- ProductController.js
- helpers
- ArrayUtils.js
- DateUtils.js
Código ArrayUtils.js:
class ArrayUtils {
constructor() {
throw new Error("ArrayUtils cannot be instantiated.");
}
static removeIndex(array, index) {
if (ArrayUtils.isArrayValid(array)) {
let newArray = [];
array.forEach((element,elementIndex) => {
if (elementIndex != index) {
newArray.push(element);
}
});
return newArray;
} else {
return array;
}
}
static isArrayValid(array) {
return (typeof array != "undefined"
&& array != null
&& array.length != null
&& array.length > 0);
}
}
Código DateUtils.js:
class DateUtils {
constructor() {
throw new Error("DateUtils cannot be instantiated.");
}
static textToDate(textToConvert) {
if (/\d{4}-\d{2}-\d{2}/.test(date)) {
//the "index % 2" will return 0 for the first and third position of the date array and 1
//for the second and then it will be subtract from the item value. The result is the month -1
//because the Date class counts months from the 0
return new Date(...textToConvert.split('-').map((item, index) => item - index % 2));
} else {
throw new Error("The date format must be AAAA-MM-DD");
}
}
static dateToText(date) {
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`; }
}
Utilizamos também 2 imagens no formato svg para representar os botões de editar e remover.
na estrutura de pasta temos então:
- vanilla-js-crud
- index.html
- js
- app
- models
- Product.js
- ProductList.js
- view
- ProductView.js
- controllers
- ProductController.js
- helpers
- ArrayUtils.js
- DateUtils.js
- img
- edit.svg
- remove.svg