feat: ProductManager als Modul mit template und JSON

This commit is contained in:
Philippe Torrel
2026-07-20 14:21:44 +02:00
parent 549c2ac243
commit 33e6bef2e5
13 changed files with 479 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
// 'use strict'; // - nicht mehr notwendig, da JS über type="module" in den strikten Modus gesetzt wird.
import EigenerName from './modules/Example.js'; // WICHTIG mit Dateiendung ohne JS-Bundler (esbuild, rollup, browserify, parcel, webpack). Es muss der komplette Pfad (absolut oder relativ) mit Dateiendung angegeben werden.
import { Example as ExampleRenamed, Example2, TAX_RATE } from './modules/examples.js';
import Examples from './modules/examples.js';
(() => {
// === DOM & VARS =======
const DOM = {};
// === INIT =============
const init = () => {
EigenerName(); // => init Example
ExampleRenamed(); // => Example init aus examples
Example2(); // => Example2 init aus examples
console.log(TAX_RATE); // => 1.19
Examples.method();
Examples.method2();
console.log(Examples.eigenschaft); //=> 'Wert'
console.log(Examples.text); // => 'Ich bin ein Text'
};
// === EVENTHANDLER =====
// === XHR/FETCH ========
// === FUNCTIONS ========
init();
})();

View File

@@ -0,0 +1,9 @@
const Example = () => {
console.log('init Example');
};
// Freigabe in ES6 // import Example from './Example.js'
export default Example;
// CommonJS ("old" Node) -> require('./Example)
// exports.Example = Example;

View File

@@ -0,0 +1,21 @@
export const Example = () => {
console.log('Example init aus examples');
};
export const Example2 = () => {
console.log('Example2 init aus examples');
};
export const TAX_RATE = 1.19;
export const UPDATE_TODO = 'updateTodo';
const text = 'Ich bin ein Text';
// Default Objekt als Freigabe
export default {
method: Example,
method2: Example2,
eigenschaft: 'Wert',
// text: text
text, // Kurzschreibweise für text: text
};

View File

@@ -0,0 +1,50 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Verwendung von Import - Module in (ES6)</title>
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" /> -->
<style>
@import url('https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css');
</style>
<script src="assets/js/main.js" type="module"></script>
</head>
<body>
<main>
<div class="container py-5">
<h1>Verwendung von Import - Module in (ES6)</h1>
<p>
Die statische
<a href="https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Statements/import">import</a
>-Statement Deklaration wird verwendet, um schreibgeschützte, dynamische Bindings zu importieren, die von
einem anderen Modul exportiert werden. Die importierten Bindings werden dynamische Bindings genannt, weil sie
durch das Modul, das das Binding exportiert, aktualisiert werden, aber nicht durch das importierende Modul neu
zugewiesen werden können.
</p>
<p>
Um die import-Deklaration in einer Quelldatei zu verwenden, muss die Datei zur Laufzeit als Modul
interpretiert werden. In HTML geschieht dies, indem <strong>type="module"</strong> zum
<strong>&lt;script&gt;</strong>-Tag hinzugefügt wird. Module werden automatisch im Strict Mode interpretiert.
</p>
<p>
Es gibt auch eine funktionsähnliche dynamische import(), die keine Skripte des Typs
<strong>type="module"</strong> erfordert.
</p>
<hr />
<p>Um die Modul-Schreibweise in JS zu verwenden, müssen folgende Punkte berücksichtigt werden:</p>
<ul class="list list-group">
<li class="list-group-item">Script Tag benötigt Modulangabe mit <strong>type="module"</strong></li>
<li class="list-group-item">
Kommunikation läuft über über das HTTP-Protokoll (HTTP-Kommunikation lokal über Server)
</li>
<li class="list-group-item">Eingebundene Module müssen mit Dateiendung (.js) angegeben werden.</li>
</ul>
</div>
</main>
<script>
'use strict';
</script>
</body>
</html>

View File

@@ -0,0 +1,11 @@
{
"name": "05_es6-module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "npx http-server -c-1 -p 3000"
},
"keywords": [],
"type": "commonjs"
}

View File

@@ -0,0 +1,11 @@
node_modules/
dist/
.env
.env.local
.env.*.local
.DS_Store
*.log
.vite/
coverage/
.idea/
# .vscode/

View File

@@ -0,0 +1,34 @@
*,
html {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
body {
background-color: #efefef;
}
/* Mobile first */
.product-manager table tfoot .row > * {
margin-bottom: 1rem;
}
@media (min-width: 992px) {
.product-manager table tfoot .row > * {
margin-bottom: 0;
}
}
.product-manager table .th-actions {
width: 150px;
}
.product-manager table tbody td button {
margin: 3px;
}

View File

@@ -0,0 +1,24 @@
import ProductManager from './modules/ProductManager.js';
(() => {
// === DOM & VARS =======
const DOM = {
productManagers: Array.from(document.querySelectorAll('.product-manager')),
};
// === INIT =============
const init = () => {
DOM.productManagers.forEach((el) => {
const pm = ProductManager(el);
// pm.init()
});
};
// === EVENTHANDLER =====
// === XHR/FETCH ========
// === FUNCTIONS ========
init();
})();

View File

@@ -0,0 +1,169 @@
// Factory Function
const ProductManager = (el = null) => {
// === DOM & VARS =======
const module = el || document.querySelector('.product-manager') || console.error('Product Manager not found');
if (!module) return;
const DOM = {
module,
table: module.querySelector('.table-products'),
tBody: module.querySelector('tbody'),
inputName: module.querySelector('.input-product-name'),
inputPrice: module.querySelector('.input-product-price'),
btnAdd: module.querySelector('.button-product-add'),
templateRow: module.querySelector('.template-row'),
};
console.log(DOM);
// === INIT =============
const init = () => {
console.log('init');
// Funktionaufrufe zu beginn der Anwendung
initProducts();
// Event-Lauscher zu beginn der Anwendung
DOM.btnAdd.addEventListener('click', onClickAdd);
};
// === EVENTHANDLER =====
const onClickAdd = (e) => {
console.log('click');
const product = {
name: DOM.inputName.value,
price: Number(DOM.inputPrice.value),
};
addProduct(product);
disableNonFunctionalButtons();
};
const onClickRemove = (e) => {
const btnEl = e.currentTarget;
//const currentRowEl = btnEl.parentNode.parentNode; // parentNode -> td - parentNode -> tr
// MDN https://developer.mozilla.org/de/docs/Web/API/Element/closest
//const currentRowEl = btnEl.closest('tr');
const currentRowEl = parents(btnEl, 'tr')[0];
currentRowEl.remove(); // kein IE11 support
// DOM.tBody.removeChild(currentRowEl);
disableNonFunctionalButtons();
};
const onClickMoveUp = (e) => {
const btnEl = e.currentTarget;
// const currentRowEl = btnEl.parentNode.parentNode; // aktuelle Zeile (tr) mit dem Button
const currentRowEl = parents(btnEl, 'tr')[0];
// ParentNode.insertBefore(referenceElement, targetElement)
// DOM.tBody.insertBefore(currentRowEl, currentRowEl.previousElementSibling);
// targetElement.before(referenceElement)
currentRowEl.previousElementSibling.before(currentRowEl);
console.log('move up');
disableNonFunctionalButtons();
};
const onClickMoveDown = (e) => {
const btnEl = e.currentTarget;
// const currentRowEl = btnEl.parentNode.parentNode; // aktuelle Zeile (tr) mit dem Button
const currentRowEl = parents(btnEl, 'tr')[0];
// ParentNode.insertBefore(referenceElement, targetElement)
// DOM.tBody.insertBefore(currentRowEl, currentRowEl.nextElementSibling.nextElementSibling);
// targetElement.after(referenceElement)
currentRowEl.nextElementSibling.after(currentRowEl);
console.log('move down');
disableNonFunctionalButtons();
};
// === XHR/FETCH ========
const fetchProducts = async () => {
try {
const response = await fetch('data/products.json'); // HTTP Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response
return data;
} catch (error) {
console.error(error);
return error;
}
};
// === FUNCTIONS ========
const initProducts = () => {
fetchProducts().then((products) => {
products.forEach((product) => {
addProduct(product);
});
disableNonFunctionalButtons();
});
// PRODUCTS.forEach((product) => {
// addProduct(product);
// });
//disableNonFunctionalButtons();
// PRODUCTS.forEach(addProduct);
};
const addProduct = (product) => {
const { name, price } = product;
//
const trEl = DOM.templateRow.content.firstElementChild.cloneNode(true);
const tdNameEl = trEl.querySelector('.td-name');
const tdPriceEl = trEl.querySelector('.td-price');
const btnRemoveEl = trEl.querySelector('.button-product-remove');
const btnMoveUpEl = trEl.querySelector('.button-product-move-up');
const btnMoveDownEl = trEl.querySelector('.button-product-move-down');
tdNameEl.textContent = name;
tdPriceEl.textContent = price;
btnRemoveEl.addEventListener('click', onClickRemove);
btnMoveUpEl.addEventListener('click', onClickMoveUp);
btnMoveDownEl.addEventListener('click', onClickMoveDown);
DOM.tBody.appendChild(trEl); // Im DOM hinzufügen
};
const disableNonFunctionalButtons = () => {
Array.from(DOM.tBody.querySelectorAll('tr')).forEach((tr) => {
const btnMoveUp = tr.querySelector('.button-product-move-up');
const btnMoveDown = tr.querySelector('.button-product-move-down');
btnMoveUp.disabled = !tr.previousElementSibling;
btnMoveDown.disabled = !tr.nextElementSibling;
});
};
// Helferfunktion aus jQuery (youmightnotjquery)
const parents = (el, selector) => {
const parents = [];
while ((el = el.parentNode) && el !== document) {
if (!selector || el.matches(selector)) parents.push(el);
}
return parents;
};
init();
return {
init,
};
};
export default ProductManager;

View File

@@ -0,0 +1,7 @@
// Konfigurationsvariable
const PRODUCTS = [
{ name: '3Doodler 3D Printing Pen', price: 29.99 },
{ name: 'Powerstation 5- E. Maximus Chargus', price: 44.95 },
{ name: '8-Bit Legendary Hero Heat-Change Mug', price: 6.99 },
{ name: '16-Bit Legendary Hero Heat-Change Mug', price: 10.99 },
];

View File

@@ -0,0 +1,5 @@
[
{ "name": "3Doodler 3D Printing Pen", "price": 29.99 },
{ "name": "Powerstation 5- E. Maximus Chargus", "price": 44.95 },
{ "name": "8-Bit Legendary Hero Heat-Change Mug", "price": 6.99 }
]

View File

@@ -0,0 +1,92 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Product Manager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" />
<link rel="stylesheet" href="assets/css/main.css" />
<!-- <script src="data/products.js"></script> -->
<script src="assets/js/main.js" type="module"></script>
</head>
<body>
<main>
<div class="product-manager my-5">
<div class="container">
<!-- table.table.table-striped.table-products>(thead.table-dark>tr>th{Name}+th{Price in &euro;})+tbody>tr>td*2 -->
<table class="table table-striped table-products">
<thead class="table-dark">
<tr>
<th class="th-name">Name</th>
<th class="th-price">Price in &euro;</th>
<th class="th-actions">Actions</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="3">
<div class="row">
<div class="col-12 col-md-6 col-lg-6">
<input
type="text"
class="form-control input-product-name"
name="product-name"
placeholder="Insert product name"
aria-label="Product Name" />
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="input-group">
<input
type="number"
name="product-price"
class="form-control input-product-price"
placeholder="00.00"
aria-label="Product Price"
step="0.01"
min="0" />
<span class="input-group-text"></span>
</div>
</div>
<div class="col-12 col-md-6 col-lg">
<button class="btn btn-dark button-product-add">
<i class="fas fa-plus"></i>
Add product
</button>
</div>
</div>
<!-- ▲ /row ▲ -->
</td>
</tr>
</tfoot>
</table>
</div>
<template class="template-row">
<tr>
<td class="td-name">[PRODUCT_NAME]</td>
<td class="td-price">[PRODUCT_PRICE]</td>
<td class="td-actions">
<button class="btn btn-sm btn-danger button-product-remove">
<i class="fas fa-trash-can"></i>
<span class="visually-hidden">Remove Product</span>
</button>
<button class="btn btn-sm btn-secondary button-product-move-up">
<i class="fas fa-caret-up"></i>
<span class="visually-hidden">Move Up</span>
</button>
<button class="btn btn-sm btn-secondary button-product-move-down">
<i class="fas fa-caret-down"></i>
<span class="visually-hidden">Move Down</span>
</button>
</td>
</tr>
</template>
</div>
<!-- ▲ /product-manager ▲ -->
</main>
</body>
</html>

View File

@@ -0,0 +1,11 @@
{
"name": "06_product-manager-tpl-json",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "npx http-server -c-1 -p 3000"
},
"keywords": [],
"type": "module"
}