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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
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