all repos — h3 @ 5109642920d0098be427f14dfd8312de4a94e176

A tiny, extremely minimalist JavaScript microframework.

Refactored all app to use store.
h3rald h3rald@h3rald.com
Sat, 11 Apr 2020 17:08:16 +0200
commit

5109642920d0098be427f14dfd8312de4a94e176

parent

2f752ccce57c904ff1272be4b6bcc3215374a413

M example/assets/js/app.jsexample/assets/js/app.js

@@ -5,107 +5,28 @@ import NavigationBar from "./components/navigationBar.js";

import TodoList from "./components/todoList.js"; import store from "./store.js"; -// Main application state -let todos = []; -let filteredTodos = []; -let displayEmptyTodoError = false; -let filter = ""; -let pagesize = 10; -let page = 1; - -// UI Methods -// Add a todo item -const addTodo = () => { - const newTodo = document.getElementById("new-todo"); - if (!newTodo.value) { - store.dispatch('emptyTodoError.set'); - store.dispatch('emptyTodoError.update'); - document.getElementById("new-todo").focus(); - return; - } - store.dispatch('emptyTodoError.clear'); - todos.unshift({ - key: `todo_${Date.now()}__${newTodo.value}`, // Make todos "unique-enough" to ensure they are processed correctly - text: newTodo.value, - }); - newTodo.value = ""; - update(); - document.getElementById("new-todo").focus(); -}; - -// Add a todo item when pressing enter in the input field. -const addTodoOnEnter = (event, updateError) => { - if (event.keyCode == 13) { - addTodo(updateError); - event.preventDefault(); - } -}; - -const toggleTodo = (todo) => { - todo.done = !todo.done; - update(); -}; -const removeTodo = (todo) => { - todos = todos.filter(({ key }) => key !== todo.key); - update(); -}; - -// Set the todo filter. -const setFilter = (event) => { - let f = document.getElementById("filter-text"); - filter = f.value; - update(); - f = document.getElementById("filter-text"); - f.focus(); -}; - -// Clear error message -const clearError = () => { - displayEmptyTodoError = false; - update(); - document.getElementById("new-todo").focus(); -}; - -const refresh = () => { - update(); -} - -// Filtering function for todo items -const filterTodos = ({ text }) => text.match(filter); +//store.on("log", (state, data) => console.log(data, state)); // Main rendering function (creates virtual dom) const build = () => { + const { todos, filteredTodos, filter } = store.get(); localStorage.setItem("h3_todo_list", JSON.stringify(todos)); - const hash = window.location.hash; - filteredTodos = todos.filter(filterTodos); - if (hash.match(/page=(\d+)/)) { - page = parseInt(hash.match(/page=(\d+)/)[1]); - } - // Recalculate page in case data is filtered. - page = Math.min(Math.ceil(filteredTodos.length / pagesize), page) || 1; - const paginatorData = { - size: pagesize, - page: page, - total: filteredTodos.length, - }; - const start = (page - 1) * pagesize; - const end = Math.min(start + pagesize, filteredTodos.length); - const [error, updateError] = region(() => EmptyTodoError()) - store.on("emptyTodoError.update", updateError); + store.dispatch("todos/filter", filter); + const [error, updateError] = region(EmptyTodoError); + store.on("error/update", updateError); return h3("div#todolist.todo-list-container", [ h3("h1", "To Do List"), - AddTodoForm({ addTodo, addTodoOnEnter, updateError }), + AddTodoForm(), error, - NavigationBar({ filter, paginatorData }, { refresh, setFilter }), - TodoList({ filteredTodos, start, end }, { toggleTodo, removeTodo }), + NavigationBar(), + TodoList(), ]); }; -const storedTodos = localStorage.getItem("h3_todo_list"); -if (storedTodos) { - todos = JSON.parse(storedTodos); -} +store.dispatch("todos/load"); const [app, update] = region(build); + +store.on("app/update", update); mount("app", app);
M example/assets/js/components/addTodoForm.jsexample/assets/js/components/addTodoForm.js

@@ -1,20 +1,42 @@

import h3 from "../h3.js"; +import store from "../store.js"; -export default function AddTodoForm(actions) { - const { addTodo, addTodoOnEnter, updateError, updateMainSection } = actions; +export default function AddTodoForm() { + const addTodo = () => { + const newTodo = document.getElementById("new-todo"); + if (!newTodo.value) { + store.dispatch("error/set"); + store.dispatch("error/update"); + document.getElementById("new-todo").focus(); + return; + } + store.dispatch("error/clear"); + store.dispatch("todos/add", { + key: `todo_${Date.now()}__${newTodo.value}`, // Make todos "unique-enough" to ensure they are processed correctly + text: newTodo.value, + }); + newTodo.value = ""; + store.dispatch("app/update"); + document.getElementById("new-todo").focus(); + }; + const addTodoOnEnter = (event) => { + if (event.keyCode == 13) { + addTodo(); + event.preventDefault(); + } + }; return h3("form.add-todo-form", [ h3("input", { id: "new-todo", placeholder: "What do you want to do?", - autofocus: true, - onkeydown: (event) => addTodoOnEnter(event, updateError, updateMainSection), + onkeydown: addTodoOnEnter, }), h3( "span.submit-todo", { - onclick: () => addTodo(updateError, updateMainSection), + onclick: addTodo, }, - ["+"] + "+" ), ]); }
M example/assets/js/components/emptyTodoError.jsexample/assets/js/components/emptyTodoError.js

@@ -4,8 +4,8 @@

export default function EmptyTodoError(data, actions) { const emptyTodoErrorClass = store.get('displayEmptyTodoError') ? "" : ".hidden"; const clearError = () => { - store.dispatch('emptyTodoError.clear'); - store.dispatch('emptyTodoError.update'); + store.dispatch('error/clear'); + store.dispatch('error/update'); } return h3(`div#empty-todo-error.error${emptyTodoErrorClass}`, [ h3("span.error-message", ["Please enter a non-empty todo item."]),
M example/assets/js/components/navigationBar.jsexample/assets/js/components/navigationBar.js

@@ -1,16 +1,23 @@

import h3 from "../h3.js"; import Paginator from "./paginator.js"; +import store from "../store.js"; -export default function NavigationBar(data, actions) { - const { filter, paginatorData } = data; - const { setFilter, refresh } = actions; +export default function NavigationBar() { + // Set the todo filter. + const setFilter = () => { + let f = document.getElementById("filter-text"); + store.dispatch("todos/filter", f.value); + store.dispatch("app/update"); + f = document.getElementById("filter-text"); + f.focus(); + }; + // Filtering function for todo items return h3("div.navigation-bar", [ h3("input", { id: "filter-text", placeholder: "Type to filter todo items...", onkeyup: setFilter, - value: filter, }), - Paginator(paginatorData, { refresh }), + Paginator(), ]); }
M example/assets/js/components/paginator.jsexample/assets/js/components/paginator.js

@@ -1,22 +1,32 @@

import h3 from "../h3.js"; +import store from "../store.js"; -export default function Paginator(data, actions) { - const { refresh } = actions; - let page = data.page; - const size = data.size; - const total = data.total; - const pages = Math.ceil(total / size) || 1; +export default function Paginator() { + const hash = window.location.hash; + let { page, pagesize, filteredTodos } = store.get(); + let total = filteredTodos.length; + if (hash.match(/page=(\d+)/)) { + page = parseInt(hash.match(/page=(\d+)/)[1]); + } + // Recalculate page in case data is filtered. + page = Math.min(Math.ceil(filteredTodos.length / pagesize), page) || 1; + store.dispatch("pages/set", page); + const pages = Math.ceil(total / pagesize) || 1; const previousClass = page > 1 ? ".link" : ".disabled"; const nextClass = page < pages ? ".link" : ".disabled"; function setPreviousPage() { - page = page - 1; - window.location.hash = `/?page=${page}`; - refresh(); + const page = store.get('page'); + const newPage = page - 1; + store.dispatch("pages/set", newPage); + window.location.hash = `/?page=${newPage}`; + store.dispatch("app/update"); } function setNextPage() { - page = page + 1; - window.location.hash = `/?page=${page}`; - refresh(); + const page = store.get('page'); + const newPage = page + 1; + store.dispatch("pages/set", newPage); + window.location.hash = `/?page=${newPage}`; + store.dispatch("app/update"); } return h3("div.paginator", [ h3(
M example/assets/js/components/todo.jsexample/assets/js/components/todo.js

@@ -1,12 +1,20 @@

import h3 from "../h3.js"; +import store from "../store.js"; -export default function Todo(data, actions) { - const { toggleTodo, removeTodo } = actions; +export default function Todo(data) { const todoStateClass = data.done ? ".done" : ".todo"; - return h3("div.todo-item", { id: data.key }, [ + const toggleTodo = (todo) => { + store.dispatch("todos/toggle", data); + store.dispatch("app/update"); + }; + const removeTodo = (todo) => { + store.dispatch("todos/remove", data); + store.dispatch("app/update"); + }; + return h3(`div#${data.key}.todo-item`, [ h3(`div.todo-content${todoStateClass}`, [ h3("span.todo-text", { onclick: () => toggleTodo(data) }, data.text), ]), h3("span.delete-todo", { onclick: () => removeTodo(data) }, "✘"), ]); -}; +}
M example/assets/js/components/todoList.jsexample/assets/js/components/todoList.js

@@ -1,13 +1,14 @@

import h3 from "../h3.js"; import Todo from "./todo.js"; +import store from "../store.js"; -export default function TodoList(data, actions) { - const { start, end, filteredTodos } = data; - const { toggleTodo, removeTodo } = actions; +export default function TodoList() { + const { page, pagesize } = store.get(); + const filteredTodos = store.get('filteredTodos'); + const start = (page - 1) * pagesize; + const end = Math.min(start + pagesize, filteredTodos.length); return h3( "div.todo-list", - filteredTodos - .slice(start, end) - .map((t) => Todo(t, { toggleTodo, removeTodo })) + filteredTodos.slice(start, end).map((t) => Todo(t)) ); }
M example/assets/js/store.jsexample/assets/js/store.js

@@ -2,19 +2,48 @@ import { createStore } from "./h3.js";

const todos = (store) => { store.on("init", () => ({ todos: [], filteredTodos: [], filter: "" })); + store.on("todos/add", (state, data) => { + let todos = state.todos; + todos.unshift({ + key: `todo_${Date.now()}__${data.text}`, // Make todos "unique-enough" to ensure they are processed correctly + text: data.text, + }); + return { todos }; + }); + store.on("todos/load", () => { + const storedTodos = localStorage.getItem("h3_todo_list"); + const todos = storedTodos ? JSON.parse(storedTodos) : []; + return { todos }; + }); + store.on("todos/remove", (state, data) => { + const todos = state.todos.filter(({ key }) => key !== data.key); + return { todos }; + }); + store.on("todos/toggle", (state, data) => { + const todos = state.todos; + const todo = state.todos.find((t) => t.key === data.key); + todo.done = !todo.done; + return { todos }; + }); + store.on("todos/filter", (state, filter) => { + const todos = state.todos; + const filteredTodos = todos.filter(({ text }) => text.match(filter)); + return { filteredTodos, filter }; + }); }; -const flags = (store) => { +const error = (store) => { store.on("init", () => ({ displayEmptyTodoError: false })); - store.on("emptyTodoError.clear", (state) => ({ displayEmptyTodoError: false })); - store.on("emptyTodoError.set", (state) => ({ displayEmptyTodoError: true })); + store.on("error/clear", (state) => ({ displayEmptyTodoError: false })); + store.on("error/set", (state) => ({ displayEmptyTodoError: true })); }; const pages = (store) => { store.on("init", () => ({ pagesize: 10, page: 1 })); + store.on("pages/set", (state, page) => ({ page })); }; -const store = createStore([todos, flags, pages]); +const store = createStore([todos, error, pages]); store.dispatch("init");
M h3.jsh3.js

@@ -285,6 +285,7 @@

let store = { dispatch(event, data) { if (events[event]) { + if (event !== "log") this.dispatch("log", { event, data }); let changes = {}; let changed; events[event].forEach((i) => {

@@ -293,7 +294,7 @@ });

} }, - get: (arg) => arg ? state[arg] : state, + get: (arg) => (arg ? state[arg] : state), on(event, cb) { (events[event] || (events[event] = [])).push(cb);