Small contribution: examples ported to htm
for no-compile JSX / in-browser Crank demos
#227
Replies: 3 comments 5 replies
-
TODO MVCCrank source: =======> Try on JSPM.org =======> Try on ESM.codes Code: click to expand(note that the import {createElement, Fragment} from "https://unpkg.com/@b9g/crank?module"
import {renderer} from "https://unpkg.com/@b9g/crank/dom?module"
import htm from 'https://unpkg.com/htm?module'
const html = htm.bind(createElement)
// import "https://cdn.skypack.dev/todomvc-common"
const ENTER_KEY = 13;
const ESC_KEY = 27;
function* Header() {
let title = "";
this.addEventListener("input", (ev) => {
title = ev.target.value;
});
this.addEventListener("keydown", (ev) => {
if (ev.target.tagName === "INPUT" && ev.keyCode === ENTER_KEY) {
if (title.trim()) {
ev.preventDefault();
const title1 = title.trim();
title = "";
this.dispatchEvent(
new CustomEvent("todocreate", {
bubbles: true,
detail: {title: title1},
}),
);
}
}
});
for ({} of this) {
yield (html`
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
value=${title}
/>
</header>
`);
}
}
function* TodoItem({todo}) {
let active = false;
let title = todo.title;
this.addEventListener("click", (ev) => {
if (ev.target.className === "toggle") {
this.dispatchEvent(
new CustomEvent("todotoggle", {
bubbles: true,
detail: {id: todo.id, completed: !todo.completed},
}),
);
} else if (ev.target.className === "destroy") {
this.dispatchEvent(
new CustomEvent("tododestroy", {
bubbles: true,
detail: {id: todo.id},
}),
);
}
});
this.addEventListener("dblclick", (ev) => {
if (ev.target.tagName === "LABEL") {
active = true;
this.refresh();
ev.target.parentElement.nextSibling.focus();
}
});
this.addEventListener("input", (ev) => {
if (ev.target.className === "edit") {
title = ev.target.value;
}
});
this.addEventListener("keydown", (ev) => {
if (ev.target.className === "edit") {
if (ev.keyCode === ENTER_KEY) {
active = false;
title = title.trim();
if (title) {
this.dispatchEvent(
new CustomEvent("todoedit", {
bubbles: true,
detail: {id: todo.id, title},
}),
);
} else {
this.dispatchEvent(
new CustomEvent("tododestroy", {
bubbles: true,
detail: {id: todo.id},
}),
);
}
} else if (ev.keyCode === ESC_KEY) {
active = false;
title = todo.title;
this.refresh();
}
}
});
this.addEventListener(
"blur",
(ev) => {
if (ev.target.className === "edit") {
active = false;
if (title) {
this.dispatchEvent(
new CustomEvent("todoedit", {
bubbles: true,
detail: {id: todo.id, title},
}),
);
} else {
this.dispatchEvent(
new CustomEvent("tododestroy", {
bubbles: true,
detail: {id: todo.id},
}),
);
}
}
},
{capture: true},
);
for ({todo} of this) {
const classes = [];
if (active) {
classes.push("editing");
}
if (todo.completed) {
classes.push("completed");
}
yield (html`
<li class=${classes.join(" ")}>
<div class="view">
<input class="toggle" type="checkbox" checked=${todo.completed} />
<label>${todo.title}</label>
<button class="destroy" />
</div>
<input class="edit" value=${title} />
</li>
`);
}
}
function Main({todos, filter}) {
const completed = todos.every((todo) => todo.completed);
this.addEventListener("click", (ev) => {
if (ev.target.className === "toggle-all") {
this.dispatchEvent(
new CustomEvent("todotoggleall", {
bubbles: true,
detail: {completed: !completed},
}),
);
}
});
if (filter === "active") {
todos = todos.filter((todo) => !todo.completed);
} else if (filter === "completed") {
todos = todos.filter((todo) => todo.completed);
}
return (html`
<section class="main">
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
checked=${completed}
/>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
${todos.map((todo) => (
html`<${TodoItem} todo=${todo} crank-key=${todo.id} />`
))}
</ul>
</section>
`);
}
function Filters({filter}) {
return (html`
<ul class="filters">
<li>
<a class=${filter === "" ? "selected" : ""} href="#/">
All
</a>
</li>
<li>
<a class=${filter === "active" ? "selected" : ""} href="#/active">
Active
</a>
</li>
<li>
<a class=${filter === "completed" ? "selected" : ""} href="#/completed">
Completed
</a>
</li>
</ul>
`);
}
function Footer({todos, filter}) {
const completed = todos.filter((todo) => todo.completed).length;
const remaining = todos.length - completed;
this.addEventListener("click", (ev) => {
if (ev.target.className === "clear-completed") {
this.dispatchEvent(new Event("todoclearcompleted", {bubbles: true}));
}
});
return (html`
<footer class="footer">
<span class="todo-count">
<strong>${remaining}</strong> ${remaining === 1 ? "item" : "items"} left
</span>
<${Filters} filter=${filter} />
${!!completed && html`<button class="clear-completed">Clear completed</button>`}
</footer>
`);
}
const STORAGE_KEY = "todos-crank";
function save(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}
function* App() {
let todos = [];
let nextTodoId = 0;
try {
const storedTodos = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (Array.isArray(storedTodos) && storedTodos.length) {
todos = storedTodos;
nextTodoId = Math.max(...storedTodos.map((todo) => todo.id)) + 1;
} else {
localStorage.removeItem(STORAGE_KEY);
}
} catch (err) {
localStorage.removeItem(STORAGE_KEY);
}
let filter = "";
this.addEventListener("todocreate", (ev) => {
todos.push({id: nextTodoId++, title: ev.detail.title, completed: false});
this.refresh();
save(todos);
});
this.addEventListener("todoedit", (ev) => {
const i = todos.findIndex((todo) => todo.id === ev.detail.id);
todos[i].title = ev.detail.title;
this.refresh();
save(todos);
});
this.addEventListener("todotoggle", (ev) => {
const i = todos.findIndex((todo) => todo.id === ev.detail.id);
todos[i].completed = ev.detail.completed;
this.refresh();
save(todos);
});
this.addEventListener("todotoggleall", (ev) => {
todos = todos.map((todo) => ({...todo, completed: ev.detail.completed}));
this.refresh();
save(todos);
});
this.addEventListener("todoclearcompleted", () => {
todos = todos.filter((todo) => !todo.completed);
this.refresh();
save(todos);
});
this.addEventListener("tododestroy", (ev) => {
todos = todos.filter((todo) => todo.id !== ev.detail.id);
this.refresh();
save(todos);
});
const route = (ev) => {
switch (window.location.hash) {
case "#/active": {
filter = "active";
break;
}
case "#/completed": {
filter = "completed";
break;
}
case "#/": {
filter = "";
break;
}
default: {
filter = "";
window.location.hash = "#/";
}
}
if (ev != null) {
this.refresh();
}
};
route();
window.addEventListener("hashchange", route);
try {
for ({} of this) {
yield (html`
<${Fragment}>
<${Header} />
${!!todos.length && html`<${Main} todos=${todos} filter=${filter} />`}
${!!todos.length && html`<${Footer} todos=${todos} filter=${filter} />`}
<//>
`);
}
} finally {
window.removeEventListener("hashchange", route);
}
}
renderer.render(html`<${App} />`, document.getElementsByClassName("todoapp")[0]); // document.body |
Beta Was this translation helpful? Give feedback.
-
HACKER NEWSCrank source: =======> Try on JSPM.org =======> Try on ESM.codes Code: click to expandimport {createElement, Fragment, Raw} from "https://unpkg.com/@b9g/crank?module"
import {renderer} from "https://unpkg.com/@b9g/crank/dom?module"
import htm from 'https://unpkg.com/htm?module'
const html = htm.bind(createElement)
function* Comment() {
let expanded = true;
this.addEventListener("click", (ev) => {
if (ev.target.className === "expand") {
expanded = !expanded;
this.refresh();
ev.stopPropagation();
}
});
for (const {comment} of this) {
yield (html`
<div class="comment">
<p>
<button class="expand">${expanded ? "[-]" : "[+]"}</button>${" "}
<a href="">${comment.user}</a> ${comment.time_ago}{" "}
</p>
<div style=${{display: expanded ? null : "none"}}>
<p>
<${Raw} value=${comment.content} />
</p>
<div class="replies">
${comment.comments.map((reply) => (html`
<${Comment} crank-key=${reply.id} comment=${reply} />
`))}
</div>
</div>
</div>
`);
}
}
async function Item({id}) {
const result = await fetch(`https://api.hnpwa.com/v0/item/${id}.json`);
const item = await result.json();
return (html`
<div class="item">
<a href=${item.url}>
<h1>${item.title}</h1>
</a>
<p class="domain">${item.domain}</p>
<p class="meta">
submitted by <a>${item.user}</a> ${item.time_ago}
</p>
${item.comments.map((comment) => (html`
<${Comment} comment=${comment} crank-key=${comment.id} />
`))}
</div>
`);
}
function Story({story}) {
return (html`
<li class="story">
<a href=${story.url}>${story.title}</a> <span>(${story.domain})</span>
<p class="meta">
${story.points} points by <a href="">${story.user}</a> | ${story.time_ago}${" "}
| <a href=${`#/item/${story.id}`}>${story.comments_count} comments</a>
</p>
</li>
`);
}
function Pager({page}) {
return (html`
<div class="pager">
<div>
<a>Previous </a> ${page}/25 <a>Next</a>
</div>
</div>
`);
}
async function List({page, start = 1}) {
const result = await fetch(`https://api.hnpwa.com/v0/news/${page}.json`);
const stories = await result.json();
const items = stories.map((story) => (html`
<${Story} story=${story} crank-key=${story.id} />
`));
return (html`
<${Fragment}>
<${Pager} page=${page} />
<ol start=${start}>${items}</ol>
<${Pager} page=${page} />
<//>
`);
}
function parseHash(hash) {
if (hash.startsWith("#/item/")) {
const id = hash.slice(7);
if (id) {
return {route: "item", id};
}
} else if (hash.startsWith("#/top/")) {
const page = parseInt(hash.slice(6)) || 1;
if (!Number.isNaN(page)) {
return {route: "top", page};
}
}
}
async function Loading({wait = 2000}) {
await new Promise((resolve) => setTimeout(resolve, wait));
return "Loading...";
}
async function* App() {
let data;
const route = (ev) => {
const hash = window.location.hash;
data = parseHash(hash);
if (data == null) {
data = {route: "top", page: 1};
window.location.hash = "#/";
}
if (ev) {
this.refresh();
}
};
window.addEventListener("hashchange", route);
route();
try {
for await (const _ of this) {
yield html`<${Loading} />`;
switch (data.route) {
case "item": {
await (yield html`<${Item} ...${data} />`);
break;
}
case "top": {
await (yield html`<${List} ...${data} />`);
break;
}
}
window.scrollTo(0, 0);
}
} finally {
window.removeEventListener("hashchange", route);
}
}
function Navbar() {
return html`<div class="navbar">Top New Show Ask Jobs</div>`;
}
function Root() {
return (html`
<div class="root">
<${Navbar} />
<${App} />
</div>
`);
}
renderer.render(html`<${Root} />`, document.body.firstElementChild); |
Beta Was this translation helpful? Give feedback.
-
@danielweck ESM dot codes is pretty cool. If you check out the docs site I shipped a very early interactive examples component that I’ve hacked together based on another project which I have yet to write a blog post for (https://github.com/bikeshaving/revise) — for working with contenteditables and text editing. Honestly, for the longest time working on that project felt kind of insane and futile, but a couple days ago I said flip it and published the work I had so far (https://crank.js.org/guides/getting-started), and it made me feel a little better. As far as I do wish to provide a template tag-based alternative to JSX, and I do think that I don’t know. It was funny, I started writing a potato parser for interpreting markdown HTML as Crank JSX, because MDX is practically hard-coded to React, and I thought, I could probably write an |
Beta Was this translation helpful? Give feedback.
-
Hello, I was looking at the README examples ( https://github.com/bikeshaving/crank/blob/master/README.md#key-examples ) and I thought it would be fun to port the compilable JSX to
htm
( https://github.com/developit/htm ), so that Crank "quick demos" / "quick repros" can be made instantly / in-browser via the https://esm.codes REPL, for example.A Simple Component
=======> Try on JSPM.org
=======> Try on ESM.codes
Code: click to expand
==>
A Stateful Component
=======> Try on JSPM.org
=======> Try on ESM.codes
Code: click to expand
==>
An Async Component
=======> Try on JSPM.org
=======> Try on ESM.codes
Code: click to expand
==>
A Loading Component
=======> Try on JSPM.org
=======> Try on ESM.codes
Code: click to expand
==>
Beta Was this translation helpful? Give feedback.
All reactions