mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-07-01 04:12:14 +02:00
⚡ 🐛 🔤 v0.9.4-alpha (#42)
* 🐛 fixes #41 * ⚡ adjust package size limits * 🔤 * 📺 requestIdleCallback doesn need to be global * 🔤 corrects irland page headers * 📺 version * ⚡ Signal ← SignalReadonly * 🐛 ensures only one disconncetd listener …for cleanup * ⚡ 🔤 Better build and improve texting * 🐛 logo alignemt (due to gh) * 🔤 md enhancements * 🔤 ⚡ products
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { registerClientFile, styles } from "../ssr.js";
|
||||
import { page_id, registerClientFile, styles } from "../ssr.js";
|
||||
const host= "."+code.name;
|
||||
styles.css`
|
||||
/* Code block styling */
|
||||
@ -177,6 +177,9 @@ ${host}:hover .copy-button {
|
||||
}
|
||||
`;
|
||||
import { el } from "deka-dom-el";
|
||||
/**
|
||||
* @typedef {"js"|"ts"|"html"|"css"|"shell"|"-"} Language
|
||||
* */
|
||||
/**
|
||||
* Prints code to the page and registers flems to make it interactive.
|
||||
* @param {object} attrs
|
||||
@ -184,15 +187,17 @@ import { el } from "deka-dom-el";
|
||||
* @param {string} [attrs.className]
|
||||
* @param {URL} [attrs.src] Example code file path
|
||||
* @param {string} [attrs.content] Example code
|
||||
* @param {"js"|"ts"|"html"|"css"|"shell"} [attrs.language="js"] Language of the code
|
||||
* @param {string} [attrs.page_id] ID of the page, if setted it registers shiki
|
||||
* @param {Language} [attrs.language="-s"] Language of the code
|
||||
* */
|
||||
export function code({ id, src, content, language= "js", className= host.slice(1), page_id }){
|
||||
if(src) content= s.cat(src);
|
||||
export function code({ id, src, content, language= "-", className= host.slice(1) }){
|
||||
if(src){
|
||||
content= s.cat(src);
|
||||
if(language=== "-") language= /** @type {Language} */(src.pathname.split(".").pop());
|
||||
}
|
||||
content= normalizeIndentation(content);
|
||||
let dataJS;
|
||||
if(page_id){
|
||||
registerClientPart(page_id);
|
||||
if(language!== "-"){
|
||||
registerClientPart();
|
||||
dataJS= "todo";
|
||||
}
|
||||
return el("div", { id, className, dataJS, tabIndex: 0 }).append(
|
||||
@ -204,8 +209,7 @@ export function pre({ content }){
|
||||
return el("pre").append(el("code", content.trim()));
|
||||
}
|
||||
let is_registered= {};
|
||||
/** @param {string} page_id */
|
||||
function registerClientPart(page_id){
|
||||
function registerClientPart(){
|
||||
if(is_registered[page_id]) return;
|
||||
|
||||
// Add Shiki with a more reliable loading method
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { styles } from "../ssr.js";
|
||||
import { page_id, styles } from "../ssr.js";
|
||||
|
||||
styles.css`
|
||||
#html-to-dde-converter {
|
||||
@ -149,12 +149,11 @@ import { ireland } from "./ireland.html.js";
|
||||
import { el } from "deka-dom-el";
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
|
||||
export function converter({ page_id }){
|
||||
export function converter(){
|
||||
registerClientPart(page_id);
|
||||
return el(ireland, {
|
||||
src: fileURL("./converter.js.js"),
|
||||
exportName: "converter",
|
||||
page_id,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { styles } from "../ssr.js";
|
||||
import { page_id, styles } from "../ssr.js";
|
||||
const host= "."+example.name;
|
||||
styles.css`
|
||||
${host} {
|
||||
@ -119,9 +119,8 @@ import { relative } from "node:path";
|
||||
* @param {URL} attrs.src Example code file path
|
||||
* @param {"js"|"ts"|"html"|"css"} [attrs.language="js"] Language of the code
|
||||
* @param {"normal"|"big"} [attrs.variant="normal"] Size of the example
|
||||
* @param {string} attrs.page_id ID of the page
|
||||
* */
|
||||
export function example({ src, language= "js", variant= "normal", page_id }){
|
||||
export function example({ src, language= "js", variant= "normal" }){
|
||||
registerClientPart(page_id);
|
||||
const content= s.cat(src).toString()
|
||||
.replaceAll(/ from "deka-dom-el(\/signals)?";/g, ' from "./esm-with-signals.js";');
|
||||
|
@ -303,6 +303,7 @@ document.body.append(
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
|
501
docs/components/examples/case-studies/products.js
Normal file
501
docs/components/examples/case-studies/products.js
Normal file
@ -0,0 +1,501 @@
|
||||
import { el, on } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
export function ProductCatalog() {
|
||||
const itemsPerPage = 5;
|
||||
const products = asyncSignal(S, fetchProducts, { initial: [], keepLast: true });
|
||||
const searchTerm = S("");
|
||||
const handleSearch = (e) => searchTerm.set(e.target.value);
|
||||
const sortOrder = S("default");
|
||||
const handleSort = (e) => sortOrder.set(e.target.value);
|
||||
const page = S(1);
|
||||
const handlePageChange = (newPage) => page.set(newPage);
|
||||
const resetFilters = () => {
|
||||
searchTerm.set("");
|
||||
sortOrder.set("default");
|
||||
page.set(1);
|
||||
};
|
||||
|
||||
const filteredProducts = S(() => {
|
||||
if (products.status.get() !== "resolved") return [];
|
||||
|
||||
const results = products.result.get().filter(product =>
|
||||
product.title.toLowerCase().includes(searchTerm.get().toLowerCase()) ||
|
||||
product.description.toLowerCase().includes(searchTerm.get().toLowerCase())
|
||||
);
|
||||
|
||||
return [...results].sort((a, b) => {
|
||||
const order = sortOrder.get();
|
||||
if (order === "price-asc") return a.price - b.price;
|
||||
if (order === "price-desc") return b.price - a.price;
|
||||
if (order === "rating") return b.rating - a.rating;
|
||||
return 0; // default: no sorting
|
||||
});
|
||||
});
|
||||
const totalPages = S(() => Math.ceil(filteredProducts.get().length / itemsPerPage));
|
||||
const paginatedProducts = S(() => {
|
||||
const currentPage = page.get();
|
||||
const filtered = filteredProducts.get();
|
||||
const start = (currentPage - 1) * itemsPerPage;
|
||||
return filtered.slice(start, start + itemsPerPage);
|
||||
});
|
||||
|
||||
// Component structure
|
||||
return el("div", { className: "product-catalog" }).append(
|
||||
el("header", { className: "catalog-header" }).append(
|
||||
el("h2", "Product Catalog"),
|
||||
el("div", { className: "toolbar" }).append(
|
||||
el("button", {
|
||||
className: "refresh-btn",
|
||||
textContent: "Refresh Products",
|
||||
type: "button",
|
||||
onclick: () => products.invoke(),
|
||||
}),
|
||||
el("button", {
|
||||
className: "reset-btn",
|
||||
textContent: "Reset Filters",
|
||||
type: "button",
|
||||
onclick: resetFilters,
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
// Search and filter controls
|
||||
el("div", { className: "controls" }).append(
|
||||
el("div", { className: "search-box" }).append(
|
||||
el("input", {
|
||||
type: "search",
|
||||
placeholder: "Search products...",
|
||||
value: searchTerm,
|
||||
oninput: handleSearch,
|
||||
})
|
||||
),
|
||||
el("div", { className: "sort-options" }).append(
|
||||
el("label", "Sort by: "),
|
||||
el("select", { onchange: handleSort }, on.defer(el => el.value = sortOrder.get())).append(
|
||||
el("option", { value: "default", textContent: "Default" }),
|
||||
el("option", { value: "price-asc", textContent: "Price: Low to High" }),
|
||||
el("option", { value: "price-desc", textContent: "Price: High to Low" }),
|
||||
el("option", { value: "rating", textContent: "Top Rated" })
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Status indicators
|
||||
el("div", { className: "status-container" }).append(
|
||||
S.el(products.status, status =>
|
||||
status === "pending" ?
|
||||
el("div", { className: "loader" }).append(
|
||||
el("div", { className: "spinner" }),
|
||||
el("p", "Loading products...")
|
||||
)
|
||||
: status === "rejected" ?
|
||||
el("div", { className: "error-message" }).append(
|
||||
el("p", products.error.get().message),
|
||||
el("button", {
|
||||
textContent: "Try Again",
|
||||
onclick: () => products.invoke()
|
||||
})
|
||||
)
|
||||
: el()
|
||||
)
|
||||
),
|
||||
|
||||
// Results count
|
||||
S.el(S(()=> [filteredProducts.get(), searchTerm.get()]), ([filtered, term]) =>
|
||||
products.status.get() === "resolved"
|
||||
? el("div", {
|
||||
className: "results-info",
|
||||
textContent: term ?
|
||||
`Found ${filtered.length} products matching "${term}"`
|
||||
: `Showing all ${filtered.length} products`
|
||||
})
|
||||
: el()
|
||||
),
|
||||
|
||||
// Products grid
|
||||
el("div", { className: "products-grid" }).append(
|
||||
S.el(paginatedProducts, paginatedItems =>
|
||||
products.status.get() === "resolved" && paginatedItems.length > 0 ?
|
||||
paginatedItems.map(product => el(ProductCard, { product }))
|
||||
: products.status.get() === "resolved" && paginatedItems.length === 0 ?
|
||||
el("p", { className: "no-results", textContent: "No products found matching your criteria." })
|
||||
: el()
|
||||
)
|
||||
),
|
||||
|
||||
// Pagination
|
||||
S.el(S(()=> [totalPages.get(), page.get()]), ([total, current]) =>
|
||||
products.status.get() === "resolved" && total > 1 ?
|
||||
el("div", { className: "pagination" }).append(
|
||||
el("button", {
|
||||
textContent: "Previous",
|
||||
disabled: current === 1,
|
||||
onclick: () => handlePageChange(current - 1)
|
||||
}),
|
||||
...Array.from({ length: total }, (_, i) => i + 1).map(num =>
|
||||
el("button", {
|
||||
className: num === current ? "current-page" : "",
|
||||
textContent: num,
|
||||
onclick: () => handlePageChange(num)
|
||||
})
|
||||
),
|
||||
el("button", {
|
||||
textContent: "Next",
|
||||
disabled: current === total,
|
||||
onclick: () => handlePageChange(current + 1)
|
||||
})
|
||||
)
|
||||
: el()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Product card component
|
||||
function ProductCard({ product }) {
|
||||
const showDetails = S(false);
|
||||
|
||||
return el("div", { className: "product-card" }).append(
|
||||
el("div", { className: "product-image" }).append(
|
||||
el("img", { src: product.thumbnail, alt: product.title })
|
||||
),
|
||||
el("div", { className: "product-info" }).append(
|
||||
el("h3", { className: "product-title", textContent: product.title }),
|
||||
el("div", { className: "product-price-rating" }).append(
|
||||
el("span", { className: "product-price", textContent: `$${product.price.toFixed(2)}` }),
|
||||
el("span", { className: "product-rating" }).append(
|
||||
el("span", { className: "stars", textContent: "★".repeat(Math.round(product.rating)) }),
|
||||
el("span", { className: "rating-value", textContent: `(${product.rating})` }),
|
||||
)
|
||||
),
|
||||
el("p", { className: "product-category", textContent: `Category: ${product.category}` }),
|
||||
S.el(showDetails, details =>
|
||||
details ?
|
||||
el("div", { className: "product-details" }).append(
|
||||
el("p", { className: "product-description", textContent: product.description }),
|
||||
el("div", { className: "product-meta" }).append(
|
||||
el("p", `Brand: ${product.brand}`),
|
||||
el("p", `Stock: ${product.stock} units`),
|
||||
el("p", `Discount: ${product.discountPercentage}%`)
|
||||
)
|
||||
)
|
||||
: el()
|
||||
),
|
||||
el("div", { className: "product-actions" }).append(
|
||||
el("button", {
|
||||
className: "details-btn",
|
||||
textContent: S(() => showDetails.get() ? "Hide Details" : "Show Details"),
|
||||
onclick: () => showDetails.set(!showDetails.get())
|
||||
}),
|
||||
el("button", {
|
||||
className: "add-to-cart-btn",
|
||||
textContent: "Add to Cart"
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Data fetching function
|
||||
async function fetchProducts({ signal }) {
|
||||
await simulateNetworkDelay();
|
||||
// Simulate random errors for demonstration
|
||||
if (Math.random() > 0.9) throw new Error("Failed to load products. Network error.");
|
||||
|
||||
const response = await fetch("https://dummyjson.com/products", { signal });
|
||||
if (!response.ok) throw new Error(`API error: ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
return data.products.slice(0, 20); // Limit to 20 products for the demo
|
||||
}
|
||||
|
||||
// Utility for simulating network latency
|
||||
function simulateNetworkDelay(min = 300, max = 1200) {
|
||||
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
return new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for async data fetching with signals
|
||||
* @template T
|
||||
* @param {typeof S} S - Signal constructor
|
||||
* @param {(params: { signal: AbortSignal }) => Promise<T>} invoker - Async function to execute
|
||||
* @param {{ initial?: T, keepLast?: boolean }} options - Configuration options
|
||||
* @returns {Object} Status signals and control methods
|
||||
*/
|
||||
export function asyncSignal(S, invoker, { initial, keepLast } = {}) {
|
||||
// Status tracking signals
|
||||
const status = S("pending");
|
||||
const result = S(initial);
|
||||
const error = S(null);
|
||||
let controller = null;
|
||||
|
||||
// Function to trigger data fetching
|
||||
async function invoke() {
|
||||
// Cancel any in-flight request
|
||||
if (controller) controller.abort();
|
||||
controller = new AbortController();
|
||||
|
||||
status.set("pending");
|
||||
error.set(null);
|
||||
if (!keepLast) result.set(initial);
|
||||
|
||||
try {
|
||||
const data = await invoker({
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!controller.signal.aborted) {
|
||||
status.set("resolved");
|
||||
result.set(data);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name !== "AbortError") {
|
||||
error.set(e);
|
||||
status.set("rejected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initial data fetch
|
||||
invoke();
|
||||
|
||||
return { status, result, error, invoke };
|
||||
}
|
||||
|
||||
// Initialize the component
|
||||
document.body.append(
|
||||
el(ProductCatalog),
|
||||
el("style", `
|
||||
.product-catalog {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.catalog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
margin-left: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: #4a6cf7;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sort-options select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
border-left-color: #4a6cf7;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.results-info {
|
||||
margin-bottom: 15px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.product-image img {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.product-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 1.1rem;
|
||||
height: 2.4rem;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.product-price-rating {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-weight: bold;
|
||||
color: #4a6cf7;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.stars {
|
||||
color: gold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.product-category {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
margin: 15px 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
line-height: 1.5;
|
||||
margin-bottom: 10px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.product-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.product-actions button {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.details-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.add-to-cart-btn {
|
||||
background: #4a6cf7;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pagination button.current-page {
|
||||
background: #4a6cf7;
|
||||
color: white;
|
||||
border-color: #4a6cf7;
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
}
|
||||
}
|
||||
`),
|
||||
);
|
@ -483,7 +483,7 @@ document.body.append(
|
||||
|
||||
.task-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,11 @@ import { el } from "deka-dom-el";
|
||||
const button = el("button", {
|
||||
textContent: "Click me",
|
||||
className: "primary",
|
||||
disabled: true
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
// Shorter and more expressive
|
||||
// than the native approach
|
||||
|
||||
// Add to DOM
|
||||
document.body.append(button);
|
||||
document.body.append(button);
|
||||
|
@ -6,6 +6,6 @@ import { el } from "deka-dom-el";
|
||||
document.body.append(
|
||||
el("div").append(
|
||||
el("h1", "Title"),
|
||||
el("p", "Paragraph")
|
||||
)
|
||||
el("p", "Paragraph"),
|
||||
),
|
||||
);
|
||||
|
@ -81,11 +81,11 @@ function Todos(){
|
||||
)
|
||||
)
|
||||
),
|
||||
S.el(todosS, todos => !todos.length
|
||||
S.el(todosS, ({ length }) => !length
|
||||
? el()
|
||||
: el("footer", { className: "footer" }).append(
|
||||
el("span", { className: "todo-count" }).append(
|
||||
noOfLeft()
|
||||
el("strong", length + " " + (length === 1 ? "item" : "items")),
|
||||
),
|
||||
memo("filters", ()=>
|
||||
el("ul", { className: "filters" }).append(
|
||||
@ -100,7 +100,7 @@ function Todos(){
|
||||
)
|
||||
),
|
||||
),
|
||||
todos.length - todosRemainingS.get() === 0
|
||||
length - todosRemainingS.get() === 0
|
||||
? el()
|
||||
: memo("delete", () =>
|
||||
el("button",
|
||||
@ -110,13 +110,6 @@ function Todos(){
|
||||
)
|
||||
)
|
||||
);
|
||||
function noOfLeft(){
|
||||
const length = todosRemainingS.get();
|
||||
return el("strong").append(
|
||||
length + " ",
|
||||
length === 1 ? "item left" : "items left"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { styles } from "../ssr.js";
|
||||
import { styles, page_id } from "../ssr.js";
|
||||
|
||||
styles.css`
|
||||
#library-url-form {
|
||||
@ -74,7 +74,7 @@ styles.css`
|
||||
import { el } from "deka-dom-el";
|
||||
import { ireland } from "./ireland.html.js";
|
||||
|
||||
export function getLibraryUrl({ page_id }){
|
||||
export function getLibraryUrl(){
|
||||
return el(ireland, {
|
||||
src: new URL("./getLibraryUrl.js.js", import.meta.url),
|
||||
exportName: "getLibraryUrl",
|
||||
|
@ -43,7 +43,6 @@ const componentsRegistry = new Map();
|
||||
* @param {object} attrs
|
||||
* @param {URL} attrs.src - Path to the file containing the component
|
||||
* @param {string} [attrs.exportName="default"] - Name of the export to use
|
||||
* @param {string} attrs.page_id - ID of the current page
|
||||
* @param {object} [attrs.props={}] - Props to pass to the component
|
||||
*/
|
||||
export function ireland({ src, exportName = "default", props = {} }) {
|
||||
|
@ -12,6 +12,10 @@ export function mnemonic(){
|
||||
" — corresponds to custom elemnets callbacks ", el("code", "<live-cycle>Callback(...){...}"),
|
||||
". To connect to custom element see following page, else it is simulated by MutationObserver."
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "on.defer(<identity>=> <identity>)(<identity>)"),
|
||||
" — calls callback later",
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "dispatchEvent(<event>[, <options>])(element)"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))")
|
||||
|
Reference in New Issue
Block a user