Skip to content

Instantly share code, notes, and snippets.

@geomago
Last active April 1, 2023 12:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save geomago/56cac7d102f0a67cf204e3099c82394c to your computer and use it in GitHub Desktop.
Save geomago/56cac7d102f0a67cf204e3099c82394c to your computer and use it in GitHub Desktop.
Experiment with Reactivity
<!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