1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-04-01 19:55:53 +02:00

🔤 Examples

This commit is contained in:
Jan Andrle 2025-03-15 18:45:18 +01:00
parent 90dfc56dc4
commit 9e61c6a6c5
Signed by: jaandrle
GPG Key ID: B3A25AED155AFFAB
6 changed files with 1939 additions and 1 deletions

View File

@ -116,7 +116,12 @@ format selector](https://jaandrle.github.io/deka-dom-el/) on the documentation s
### Documentation
- [**Interactive Guide**](https://jaandrle.github.io/deka-dom-el): WIP
- [Examples](./examples/): TBD/WIP
- [**Examples Gallery**](https://jaandrle.github.io/deka-dom-el/p15-examples.html): A comprehensive collection of code examples and case studies
- Interactive Form with Validation
- Data Dashboard with Charts
- Image Gallery with Lightbox
- Kanban Task Manager
- TodoMVC Implementation
## Understanding Signals

View File

@ -0,0 +1,394 @@
/**
* Case Study: Data Dashboard with Charts
*
* This example demonstrates:
* - Integration with a third-party charting library
* - Data fetching and state management
* - Responsive layout design
* - Multiple interactive components working together
*/
import { el, on } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
/**
* Data Dashboard Component with Chart Integration
* @returns {HTMLElement} Dashboard element
*/
export function DataDashboard() {
// Mock data for demonstration
const DATA = {
sales: [42, 58, 65, 49, 72, 85, 63, 70, 78, 89, 95, 86],
visitors: [1420, 1620, 1750, 1850, 2100, 2400, 2250, 2500, 2750, 2900, 3100, 3200],
conversion: [2.9, 3.5, 3.7, 2.6, 3.4, 3.5, 2.8, 2.8, 2.8, 3.1, 3.0, 2.7],
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
};
// Application state
const selectedYear = S(2024);
const selectedDataType = S(/** @type {'sales' | 'visitors' | 'conversion'} */ ('sales'));
const isLoading = S(false);
const error = S(null);
// Filter options
const years = [2022, 2023, 2024];
const dataTypes = [
{ id: 'sales', label: 'Sales', unit: 'K' },
{ id: 'visitors', label: 'Visitors', unit: '' },
{ id: 'conversion', label: 'Conversion Rate', unit: '%' }
];
// Computed values
const selectedData = S(() => {
return DATA[selectedDataType.get()];
});
const currentDataType = S(() => {
return dataTypes.find(type => type.id === selectedDataType.get());
});
const totalValue = S(() => {
const data = selectedData.get();
return data.reduce((sum, value) => sum + value, 0);
});
const averageValue = S(() => {
const data = selectedData.get();
return data.reduce((sum, value) => sum + value, 0) / data.length;
});
const highestValue = S(() => {
return Math.max(...selectedData.get());
});
// Event handlers
const onYearChange = on("change", e => {
selectedYear.set(parseInt(/** @type {HTMLSelectElement} */(e.target).value));
loadData();
});
const onDataTypeChange = on("click", e => {
const type = /** @type {'sales' | 'visitors' | 'conversion'} */(
/** @type {HTMLButtonElement} */(e.currentTarget).dataset.type);
selectedDataType.set(type);
});
// Simulate data loading
function loadData() {
isLoading.set(true);
error.set(null);
// Simulate API call
setTimeout(() => {
if (Math.random() > 0.9) {
// Simulate occasional error
error.set('Failed to load data. Please try again.');
}
isLoading.set(false);
}, 800);
}
// Reactive chart rendering
const chart = S(()=> {
const chart= el("canvas", { id: "chart-canvas", width: 800, height: 400 });
const ctx = chart.getContext('2d');
const data = selectedData.get();
const months = DATA.months;
const width = chart.width;
const height = chart.height;
const maxValue = Math.max(...data) * 1.1;
const barWidth = width / data.length - 10;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw background grid
ctx.beginPath();
ctx.strokeStyle = '#f0f0f0';
ctx.lineWidth = 1;
for(let i = 0; i < 5; i++) {
const y = height - (height * (i / 5)) - 30;
ctx.moveTo(50, y);
ctx.lineTo(width - 20, y);
// Draw grid labels
ctx.fillStyle = '#999';
ctx.font = '12px Arial';
ctx.fillText(Math.round(maxValue * (i / 5)), 20, y + 5);
}
ctx.stroke();
// Draw bars
data.forEach((value, index) => {
const x = index * (barWidth + 10) + 60;
const barHeight = (value / maxValue) * (height - 60);
// Bar
ctx.fillStyle = '#4a90e2';
ctx.fillRect(x, height - barHeight - 30, barWidth, barHeight);
// Month label
ctx.fillStyle = '#666';
ctx.font = '12px Arial';
ctx.fillText(months[index], x + barWidth/2 - 10, height - 10);
});
// Chart title
ctx.fillStyle = '#333';
ctx.font = 'bold 14px Arial';
ctx.fillText(`${currentDataType.get().label} (${selectedYear.get()})`, width/2 - 80, 20);
return chart;
});
return el("div", { className: "dashboard" }).append(
el("header", { className: "dashboard-header" }).append(
el("h1", "Sales Performance Dashboard"),
el("div", { className: "year-filter" }).append(
el("label", { htmlFor: "yearSelect", textContent: "Select Year:" }),
el("select", { id: "yearSelect" },
on.host(el=> el.value = selectedYear.get().toString()),
onYearChange
).append(
...years.map(year => el("option", { value: year, textContent: year }))
)
)
),
// Error message (only shown when there's an error)
S.el(error, errorMsg => !errorMsg
? el()
: el("div", { className: "error-message" }).append(
el("p", errorMsg),
el("button", { textContent: "Retry", type: "button" }, on("click", loadData)),
),
),
// Loading indicator
S.el(isLoading, loading => !loading
? el()
: el("div", { className: "loading-spinner" })
),
// Main dashboard content
el("div", { className: "dashboard-content" }).append(
// Metrics cards
el("div", { className: "metrics-container" }).append(
el("div", { className: "metric-card" }).append(
el("h3", "Total"),
el("#text", S(() => `${totalValue.get().toLocaleString()}${currentDataType.get().unit}`)),
),
el("div", { className: "metric-card" }).append(
el("h3", "Average"),
el("#text", S(() => `${averageValue.get().toFixed(1)}${currentDataType.get().unit}`)),
),
el("div", { className: "metric-card" }).append(
el("h3", "Highest"),
el("#text", S(() => `${highestValue.get()}${currentDataType.get().unit}`)),
),
),
// Data type selection tabs
el("div", { className: "data-type-tabs" }).append(
...dataTypes.map(type =>
el("button", {
type: "button",
className: S(() => selectedDataType.get() === type.id ? 'active' : ''),
dataType: type.id,
textContent: type.label
}, onDataTypeChange)
)
),
// Chart container
el("div", { className: "chart-container" }).append(
S.el(chart, chart => chart)
)
),
);
}
// Render the component
document.body.append(
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
el(DataDashboard)
),
el("style", `
.dashboard {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 1rem;
background: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.dashboard-header h1 {
font-size: 1.5rem;
margin: 0;
color: #333;
}
.year-filter {
display: flex;
align-items: center;
gap: 0.5rem;
}
.year-filter select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.metrics-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.metric-card {
background: #f9f9f9;
border-radius: 8px;
padding: 1rem;
text-align: center;
transition: transform 0.2s ease;
}
.metric-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
}
.metric-card h3 {
margin-top: 0;
color: #666;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.metric-card p {
font-size: 1.5rem;
font-weight: bold;
color: #333;
margin: 0;
}
.data-type-tabs {
display: flex;
border-bottom: 1px solid #eee;
margin-bottom: 1.5rem;
}
.data-type-tabs button {
background: none;
border: none;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
color: #666;
position: relative;
}
.data-type-tabs button.active {
color: #4a90e2;
font-weight: 500;
}
.data-type-tabs button.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 3px;
background: #4a90e2;
border-radius: 3px 3px 0 0;
}
.chart-container {
background: #fff;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
.loading-spinner::before {
content: '';
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #4a90e2;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
background: #ffecec;
color: #e74c3c;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.error-message p {
margin: 0;
}
.error-message button {
background: #e74c3c;
color: white;
border: none;
border-radius: 4px;
padding: 0.5rem 1rem;
cursor: pointer;
}
@media (max-width: 768px) {
.metrics-container {
grid-template-columns: 1fr;
}
.dashboard-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.year-filter {
width: 100%;
}
.year-filter select {
flex-grow: 1;
}
}
`)
);

View File

@ -0,0 +1,417 @@
/**
* Case Study: Interactive Image Gallery
*
* This example demonstrates:
* - Dynamic loading of content
* - Lightbox functionality
* - Animation handling
* - Keyboard and gesture navigation
*/
import { el, memo, on } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
/**
* Interactive Image Gallery Component
* @returns {HTMLElement} Gallery element
*/
export function ImageGallery() {
// Sample image data
const images = [
{ id: 1, src: 'https://api.algobook.info/v1/randomimage?category=nature', alt: 'Nature', title: 'Beautiful Landscape' },
{ id: 2, src: 'https://api.algobook.info/v1/randomimage?category=places', alt: 'City', title: 'Urban Architecture' },
{ id: 3, src: 'https://api.algobook.info/v1/randomimage?category=people', alt: 'People', title: 'Street Photography' },
{ id: 4, src: 'https://api.algobook.info/v1/randomimage?category=food', alt: 'Food', title: 'Culinary Delights' },
{ id: 5, src: 'https://api.algobook.info/v1/randomimage?category=animals', alt: 'Animals', title: 'Wildlife' },
{ id: 6, src: 'https://api.algobook.info/v1/randomimage?category=travel', alt: 'Travel', title: 'Adventure Awaits' },
{ id: 7, src: 'https://api.algobook.info/v1/randomimage?category=computer', alt: 'Technology', title: 'Modern Tech' },
{ id: 8, src: 'https://api.algobook.info/v1/randomimage?category=music', alt: 'Art', title: 'Creative Expression' },
];
// Application state
const selectedImageId = S(null);
const filterTag = S('all');
const imagesToDisplay = S(() => {
const tag = filterTag.get();
if (tag === 'all') return images;
else return images.filter(img => img.alt.toLowerCase() === tag);
})
// Derived state
const selectedImage = S(() => {
const id = selectedImageId.get();
return id ? images.find(img => img.id === id) : null;
});
const isLightboxOpen = S(() => selectedImage.get() !== null);
// Event handlers
const onImageClick = id => on("click", () => {
selectedImageId.set(id);
document.body.style.overflow = 'hidden'; // Prevent scrolling when lightbox is open
// Add keyboard event listeners when lightbox opens
document.addEventListener('keydown', handleKeyDown);
});
const closeLightbox = () => {
selectedImageId.set(null);
document.body.style.overflow = ''; // Restore scrolling
// Remove keyboard event listeners when lightbox closes
document.removeEventListener('keydown', handleKeyDown);
};
const onPrevImage = e => {
e.stopPropagation(); // Prevent closing the lightbox
const images = imagesToDisplay.get();
const currentId = selectedImageId.get();
const currentIndex = images.findIndex(img => img.id === currentId);
const prevIndex = (currentIndex - 1 + images.length) % images.length;
selectedImageId.set(images[prevIndex].id);
};
const onNextImage = e => {
e.stopPropagation(); // Prevent closing the lightbox
const images = imagesToDisplay.get();
const currentId = selectedImageId.get();
const currentIndex = images.findIndex(img => img.id === currentId);
const nextIndex = (currentIndex + 1) % images.length;
selectedImageId.set(images[nextIndex].id);
};
const onFilterChange = tag => on("click", () => {
filterTag.set(tag);
});
// Keyboard navigation handler
function handleKeyDown(e) {
switch(e.key) {
case 'Escape':
closeLightbox();
break;
case 'ArrowLeft':
document.querySelector('.lightbox-prev-btn').click();
break;
case 'ArrowRight':
document.querySelector('.lightbox-next-btn').click();
break;
}
}
// Build the gallery UI
return el("div", { className: "gallery-container" }).append(
// Gallery header
el("header", { className: "gallery-header" }).append(
el("h1", "Interactive Image Gallery"),
el("p", "Click on any image to view it in the lightbox. Use arrow keys for navigation.")
),
// Filter options
el("div", { className: "gallery-filters" }).append(
el("button", {
classList: { active: S(() => filterTag.get() === 'all') },
textContent: "All"
}, onFilterChange('all')),
el("button", {
classList: { active: S(() => filterTag.get() === 'nature') },
textContent: "Nature"
}, onFilterChange('nature')),
el("button", {
classList: { active: S(() => filterTag.get() === 'urban') },
textContent: "Urban"
}, onFilterChange('urban')),
el("button", {
classList: { active: S(() => filterTag.get() === 'people') },
textContent: "People"
}, onFilterChange('people'))
),
// Image grid
el("div", { className: "gallery-grid" }).append(
S.el(imagesToDisplay, images =>
images.map(image =>
memo(image.id, ()=>
el("div", {
className: "gallery-item",
dataTag: image.alt.toLowerCase()
}).append(
el("img", {
src: image.src,
alt: image.alt,
loading: "lazy"
}, onImageClick(image.id)),
el("div", { className: "gallery-item-caption" }).append(
el("h3", image.title),
el("p", image.alt)
)
)
)
)
)
),
// Lightbox (only shown when an image is selected)
S.el(isLightboxOpen, open => !open
? el()
: el("div", { className: "lightbox-overlay" }, on("click", closeLightbox)).append(
el("div", {
className: "lightbox-content",
onClick: e => e.stopPropagation() // Prevent closing when clicking inside
}).append(
el("button", {
className: "lightbox-close-btn",
"aria-label": "Close lightbox"
}, on("click", closeLightbox)).append("×"),
el("button", {
className: "lightbox-prev-btn",
"aria-label": "Previous image"
}, on("click", onPrevImage)).append(""),
el("button", {
className: "lightbox-next-btn",
"aria-label": "Next image"
}, on("click", onNextImage)).append(""),
S.el(selectedImage, img => !img
? el()
: el("div", { className: "lightbox-image-container" }).append(
el("img", {
src: img.src,
alt: img.alt,
className: "lightbox-image"
}),
el("div", { className: "lightbox-caption" }).append(
el("h2", img.title),
el("p", img.alt)
)
)
)
)
)
),
);
}
// Render the component
document.body.append(
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
el(ImageGallery)
),
el("style", `
.gallery-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.gallery-header {
text-align: center;
margin-bottom: 2rem;
}
.gallery-header h1 {
margin-bottom: 0.5rem;
color: #333;
}
.gallery-header p {
color: #666;
}
.gallery-filters {
display: flex;
justify-content: center;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.gallery-filters button {
background: none;
border: none;
padding: 0.5rem 1.5rem;
margin: 0 0.5rem;
font-size: 1rem;
cursor: pointer;
border-radius: 30px;
transition: all 0.3s ease;
color: #555;
}
.gallery-filters button:hover {
background: #f0f0f0;
}
.gallery-filters button.active {
background: #4a90e2;
color: white;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
}
.gallery-item {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.gallery-item:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
}
.gallery-item img {
width: 100%;
height: 200px;
object-fit: cover;
display: block;
transition: transform 0.5s ease;
}
.gallery-item:hover img {
transform: scale(1.05);
}
.gallery-item-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
color: white;
padding: 1rem;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.gallery-item:hover .gallery-item-caption {
transform: translateY(0);
}
.gallery-item-caption h3 {
margin: 0 0 0.5rem;
font-size: 1.2rem;
}
.gallery-item-caption p {
margin: 0;
font-size: 0.9rem;
opacity: 0.8;
}
/* Lightbox styles */
.lightbox-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 2rem;
}
.lightbox-content {
position: relative;
max-width: 90%;
max-height: 90%;
}
.lightbox-image-container {
overflow: hidden;
border-radius: 4px;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
background: #000;
}
.lightbox-image {
max-width: 100%;
max-height: 80vh;
display: block;
margin: 0 auto;
}
.lightbox-caption {
background: #222;
color: white;
padding: 1rem;
text-align: center;
}
.lightbox-caption h2 {
margin: 0 0 0.5rem;
}
.lightbox-caption p {
margin: 0;
opacity: 0.8;
}
.lightbox-close-btn,
.lightbox-prev-btn,
.lightbox-next-btn {
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
font-size: 1.5rem;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background 0.3s ease;
position: absolute;
}
.lightbox-close-btn:hover,
.lightbox-prev-btn:hover,
.lightbox-next-btn:hover {
background: rgba(0, 0, 0, 0.8);
}
.lightbox-close-btn {
top: -25px;
right: -25px;
}
.lightbox-prev-btn {
left: -25px;
top: 50%;
transform: translateY(-50%);
}
.lightbox-next-btn {
right: -25px;
top: 50%;
transform: translateY(-50%);
}
@media (max-width: 768px) {
.gallery-container {
padding: 1rem;
}
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
}
.lightbox-prev-btn,
.lightbox-next-btn {
width: 40px;
height: 40px;
font-size: 1.2rem;
}
}
`)
);

View File

@ -0,0 +1,342 @@
/**
* Case Study: Interactive Form with Validation
*
* This example demonstrates:
* - Form handling with real-time validation
* - Reactive UI updates based on input state
* - Complex form state management
* - Clean separation of concerns (data, validation, UI)
*/
import { dispatchEvent, el, on, scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
/**
* @typedef {Object} FormState
* @property {string} name
* @property {string} email
* @property {string} password
* @property {string} confirmPassword
* @property {boolean} agreedToTerms
* */
/**
* Interactive Form with Validation Component
* @returns {HTMLElement} Form element
*/
export function InteractiveForm() {
const submitted = S(false);
/** @type {FormState|null} */
let formState = null;
/** @param {CustomEvent<FormState>} event */
const onSubmit = ({ detail }) => {
submitted.set(true);
formState = detail;
};
const onAnotherAccount = () => {
submitted.set(false)
formState = null;
};
return el("div", { className: "form-container" }).append(
S.el(submitted, s => s
? el("div", { className: "success-message" }).append(
el("h3", "Thank you for registering!"),
el("p", `Welcome, ${formState.name}! Your account has been created successfully.`),
el("button", { textContent: "Register another account", type: "button" },
on("click", onAnotherAccount)
),
)
: el(Form, { initial: formState }, on("form:submit", onSubmit))
)
);
}
/**
* Form Component
* @type {(props: { initial: FormState | null }) => HTMLElement}
* */
export function Form({ initial }) {
const { host }= scope;
// Form state management
const formState = S(initial || {
name: '',
email: '',
password: '',
confirmPassword: '',
agreedToTerms: false
}, {
/**
* @template {keyof FormState} K
* @param {K} key
* @param {FormState[K]} value
* */
update(key, value) {
this.value[key] = value;
}
});
// Derived signals for validation
const nameValid = S(() => formState.get().name.length >= 3);
const emailValid = S(() => {
const email = formState.get().email;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
});
const passwordValid = S(() => {
const password = formState.get().password;
return password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password);
});
const passwordsMatch = S(() => {
const { password, confirmPassword } = formState.get();
return password === confirmPassword && confirmPassword !== '';
});
const termsAgreed = S(() => formState.get().agreedToTerms);
// Overall form validity
const formValid = S(() =>
nameValid.get() &&
emailValid.get() &&
passwordValid.get() &&
passwordsMatch.get() &&
termsAgreed.get()
);
// Event handlers
/**
* Event handler for input events
* @param {"value"|"checked"} prop
* @returns {(ev: Event) => void}
* */
const onChange= prop => ev => {
const input = /** @type {HTMLInputElement} */(ev.target);
S.action(formState, "update", /** @type {keyof FormState} */(input.id), input[prop]);
};
const dispatcSubmit = dispatchEvent("form:submit", host);
const onSubmit = on("submit", e => {
e.preventDefault();
if (!formValid.get()) {
return;
}
dispatcSubmit(formState.get());
});
// Component UI
return el("form", { className: "registration-form" }, onSubmit).append(
el("h2", "Create an Account"),
// Name field
el("div", { classList: {
"form-group": true,
valid: nameValid,
invalid: S(()=> !nameValid.get() && formState.get().name)
}}).append(
el("label", { htmlFor: "name", textContent: "Full Name" }),
el("input", {
id: "name",
type: "text",
value: formState.get().name,
placeholder: "Enter your full name"
}, on("input", onChange("value"))),
el("div", { className: "validation-message", textContent: "Name must be at least 3 characters long" }),
),
// Email field
el("div", { classList: {
"form-group": true,
valid: emailValid,
invalid: S(()=> !emailValid.get() && formState.get().email)
}}).append(
el("label", { htmlFor: "email", textContent: "Email Address" }),
el("input", {
id: "email",
type: "email",
value: formState.get().email,
placeholder: "Enter your email address"
}, on("input", onChange("value"))),
el("div", { className: "validation-message", textContent: "Please enter a valid email address" })
),
// Password field
el("div", { classList: {
"form-group": true,
valid: passwordValid,
invalid: S(()=> !passwordValid.get() && formState.get().password)
}}).append(
el("label", { htmlFor: "password", textContent: "Password" }),
el("input", {
id: "password",
type: "password",
value: formState.get().password,
placeholder: "Create a password"
}, on("input", onChange("value"))),
el("div", {
className: "validation-message",
textContent: "Password must be at least 8 characters with at least one uppercase letter and one number",
}),
),
// Confirm password field
el("div", { classList: {
"form-group": true,
valid: passwordsMatch,
invalid: S(()=> !passwordsMatch.get() && formState.get().confirmPassword)
}}).append(
el("label", { htmlFor: "confirmPassword", textContent: "Confirm Password" }),
el("input", {
id: "confirmPassword",
type: "password",
value: formState.get().confirmPassword,
placeholder: "Confirm your password"
}, on("input", onChange("value"))),
el("div", { className: "validation-message", textContent: "Passwords must match" }),
),
// Terms agreement
el("div", { className: "form-group checkbox-group" }).append(
el("input", {
id: "agreedToTerms",
type: "checkbox",
checked: formState.get().agreedToTerms
}, on("change", onChange("checked"))),
el("label", { htmlFor: "agreedToTerms", textContent: "I agree to the Terms and Conditions" }),
),
// Submit button
el("button", {
textContent: "Create Account",
type: "submit",
className: "submit-button",
disabled: S(() => !formValid.get())
}),
);
}
// Render the component
document.body.append(
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
el(InteractiveForm)
),
el("style", `
.form-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 500px;
margin: 0 auto;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
background: #fff;
}
h2 {
margin-top: 0;
color: #333;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
position: relative;
transition: all 0.3s ease;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #555;
font-weight: 500;
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
input:focus {
outline: none;
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}
.checkbox-group {
display: flex;
align-items: center;
}
.checkbox-group label {
margin: 0 0 0 0.5rem;
}
.validation-message {
font-size: 0.85rem;
color: #e74c3c;
margin-top: 0.5rem;
height: 0;
overflow: hidden;
opacity: 0;
transition: all 0.3s ease;
}
.form-group.invalid .validation-message {
height: auto;
opacity: 1;
}
.form-group.valid input {
border-color: #2ecc71;
}
.form-group.invalid input {
border-color: #e74c3c;
}
.submit-button {
background-color: #4a90e2;
color: white;
border: none;
border-radius: 4px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
width: 100%;
}
.submit-button:hover:not(:disabled) {
background-color: #3a7bc8;
}
.submit-button:disabled {
background-color: #b5b5b5;
cursor: not-allowed;
}
.success-message {
text-align: center;
color: #2ecc71;
}
.success-message h3 {
margin-top: 0;
}
.success-message button {
background-color: #2ecc71;
color: white;
border: none;
border-radius: 4px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
margin-top: 1rem;
}
.success-message button:hover {
background-color: #27ae60;
}
`),
);

View File

@ -0,0 +1,717 @@
/**
* Case Study: Task Manager Application
*
* This example demonstrates:
* - Complex state management with signals
* - Drag and drop functionality
* - Local storage persistence
* - Responsive design for different devices
*/
import { el, on } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
/**
* Task Manager Component
* @returns {HTMLElement} Task manager UI
*/
export function TaskManager() {
// Local storage key
const STORAGE_KEY = 'dde-task-manager';
// Task statuses
const STATUSES = {
TODO: 'todo',
IN_PROGRESS: 'in-progress',
DONE: 'done'
};
// Initial tasks from localStorage or defaults
let initialTasks = [];
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
initialTasks = JSON.parse(saved);
}
} catch (e) {
console.error('Failed to load tasks from localStorage', e);
}
if (!initialTasks.length) {
// Default tasks if nothing in localStorage
initialTasks = [
{ id: 1, title: 'Create project structure', description: 'Set up folders and initial files', status: STATUSES.DONE, priority: 'high' },
{ id: 2, title: 'Design UI components', description: 'Create mockups for main views', status: STATUSES.IN_PROGRESS, priority: 'medium' },
{ id: 3, title: 'Implement authentication', description: 'Set up user login and registration', status: STATUSES.TODO, priority: 'high' },
{ id: 4, title: 'Write documentation', description: 'Document API endpoints and usage examples', status: STATUSES.TODO, priority: 'low' },
];
}
// Application state
const tasks = S(initialTasks, {
add(task) { this.value.push(task); },
remove(id) { this.value = this.value.filter(task => task.id !== id); },
update(id, task) {
const current= this.value.find(t => t.id === id);
if (current) Object.assign(current, task);
// TODO: known bug for derived signals (the object is the same)
// so filteredTasks is not updated, hotfix ↙
this.value = structuredClone(this.value);
}
});
const newTaskTitle = S('');
const newTaskDescription = S('');
const newTaskPriority = S('medium');
const editingTaskId = S(null);
const draggedTaskId = S(null);
const filterPriority = S('all');
const searchQuery = S('');
// Save tasks to localStorage whenever they change
S.on(tasks, value => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
} catch (e) {
console.error('Failed to save tasks to localStorage', e);
}
});
// Filtered tasks based on priority and search query
const filteredTasks = S(() => {
let filtered = tasks.get();
// Filter by priority
if (filterPriority.get() !== 'all') {
filtered = filtered.filter(task => task.priority === filterPriority.get());
}
// Filter by search query
const query = searchQuery.get().toLowerCase();
if (query) {
filtered = filtered.filter(task =>
task.title.toLowerCase().includes(query) ||
task.description.toLowerCase().includes(query)
);
}
return filtered;
});
// Tasks grouped by status for display in columns
const tasksByStatus = S(() => {
const filtered = filteredTasks.get();
return {
[STATUSES.TODO]: filtered.filter(t => t.status === STATUSES.TODO),
[STATUSES.IN_PROGRESS]: filtered.filter(t => t.status === STATUSES.IN_PROGRESS),
[STATUSES.DONE]: filtered.filter(t => t.status === STATUSES.DONE)
};
});
// Event handlers
const onAddTask = e => {
e.preventDefault();
const title = newTaskTitle.get().trim();
if (!title) return;
const newTask = {
id: Date.now(),
title,
description: newTaskDescription.get().trim(),
status: STATUSES.TODO,
priority: newTaskPriority.get()
};
S.action(tasks, "add", newTask);
newTaskTitle.set('');
newTaskDescription.set('');
newTaskPriority.set('medium');
};
const onDeleteTask = id => e => {
e.stopPropagation();
S.action(tasks, "remove", id);
};
const onEditStart = id => () => {
editingTaskId.set(id);
};
const onEditCancel = () => {
editingTaskId.set(null);
};
const onEditSave = id => e => {
e.preventDefault();
const form = e.target;
const title = form.elements.title.value.trim();
if (!title) return;
tasks.set(tasks.get().map(task =>
task.id === id
? {
...task,
title,
description: form.elements.description.value.trim(),
priority: form.elements.priority.value
}
: task
));
editingTaskId.set(null);
};
function onDragable(id) {
return element => {
on("dragstart", e => {
draggedTaskId.set(id);
e.dataTransfer.effectAllowed = 'move';
// Add some styling to the element being dragged
setTimeout(() => {
const el = document.getElementById(`task-${id}`);
if (el) el.classList.add('dragging');
}, 0);
})(element);
on("dragend", () => {
draggedTaskId.set(null);
// Remove the styling
const el = document.getElementById(`task-${id}`);
if (el) el.classList.remove('dragging');
})(element);
};
}
function onDragArea(status) {
return element => {
on("dragover", e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
// Add a visual indicator for the drop target
const column = document.getElementById(`column-${status}`);
if (column) column.classList.add('drag-over');
})(element);
on("dragleave", () => {
// Remove the visual indicator
const column = document.getElementById(`column-${status}`);
if (column) column.classList.remove('drag-over');
})(element);
on("drop", e => {
e.preventDefault();
const id = draggedTaskId.get();
if (id) S.action(tasks, "update", id, { status });
// Remove the visual indicator
const column = document.getElementById(`column-${status}`);
if (column) column.classList.remove('drag-over');
})(element);
};
}
// Helper function to render a task card
function renderTaskCard(task) {
const isEditing = S(() => editingTaskId.get() === task.id);
return el("div", {
id: `task-${task.id}`,
className: `task-card priority-${task.priority}`,
draggable: true
}, onDragable(task.id)).append(
S.el(isEditing, editing => editing
// Edit mode
? el("form", { className: "task-edit-form" }, on("submit", onEditSave(task.id))).append(
el("input", {
name: "title",
className: "task-title-input",
defaultValue: task.title,
placeholder: "Task title",
required: true,
autoFocus: true
}),
el("textarea", {
name: "description",
className: "task-desc-input",
defaultValue: task.description,
placeholder: "Description (optional)"
}),
el("select", {
name: "priority",
}, on.host(el=> el.value = task.priority)).append(
el("option", { value: "low", textContent: "Low Priority" }),
el("option", { value: "medium", textContent: "Medium Priority" }),
el("option", { value: "high", textContent: "High Priority" })
),
el("div", { className: "task-edit-actions" }).append(
el("button", {
type: "button",
className: "cancel-btn"
}, on("click", onEditCancel)).append("Cancel"),
el("button", {
type: "submit",
className: "save-btn"
}).append("Save")
)
)
// View mode
: el().append(
el("div", { className: "task-header" }).append(
el("h3", { className: "task-title", textContent: task.title }),
el("div", { className: "task-actions" }).append(
el("button", {
className: "edit-btn",
"aria-label": "Edit task"
}, on("click", onEditStart(task.id))).append("✎"),
el("button", {
className: "delete-btn",
"aria-label": "Delete task"
}, on("click", onDeleteTask(task.id))).append("×")
)
),
task.description
? el("p", { className: "task-description", textContent: task.description })
: el(),
el("div", { className: "task-meta" }).append(
el("span", {
className: `priority-badge priority-${task.priority}`,
textContent: task.priority.charAt(0).toUpperCase() + task.priority.slice(1)
})
)
)
)
);
}
// Build the task manager UI
return el("div", { className: "task-manager" }).append(
el("header", { className: "app-header" }).append(
el("h1", "DDE Task Manager"),
el("div", { className: "app-controls" }).append(
el("input", {
type: "text",
placeholder: "Search tasks...",
value: searchQuery.get()
}, on("input", e => searchQuery.set(e.target.value))),
el("select", null,
on.host(el=> el.value= filterPriority.get()),
on("change", e => filterPriority.set(e.target.value))
).append(
el("option", { value: "all", textContent: "All Priorities" }),
el("option", { value: "low", textContent: "Low Priority" }),
el("option", { value: "medium", textContent: "Medium Priority" }),
el("option", { value: "high", textContent: "High Priority" })
)
)
),
// Add new task form
el("form", { className: "new-task-form" }, on("submit", onAddTask)).append(
el("div", { className: "form-row" }).append(
el("input", {
type: "text",
placeholder: "New task title",
value: newTaskTitle.get(),
required: true
}, on("input", e => newTaskTitle.set(e.target.value))),
el("select", null,
on.host(el=> el.value= newTaskPriority.get()),
on("change", e => newTaskPriority.set(e.target.value))
).append(
el("option", { value: "low", textContent: "Low" }),
el("option", { value: "medium", textContent: "Medium" }),
el("option", { value: "high", textContent: "High" })
),
el("button", { type: "submit", className: "add-btn" }).append("Add Task")
),
el("textarea", {
placeholder: "Task description (optional)",
value: newTaskDescription.get()
}, on("input", e => newTaskDescription.set(e.target.value)))
),
// Task board with columns
el("div", { className: "task-board" }).append(
// Todo column
el("div", {
id: `column-${STATUSES.TODO}`,
className: "task-column"
}, onDragArea(STATUSES.TODO)).append(
el("h2", { className: "column-header" }).append(
"To Do ",
el("span", { className: "task-count" }).append(
S(() => tasksByStatus.get()[STATUSES.TODO].length)
)
),
S.el(S(() => tasksByStatus.get()[STATUSES.TODO]), tasks =>
el("div", { className: "column-tasks" }).append(
...tasks.map(renderTaskCard)
)
)
),
// In Progress column
el("div", {
id: `column-${STATUSES.IN_PROGRESS}`,
className: "task-column"
}, onDragArea(STATUSES.IN_PROGRESS)).append(
el("h2", { className: "column-header" }).append(
"In Progress ",
el("span", { className: "task-count" }).append(
S(() => tasksByStatus.get()[STATUSES.IN_PROGRESS].length)
)
),
S.el(S(() => tasksByStatus.get()[STATUSES.IN_PROGRESS]), tasks =>
el("div", { className: "column-tasks" }).append(
...tasks.map(renderTaskCard)
)
)
),
// Done column
el("div", {
id: `column-${STATUSES.DONE}`,
className: "task-column"
}, onDragArea(STATUSES.DONE)).append(
el("h2", { className: "column-header" }).append(
"Done ",
el("span", { className: "task-count" }).append(
S(() => tasksByStatus.get()[STATUSES.DONE].length)
)
),
S.el(S(() => tasksByStatus.get()[STATUSES.DONE]), tasks =>
el("div", { className: "column-tasks" }).append(
...tasks.map(renderTaskCard)
)
)
)
),
);
}
// Render the component
document.body.append(
el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append(
el(TaskManager)
),
el("style", `
.task-manager {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
color: #333;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.app-header h1 {
margin: 0;
color: #2d3748;
}
.app-controls {
display: flex;
gap: 1rem;
}
.app-controls input,
.app-controls select {
padding: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 0.9rem;
}
.new-task-form {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
margin-bottom: 2rem;
}
.form-row {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.form-row input {
flex-grow: 1;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 1rem;
}
.form-row select {
width: 100px;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 1rem;
}
.add-btn {
background: #4a90e2;
color: white;
border: none;
border-radius: 4px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s ease;
}
.add-btn:hover {
background: #3a7bc8;
}
.new-task-form textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 1rem;
resize: vertical;
min-height: 80px;
}
.task-board {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.task-column {
background: #f7fafc;
border-radius: 8px;
padding: 1rem;
min-height: 400px;
transition: background 0.2s ease;
}
.column-header {
margin-top: 0;
padding-bottom: 0.75rem;
border-bottom: 2px solid #e2e8f0;
font-size: 1.25rem;
color: #2d3748;
display: flex;
align-items: center;
}
.task-count {
display: inline-flex;
justify-content: center;
align-items: center;
background: #e2e8f0;
color: #4a5568;
border-radius: 50%;
width: 25px;
height: 25px;
font-size: 0.875rem;
margin-left: 0.5rem;
}
.column-tasks {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
min-height: 200px;
}
.task-card {
background: white;
border-radius: 6px;
padding: 1rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
cursor: grab;
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
border-left: 4px solid #ccc;
}
.task-card:hover {
transform: translateY(-3px);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
}
.task-card.dragging {
opacity: 0.5;
cursor: grabbing;
}
.task-card.priority-low {
border-left-color: #38b2ac;
}
.task-card.priority-medium {
border-left-color: #ecc94b;
}
.task-card.priority-high {
border-left-color: #e53e3e;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.task-title {
margin: 0;
font-size: 1.1rem;
color: #2d3748;
word-break: break-word;
}
.task-description {
margin: 0.5rem 0;
font-size: 0.9rem;
color: #4a5568;
word-break: break-word;
}
.task-actions {
display: flex;
gap: 0.5rem;
}
.edit-btn,
.delete-btn {
background: none;
border: none;
font-size: 1rem;
cursor: pointer;
width: 24px;
height: 24px;
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 50%;
color: #718096;
transition: background 0.2s ease, color 0.2s ease;
}
.edit-btn:hover {
background: #edf2f7;
color: #4a5568;
}
.delete-btn:hover {
background: #fed7d7;
color: #e53e3e;
}
.task-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.75rem;
}
.priority-badge {
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: 500;
}
.priority-badge.priority-low {
background: #e6fffa;
color: #2c7a7b;
}
.priority-badge.priority-medium {
background: #fefcbf;
color: #975a16;
}
.priority-badge.priority-high {
background: #fed7d7;
color: #c53030;
}
.drag-over {
background: #f0f9ff;
border: 2px dashed #4a90e2;
}
.task-edit-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.task-title-input,
.task-desc-input {
width: 100%;
padding: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 0.9rem;
}
.task-desc-input {
min-height: 60px;
resize: vertical;
}
.task-edit-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
}
.cancel-btn,
.save-btn {
padding: 0.4rem 0.75rem;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
}
.cancel-btn {
background: #edf2f7;
color: #4a5568;
border: 1px solid #e2e8f0;
}
.save-btn {
background: #4a90e2;
color: white;
border: none;
}
@media (max-width: 768px) {
.app-header {
flex-direction: column;
align-items: flex-start;
}
.app-controls {
width: 100%;
flex-direction: column;
}
.form-row {
flex-direction: column;
}
.task-board {
grid-template-columns: 1fr;
}
}
`)
);

63
docs/p15-examples.html.js Normal file
View File

@ -0,0 +1,63 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Examples Gallery`,
fullTitle: t`DDE Examples & Code Snippets`,
description: t`A comprehensive collection of examples and code snippets for working with Deka DOM Elements.`,
};
import { el } from "deka-dom-el";
import { simplePage } from "./layout/simplePage.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { example } from "./components/example.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("p").append(T`
Real-world application examples showcasing how to build complete, production-ready interfaces with dd<el>:
`),
el(h3, t`Data Dashboard`),
el("p").append(T`
Data visualization dashboard with charts, filters, and responsive layout. Integration with a
third-party charting library, data fetching and state management, responsive layout design, and multiple
interactive components working together.
`),
el(example, { src: fileURL("./components/examples/case-studies/data-dashboard.js"), variant: "big", page_id }),
el(h3, t`Interactive Form`),
el("p").append(T`
Complete form with real-time validation, conditional rendering, and responsive design. Form handling with
real-time validation, reactive UI updates, complex form state management, and clean separation of concerns.
`),
el(example, { src: fileURL("./components/examples/case-studies/interactive-form.js"), variant: "big", page_id }),
el(h3, t`Interactive Image Gallery`),
el("p").append(T`
Responsive image gallery with lightbox, keyboard navigation, and filtering. Dynamic loading of content,
lightbox functionality, animation handling, and keyboard and gesture navigation support.
`),
el(example, { src: fileURL("./components/examples/case-studies/image-gallery.js"), variant: "big", page_id }),
el(h3, t`Task Manager`),
el("p").append(T`
Kanban-style task management app with drag-and-drop and localStorage persistence. Complex state management
with signals, drag and drop functionality, local storage persistence, and responsive design for different
devices.
`),
el(example, { src: fileURL("./components/examples/case-studies/task-manager.js"), variant: "big", page_id }),
el(h3, t`TodoMVC`),
el("p").append(T`
Complete TodoMVC implementation with local storage and routing. TodoMVC implementation showing routing,
local storage persistence, filtering, and component architecture patterns. For commented code, see the
dedicated page ${el("a", { href: "./p10-todomvc.html" }).append(T`TodoMVC`)}.
`),
);
}