Last active
April 1, 2023 12:12
-
-
Save geomago/56cac7d102f0a67cf204e3099c82394c to your computer and use it in GitHub Desktop.
Experiment with Reactivity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Product Table</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> | |
<style> | |
#productTable img { | |
max-width: 100px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Product Table</h1> | |
<table id="productTable" class="table table-striped"> | |
<thead> | |
<tr> | |
<th>ID</th> | |
<th>Title <button id="sort">Sort</button></th> | |
<th>Description</th> | |
<th style="min-width:120px">Price</th> | |
<th>Thumbnail</th> | |
<th>Action</th> | |
</tr> | |
</thead> | |
<tbody> | |
<template id="product-table-row"> | |
<tr data-field="index:data-index"> | |
<td data-field="id" class="text-right"></td> | |
<td data-field="title"></td> | |
<td data-field="description"></td> | |
<td class="text-right"> | |
<span data-field="price"></span> | |
<button data-field="index:data-index" class="btn btn-light plus">+</button> | |
</td> | |
<td><img data-field="thumbnail:src"></td> | |
<td> | |
<button data-field="index:data-index" class="btn btn-danger delete">Delete</button> | |
</td> | |
</tr> | |
</template> | |
</tbody> | |
</table> | |
</div> | |
<script> | |
{ | |
let rowTemplate = document.getElementById('product-table-row'); // template | |
let trow = rowTemplate.content; // innerHTML of the template (table row) | |
let tbody = rowTemplate.parentElement; // container (tbody) | |
let products; // proxy of the products array | |
fetch("https://dummyjson.com/products") | |
.then(response => response.json()) | |
.then(data => { | |
// Wrap the product array with a proxy | |
products = new Proxy(data.products, { | |
// Set method: it enters when any item is assigned, added, or the length is changed | |
// It does not enter if a property of an item is changed | |
set: (target, property, value, receiver) => { | |
if (property == "length") { | |
Reflect.set(target, property, value, receiver); | |
return true; // Reflect.set return false, but must be done | |
} else { | |
var row = tbody.querySelector(`tr[data-index="${property}"]`); | |
if (!row) return; // no provision for push | |
applyData(row, value, property); // apply data to HTML elements in the row | |
return Reflect.set(target, property, value, receiver); // allow standard continuation | |
} | |
}, | |
// deleteProperty: it enters when an item is removed from the array | |
deleteProperty: (target, property) => { | |
tbody.querySelector(`tr[data-index="${property}"]`)?.remove(); | |
return Reflect.deleteProperty(target, property); // allow standard continuation | |
} | |
}); | |
renderArray(products, trow, tbody); // create the table | |
// Add listeners to all delete buttons | |
document.querySelectorAll('tbody button.btn-danger').forEach((item) => { | |
item.addEventListener('click', deleteRow); | |
}); | |
// Create listener for plus1 buttons | |
document.querySelectorAll('tbody button.plus').forEach((item) => { | |
item.addEventListener('click', pricePlusOne); | |
}); | |
}); | |
// Function to render an array by repeating and rendering an HTML template | |
let renderArray = (arr, trow, container) => { | |
arr.forEach((item, index) => { // loop over items | |
let newRow = trow.cloneNode(true); // create an empty row from template | |
applyData(newRow, item, index); // apply data to HTML elements in the template | |
container.appendChild(newRow); // append row to container | |
}); | |
} | |
// Generic function to apply data to HTML element containing a data-field attribute. | |
let applyData = (element, object, index) => { | |
element.querySelectorAll('[data-field]').forEach((item) => { | |
object.index = parseInt(index); | |
let [field, target] = item.dataset.field.split(':'); | |
let value = object[field]; | |
if (target===undefined) { | |
item.innerHTML = value; | |
} else { | |
item.setAttribute(target, value); | |
} | |
}); | |
} | |
// Delete row | |
let deleteRow = (event) => { | |
var btn = event.target; | |
products.splice(btn.dataset.index, 1); | |
} | |
// Sort by title | |
document.getElementById('sort').addEventListener('click', () => { | |
products.sort((a, b) => { | |
return a.title.localeCompare(b.title) | |
}); | |
}); | |
// Add 1 to price | |
let pricePlusOne = () => { | |
var index = event.target.dataset.index; | |
products[index].price++; // increment price | |
// first method: force rendering by reassigning item | |
products[index] = products[index]; | |
// second method: direct change of the HTML element | |
//tbody.querySelector(`tr[data-index="${index}"] [data-field=price]`).innerText = products[index].price; | |
} | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment