mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-07-01 12:22:15 +02:00
⚡ 🐛 🔤 v0.9.3-alpha (#40)
* 🔤 * ⚡ Replaces defined with name/host * 🐛 __dde_reactive * ⚡ v0.9.3 * 🔤 examples+best paractises * 🐛 📺 fixes npm run docs * ⚡ finalizes v0.9.3-alpha * 🔤 📺 coc tabs * 🔤
This commit is contained in:
@ -23,14 +23,6 @@ export function DataDashboard() {
|
||||
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' },
|
||||
@ -38,42 +30,32 @@ export function DataDashboard() {
|
||||
{ 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
|
||||
// Filter options
|
||||
const selectedYear = S(2024);
|
||||
const onYearChange = on("change", e => {
|
||||
selectedYear.set(parseInt(/** @type {HTMLSelectElement} */(e.target).value));
|
||||
loadData();
|
||||
});
|
||||
|
||||
const selectedDataType = S(/** @type {'sales' | 'visitors' | 'conversion'} */ ('sales'));
|
||||
const onDataTypeChange = on("click", e => {
|
||||
const type = /** @type {'sales' | 'visitors' | 'conversion'} */(
|
||||
/** @type {HTMLButtonElement} */(e.currentTarget).dataset.type);
|
||||
selectedDataType.set(type);
|
||||
});
|
||||
const currentDataType = S(() => dataTypes.find(type => type.id === selectedDataType.get()));
|
||||
const selectedData = S(() => DATA[selectedDataType.get()]);
|
||||
|
||||
// Values based on filters
|
||||
const totalValue = S(() => selectedData.get().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(() => Math.max(...selectedData.get()));
|
||||
|
||||
// Simulate data loading
|
||||
const isLoading = S(false);
|
||||
const error = S(null);
|
||||
function loadData() {
|
||||
isLoading.set(true);
|
||||
error.set(null);
|
||||
@ -114,7 +96,7 @@ export function DataDashboard() {
|
||||
// Draw grid labels
|
||||
ctx.fillStyle = '#999';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillText(Math.round(maxValue * (i / 5)), 20, y + 5);
|
||||
ctx.fillText(Math.round(maxValue * (i / 5)).toString(), 20, y + 5);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
@ -154,7 +136,6 @@ export function DataDashboard() {
|
||||
)
|
||||
),
|
||||
|
||||
// Error message (only shown when there's an error)
|
||||
S.el(error, errorMsg => !errorMsg
|
||||
? el()
|
||||
: el("div", { className: "error-message" }).append(
|
||||
@ -163,7 +144,6 @@ export function DataDashboard() {
|
||||
),
|
||||
),
|
||||
|
||||
// Loading indicator
|
||||
S.el(isLoading, loading => !loading
|
||||
? el()
|
||||
: el("div", { className: "loading-spinner" })
|
||||
|
@ -27,25 +27,23 @@ const imagesSample = (url=> [
|
||||
* @returns {HTMLElement} Gallery element
|
||||
*/
|
||||
export function ImageGallery(images= imagesSample) {
|
||||
|
||||
// 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);
|
||||
})
|
||||
const onFilterChange = tag => on("click", () => {
|
||||
filterTag.set(tag);
|
||||
});
|
||||
|
||||
// Derived state
|
||||
// Lightbox
|
||||
const selectedImageId = S(null);
|
||||
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
|
||||
@ -76,9 +74,6 @@ export function ImageGallery(images= imagesSample) {
|
||||
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) {
|
||||
|
@ -73,8 +73,17 @@ export function Form({ initial }) {
|
||||
this.value[key] = value;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 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]);
|
||||
};
|
||||
|
||||
// Derived signals for validation
|
||||
// Form validate state
|
||||
const nameValid = S(() => formState.get().name.length >= 3);
|
||||
const emailValid = S(() => {
|
||||
const email = formState.get().email;
|
||||
@ -89,8 +98,6 @@ export function Form({ initial }) {
|
||||
return password === confirmPassword && confirmPassword !== '';
|
||||
});
|
||||
const termsAgreed = S(() => formState.get().agreedToTerms);
|
||||
|
||||
// Overall form validity
|
||||
const formValid = S(() =>
|
||||
nameValid.get() &&
|
||||
emailValid.get() &&
|
||||
@ -99,16 +106,6 @@ export function Form({ initial }) {
|
||||
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();
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Example of reactive element marker
|
||||
<!--<dde:mark type="reactive" source="...">-->
|
||||
<!--<dde:mark type="reactive" component="<component-name>">-->
|
||||
<!-- content that updates when signal changes -->
|
||||
<!--</dde:mark>-->
|
||||
|
@ -9,7 +9,7 @@ export function mnemonic(){
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "el(<tag-name>, <primitive>)[.append(...)]: <element-from-tag-name>"),
|
||||
" — simple element containing only text",
|
||||
" — simple element containing only text (accepts string, number or signal)",
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "el(<tag-name>, <object>)[.append(...)]: <element-from-tag-name>"),
|
||||
@ -26,6 +26,6 @@ export function mnemonic(){
|
||||
el("li").append(
|
||||
el("code", "elNS(<namespace>)(<as-el-see-above>)[.append(...)]: <element-based-on-arguments>"),
|
||||
" — typically SVG elements",
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export function mnemonic(){
|
||||
el("li").append(
|
||||
el("code", "S.clear(...<signals>)"),
|
||||
" — off and clear signals (most of the time it is not needed as reactive ",
|
||||
"attributes and elements are cleared automatically)",
|
||||
"attributes and elements are handled automatically)",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ export function page({ pkg, info }){
|
||||
|
||||
el("p").append(T`
|
||||
By separating these concerns, your code becomes more modular, testable, and easier to maintain. This
|
||||
approach ${el("strong", "is not")} something new and/or special to dd<el>. It’s based on ${el("a", {
|
||||
approach ${el("strong", "is not something new and/or special to dd<el>")}. It’s based on ${el("a", {
|
||||
textContent: "MVC", ...references.w_mvc })} (${el("a", { textContent: "MVVM", ...references.w_mvv })}),
|
||||
but is there presented in simpler form.
|
||||
`),
|
||||
@ -105,7 +105,7 @@ export function page({ pkg, info }){
|
||||
or directly include it from a CDN for quick prototyping.
|
||||
`),
|
||||
el("h4", "npm installation"),
|
||||
el(code, { content: "npm install deka-dom-el # Coming soon", language: "shell", page_id }),
|
||||
el(code, { content: "npm install deka-dom-el --save", language: "shell", page_id }),
|
||||
el("h4", "CDN / Direct Script Usage"),
|
||||
el("p").append(T`
|
||||
Use the interactive selector below to choose your preferred format:
|
||||
@ -154,6 +154,10 @@ export function page({ pkg, info }){
|
||||
Interactive demos with server-side pre-rendering`),
|
||||
el("li").append(T`${el("a", { href: "p13-appendix.html" }).append(el("strong", "Appendix & Summary"))} —
|
||||
Comprehensive reference and best practices`),
|
||||
el("li").append(T`${el("a", { href: "p14-converter.html" }).append(el("strong", "HTML Converter"))} —
|
||||
Convert HTML to dd<el> JavaScript code`),
|
||||
el("li").append(T`${el("a", { href: "p15-examples.html" }).append(el("strong", "Examples Gallery"))} —
|
||||
Real-world application examples and case studies`),
|
||||
),
|
||||
el("p").append(T`
|
||||
Each section builds on the previous ones, so we recommend following them in order.
|
||||
|
@ -208,6 +208,6 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
),
|
||||
|
||||
el(mnemonic)
|
||||
el(mnemonic),
|
||||
);
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ export function page({ pkg, info }){
|
||||
el("li", t`Set up lifecycle behaviors`),
|
||||
el("li", t`Integrate third-party libraries`),
|
||||
el("li", t`Create reusable element behaviors`),
|
||||
el("li", t`Capture element references`)
|
||||
el("li", t`Capture element references`), // TODO: add example?
|
||||
)
|
||||
),
|
||||
el("p").append(T`
|
||||
|
@ -277,7 +277,72 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el("li").append(T`
|
||||
${el("strong", "Avoid infinite loops")}: Be careful when one signal updates another in a subscription
|
||||
`)
|
||||
`),
|
||||
),
|
||||
el("p").append(T`
|
||||
While signals provide powerful reactivity for complex UI interactions, they’re not always necessary.
|
||||
A good approach is to started with variables/constants and when necessary, convert them to signals.
|
||||
`),
|
||||
|
||||
el("div", { className: "tabs" }).append(
|
||||
el("div", { className: "tab", dataTab: "events" }).append(
|
||||
el("h4", t`We can process form events without signals`),
|
||||
el("p", t`This can be used when the form data doesn’t need to be reactive and we just waiting for
|
||||
results.`),
|
||||
el(code, { page_id, content: `
|
||||
const onFormSubmit = on("submit", e => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
// this can be sent to a server
|
||||
// or processed locally
|
||||
// e.g.: console.log(Object.fromEntries(formData))
|
||||
});
|
||||
// …
|
||||
return el("form", null, onFormSubmit).append(
|
||||
// …
|
||||
);
|
||||
` })
|
||||
),
|
||||
|
||||
el("div", { className: "tab", dataTab: "variables" }).append(
|
||||
el("h4", t`We can use variables without signals`),
|
||||
el("p", t`We use this when we dont’t need to reflect changes in the elsewhere (UI).`),
|
||||
el(code, { page_id, content: `
|
||||
let canSubmit = false;
|
||||
|
||||
const onFormSubmit = on("submit", e => {
|
||||
e.preventDefault();
|
||||
if(!canSubmit) return; // some message
|
||||
// …
|
||||
});
|
||||
const onAllowSubmit = on("click", e => {
|
||||
canSubmit = true;
|
||||
});
|
||||
`}),
|
||||
),
|
||||
|
||||
el("div", { className: "tab", dataTab: "state" }).append(
|
||||
el("h4", t`Using signals`),
|
||||
el("p", t`We use this when we need to reflect changes for example in the UI (e.g. enable/disable
|
||||
buttons).`),
|
||||
el(code, { page_id, content: `
|
||||
const canSubmit = S(false);
|
||||
|
||||
const onFormSubmit = on("submit", e => {
|
||||
e.preventDefault();
|
||||
// …
|
||||
});
|
||||
const onAllowSubmit = on("click", e => {
|
||||
canSubmit.set(true);
|
||||
});
|
||||
|
||||
return el("form", null, onFormSubmit).append(
|
||||
// ...
|
||||
el("button", { textContent: "Allow Submit", type: "button" }, onAllowSubmit),
|
||||
el("button", { disabled: S(()=> !canSubmit), textContent: "Submit" })
|
||||
);
|
||||
`}),
|
||||
),
|
||||
),
|
||||
|
||||
el("div", { className: "troubleshooting" }).append(
|
||||
@ -298,6 +363,6 @@ export function page({ pkg, info }){
|
||||
)
|
||||
),
|
||||
|
||||
el(mnemonic)
|
||||
el(mnemonic),
|
||||
);
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ export function page({ pkg, info }){
|
||||
el(MyComponent);
|
||||
|
||||
function MyComponent() {
|
||||
// 2. access the host element
|
||||
// 2. access the host element (or other scope related values)
|
||||
const { host } = scope;
|
||||
|
||||
// 3. Add behavior to host
|
||||
|
@ -80,8 +80,7 @@ export function page({ pkg, info }){
|
||||
el("li", t`listeners: A Set of functions called when the signal value changes`),
|
||||
el("li", t`actions: Custom actions that can be performed on the signal`),
|
||||
el("li", t`onclear: Functions to run when the signal is cleared`),
|
||||
el("li", t`host: Reference to the host element/scope`),
|
||||
el("li", t`defined: Stack trace information for debugging`),
|
||||
el("li", t`host: Reference to the host element/scope in which the signal was created`),
|
||||
el("li", t`readonly: Boolean flag indicating if the signal is read-only`)
|
||||
),
|
||||
el("p").append(T`
|
||||
@ -114,7 +113,13 @@ export function page({ pkg, info }){
|
||||
el("ul").append(
|
||||
el("li", t`That you’re using signal.set() to update the value, not modifying objects/arrays directly`),
|
||||
el("li", t`For mutable objects, ensure you’re using actions or making proper copies before updating`),
|
||||
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding code)`)
|
||||
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding
|
||||
code)`),
|
||||
el("li").append(T`
|
||||
That you’re passing signal corecctly (without using ${el("code", "*.get()")}) and for ${el("code",
|
||||
"S.el")} that you passing (derived) signals not a function (use ${el("code",
|
||||
"S.el(S(()=> count.get() % 2), odd=> …)")}).
|
||||
`),
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/debugging/mutations.js"), page_id }),
|
||||
|
||||
|
@ -129,75 +129,6 @@ export function page({ pkg, info }){
|
||||
`)
|
||||
),
|
||||
|
||||
el("h4", t`Using Signals Appropriately`),
|
||||
el("p").append(T`
|
||||
While signals provide powerful reactivity for complex UI interactions, they’re not always necessary.
|
||||
`),
|
||||
|
||||
el("div", { className: "tabs" }).append(
|
||||
el("div", { className: "tab", dataTab: "events" }).append(
|
||||
el("h4", t`We can process form events without signals`),
|
||||
el("p", t`This can be used when the form data doesn’t need to be reactive and we just waiting for
|
||||
results.`),
|
||||
el(code, { page_id, content: `
|
||||
const onFormSubmit = on("submit", e => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
// this can be sent to a server
|
||||
// or processed locally
|
||||
// e.g.: console.log(Object.fromEntries(formData))
|
||||
});
|
||||
// …
|
||||
return el("form", null, onFormSubmit).append(
|
||||
// …
|
||||
);
|
||||
` })
|
||||
),
|
||||
|
||||
el("div", { className: "tab", dataTab: "variables" }).append(
|
||||
el("h4", t`We can use variables without signals`),
|
||||
el("p", t`We use this when we dont’t need to reflect changes in the elsewhere (UI).`),
|
||||
el(code, { page_id, content: `
|
||||
let canSubmit = false;
|
||||
|
||||
const onFormSubmit = on("submit", e => {
|
||||
e.preventDefault();
|
||||
if(!canSubmit) return; // some message
|
||||
// …
|
||||
});
|
||||
const onAllowSubmit = on("click", e => {
|
||||
canSubmit = true;
|
||||
});
|
||||
`}),
|
||||
),
|
||||
|
||||
el("div", { className: "tab", dataTab: "state" }).append(
|
||||
el("h4", t`Using signals`),
|
||||
el("p", t`We use this when we need to reflect changes for example in the UI (e.g. enable/disable
|
||||
buttons).`),
|
||||
el(code, { page_id, content: `
|
||||
const canSubmit = S(false);
|
||||
|
||||
const onFormSubmit = on("submit", e => {
|
||||
e.preventDefault();
|
||||
// …
|
||||
});
|
||||
const onAllowSubmit = on("click", e => {
|
||||
canSubmit.set(true);
|
||||
});
|
||||
|
||||
return el("form", null, onFormSubmit).append(
|
||||
// ...
|
||||
el("button", { textContent: "Allow Submit", type: "button" }, onAllowSubmit),
|
||||
el("button", { disabled: S(()=> !canSubmit), textContent: "Submit" })
|
||||
);
|
||||
`}),
|
||||
),
|
||||
el("p").append(T`
|
||||
A good approach is to started with variables/constants and when necessary, convert them to signals.
|
||||
`),
|
||||
),
|
||||
|
||||
el("h4", t`Migrating from Traditional Approaches`),
|
||||
el("p").append(T`
|
||||
When migrating from traditional DOM manipulation or other frameworks to dd<el>:
|
||||
@ -394,46 +325,46 @@ export function page({ pkg, info }){
|
||||
el("tr").append(
|
||||
el("th", "Feature"),
|
||||
el("th", "dd<el>"),
|
||||
el("th", "React"),
|
||||
el("th", "Vue"),
|
||||
el("th", "Svelte")
|
||||
el("th", "VanJS"),
|
||||
el("th", "Solid"),
|
||||
el("th", "Alpine")
|
||||
)
|
||||
),
|
||||
el("tbody").append(
|
||||
el("tr").append(
|
||||
el("td", "No Build Step Required"),
|
||||
el("td", "✅"),
|
||||
el("td", "✅"),
|
||||
el("td", "⚠️ JSX needs transpilation"),
|
||||
el("td", "⚠️ SFC needs compilation"),
|
||||
el("td", "❌ Requires compilation")
|
||||
el("td", "✅")
|
||||
),
|
||||
el("tr").append(
|
||||
el("td", "Bundle Size (minimal)"),
|
||||
el("td", "~10-15kb"),
|
||||
el("td", "~40kb+"),
|
||||
el("td", "~33kb+"),
|
||||
el("td", "Minimal runtime")
|
||||
el("td", "Bundle Size (minified)"),
|
||||
el("td", "~14kb"),
|
||||
el("td", "~3kb"),
|
||||
el("td", "~20kb"),
|
||||
el("td", "~43kb")
|
||||
),
|
||||
el("tr").append(
|
||||
el("td", "Reactivity Model"),
|
||||
el("td", "Signal-based"),
|
||||
el("td", "Virtual DOM diffing"),
|
||||
el("td", "Proxy-based"),
|
||||
el("td", "Compile-time reactivity")
|
||||
el("td", "Signal-based (basics only)"),
|
||||
el("td", "Signal-based"),
|
||||
el("td", "MVVM + Proxy")
|
||||
),
|
||||
el("tr").append(
|
||||
el("td", "DOM Interface"),
|
||||
el("td", "Direct DOM API"),
|
||||
el("td", "Virtual DOM"),
|
||||
el("td", "Virtual DOM"),
|
||||
el("td", "Compiled DOM updates")
|
||||
el("td", "Direct DOM API"),
|
||||
el("td", "Compiled DOM updates"),
|
||||
el("td", "Directive-based")
|
||||
),
|
||||
el("tr").append(
|
||||
el("td", "Server-Side Rendering"),
|
||||
el("td", "✅ Basic Support"),
|
||||
el("td", "✅ Basic Support"),
|
||||
el("td", "✅ Advanced"),
|
||||
el("td", "✅ Advanced"),
|
||||
el("td", "✅ Advanced")
|
||||
el("td", "❌")
|
||||
)
|
||||
)
|
||||
),
|
||||
|
Reference in New Issue
Block a user