Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Short, concise, Vanilla JS TodoMVC one pager #2119

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cypress/integration/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ const noLocalStorageCheck = {
puremvc: true,
'typescript-backbone': true,
enyo_backbone: true,
foam: true
foam: true,
'vanilla-slim': true
}

const noLocalStorageSpyCheck = {
canjs: true,
canjs_require: true
canjs_require: true,
'vanilla-slim': true
}

const noAppStartCheck = {
Expand Down
Binary file added cypress/videos/spec.js.mp4
Binary file not shown.
11 changes: 11 additions & 0 deletions examples/vanilla-slim/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Short, Concise, Vanilla Javascript TodoMVC Example

What if the browser had a virtual DOM? So we could write HTML as if we were writing React but without the framework?

What if we re-imagined how we coded and kept our sights on writing declarative code rather than always writing out _how_ to do things.

<<<<<<< HEAD
That is what this in. 209 lines of pure, simple, easy to parse JS.
=======
That is what this is. ~215 lines of pure, simple, easy to parse JS.
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
23 changes: 23 additions & 0 deletions examples/vanilla-slim/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en" data-framework="vanilla-slim">
<head>
<meta charset="utf-8" />
<title>Slim Vanilla JS • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css" />
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css" />
</head>
<body>
<section class="todoapp" id="container"></section>
<footer class="info">
<p>Double-click to edit a todo</p>
<<<<<<< HEAD
<p>Written by <a href="http://github.com/tantaman">Matt Wonlaw</a></p>
=======
<p>Written by <a href="http://github.com/tantaman">Matthew Wonlaw</a></p>
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="node_modules/todomvc-common/base.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
288 changes: 288 additions & 0 deletions examples/vanilla-slim/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
const tempEl = document.createElement("div");
const sanitize = (value) => {
if (value) {
if (typeof value === "object" && value.__html__) {
return value.__html__;
}
if (Array.isArray(value)) {
return value.map(sanitize).join("");
}
}
tempEl.textContent = value;
return tempEl.innerHTML;
};
const html = (parts, ...values) => {
return {
__html__: parts
.map((part, i) => {
return part + (i < values.length ? sanitize(values[i]) : "");
})
.join(""),
};
};

const todoApp = (state) => {
const remaining = state.items.filter((item) => !item.complete);
let toggleAll = "";
if (state.items.length) {
<<<<<<< HEAD
toggleAll = html`<input
=======
toggleAll = html`
<input
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
id="toggle-all"
type="checkbox"
class="toggle-all"
${remaining.length ? "" : "checked"}
onclick="toggleAll();"
<<<<<<< HEAD
/><label for="toggle-all">Mark all as complete</label>`;
=======
/>
<label for="toggle-all">Mark all as complete</label>`;
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
}
return html`<div class="todoapp">
${header()}
<section class="main" ${state.items.length ? "" : 'style="display: none;"'}>
${toggleAll}
<ul class="todo-list">
${state.items.map(todo)}
</ul>
${footer(remaining, state.items)}
</section>
</div>`;
};
const header = () =>
html`<header class="header">
<h1>todos</h1>
<input
type="text"
class="new-todo"
id="todoInput"
placeholder="What needs to be done?"
onkeydown="onCreate(event)"
autofocus
value="${state.newTodo}"
/>
</header>`;
const todo = (item, i) => {
if (state.filter === "completed" && !item.complete) return "";
if (state.filter === "active" && item.complete) return "";
let body = "";
if (item._editing) {
body = html`
<input
type="text"
class="edit"
autofocus
value="${item.name}"
onkeydown="onSave(event, ${i})"
onblur="onSave(event, ${i})"
/>
`;
} else {
body = html`
<div class="view">
<input
type="checkbox"
class="toggle"
${item.complete ? "checked" : ""}
onclick="toggle(${i});"
/>
<label ondblclick="startEditing(${i})">${item.name}</label>
<button class="destroy" onClick="remove(${i})" />
</div>
`;
}
return html`
<<<<<<< HEAD
<li
class="${item.complete ? "completed" : ""} ${item._editing
? "editing"
: ""}"
>
=======
<li class="${item.complete ? "completed" : ""} ${item._editing ? "editing" : ""}">
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
${body}
</li>
`;
};
const footer = (remaining, items) => {
let clearCompleted = "";
if (remaining.length !== items.length) {
<<<<<<< HEAD
clearCompleted = html`<button
class="clear-completed"
onClick="clearCompleted()"
>
=======
clearCompleted = html`
<button
class="clear-completed"
onClick="clearCompleted()">
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
Clear completed
</button>`;
}
return html`<footer class="footer">
<span class="todo-count">
<strong>
${remaining.length ? remaining.length : "0"}
</strong>
${remaining.length === 1 ? "item" : "items"} left
</span>
<ul class="filters">
<li>
<<<<<<< HEAD
<a
class="${state.filter === "" ? "selected" : ""}"
onClick="updateFilter('')"
>All</a
>
</li>
<li>
<a
class="${state.filter === "active" ? "selected" : ""}"
onClick="updateFilter('active')"
>Active</a
>
</li>
<li>
<a
class="${state.filter === "completed" ? "selected" : ""}"
onClick="updateFilter('completed')"
>Completed</a
>
=======
<a class="${state.filter === "" ? "selected" : ""}" onClick="updateFilter('')">
All
</a>
</li>
<li>
<a class="${state.filter === "active" ? "selected" : ""}" onClick="updateFilter('active')">Active</a>
</li>
<li>
<a class="${state.filter === "completed" ? "selected" : ""}" onClick="updateFilter('completed')">
Completed
</a>
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
</li>
</ul>
${clearCompleted}
</footer>`;
};

function toggleAll() {
const hasRemaining = state.items.filter((i) => !i.complete).length != 0;
state.items.forEach((i) => (i.complete = hasRemaining));
turnTheCrank();
}
function clearCompleted() {
state.items = state.items.filter((i) => !i.complete);
turnTheCrank();
}
function onCreate(e) {
const text = getFinalText(e);
if (text) {
state.items.push({
name: text,
complete: false,
});
state.newTodo = "";
turnTheCrank("todoInput");
}
}
function onSave(e, i) {
<<<<<<< HEAD
=======
if (e.which === 27) {
state.items[i]._editing = false;
e.target.value = state.items[i].name;
turnTheCrank();
return;
}
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
const text = getFinalText(e);
if (text) {
state.items[i].name = text;
state.items[i]._editing = false;
setItems();
turnTheCrank();
<<<<<<< HEAD
=======
} else if (text !== null && state.items[i] && state.items[i]._editing) {
state.items[i]._editing = false;
state.items.splice(i, 1);
setItems();
turnTheCrank();
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
}
}
function toggle(i) {
state.items[i].complete = !state.items[i].complete;
turnTheCrank();
}
function remove(i) {
state.items.splice(i, 1);
turnTheCrank();
}
function startEditing(i) {
state.items[i]._editing = true;
turnTheCrank();
}
function updateFilter(filter) {
window.location.hash = filter;
}
window.onhashchange = function () {
state.filter = window.location.hash.split("#")[1] || "";
turnTheCrank();
};
const getFinalText = (e) =>
<<<<<<< HEAD
e.which === 13 || e.type === "blur" || e.which === 27
? e.target.value.trim()
: null;
const container = document.getElementById("container");
const prevState = localStorage.getItem("todos-vanilla-slim");
const state = {
filter: window.location.hash.split("#")[1] || "",
newTodo: "",
items: (prevState && JSON.parse(prevState)) || [],
};
=======
e.which === 13 || e.type === "blur" ? e.target.value.trim() : null;
let state;
window.onload = () => {
const container = document.getElementById("container");
const prevState = window.localStorage.getItem("todos-vanilla-slim");
state = {
filter: window.location.hash.split("#")[1] || "",
newTodo: "",
items: (prevState && JSON.parse(prevState)) || [],
};
turnTheCrank(null, () => {
document.querySelectorAll(".new-todo")[0].focus();
setItems(); // apprently TodoMVC tests need this line
});
}
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
function turnTheCrank(refocus, cb) {
requestAnimationFrame(() => {
container.innerHTML = todoApp(state).__html__;
if (refocus) document.getElementById(refocus).focus();
if (cb) cb();
});
}
const setItems = (window.onbeforeunload = () =>
<<<<<<< HEAD
localStorage.setItem("todos-vanilla-slim", JSON.stringify(state.items)));
turnTheCrank(null, () => {
document.querySelectorAll(".new-todo")[0].focus();
setItems(); // apprently TodoMVC tests need this line
});
=======
window.localStorage.setItem("todos-vanilla-slim", JSON.stringify(state.items)));
>>>>>>> 8c9b5e20a418c134d3ed0bd8dcf215b1c0105afe
Loading