all repos — h3 @ 27341c470a6e198ec42dc525683381285d76fd00

A tiny, extremely minimalist JavaScript microframework.

Refactoring using new API.
h3rald h3rald@h3rald.com
Mon, 13 Apr 2020 22:14:44 +0200
commit

27341c470a6e198ec42dc525683381285d76fd00

parent

25dfab636dbe8070597e23bdc5d2f678aea307db

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

@@ -1,13 +1,19 @@

-import h3, { createApp } from "./h3.js"; +import h3 from "./h3.js"; import AddTodoForm from "./components/addTodoForm.js"; import EmptyTodoError from "./components/emptyTodoError.js"; import NavigationBar from "./components/navigationBar.js"; import TodoList from "./components/todoList.js"; -import store from "./store.js"; +import modules from "./modules.js"; + +let initialized = false; const app = () => { - const { todos, filteredTodos, filter } = store.get(); - store.dispatch("todos/filter", filter); + if (!initialized) { + h3.dispatch("todos/load"); + } + initialized = true; + const { todos, filteredTodos, filter } = h3.state(); + h3.dispatch("todos/filter", filter); localStorage.setItem("h3_todo_list", JSON.stringify(todos)); return h3("div#todolist.todo-list-container", [ h3("h1", "To Do List"),

@@ -19,8 +25,12 @@ ])

]); } -store.dispatch("todos/load"); - -const update = createApp('app', app); +h3.init({ + element: document.getElementById('app'), + modules, + component: app + /*routes: { + "/": app + }*/ +}); -store.on('$update', update);
M example/assets/js/components/addTodoForm.jsexample/assets/js/components/addTodoForm.js

@@ -1,22 +1,21 @@

import h3 from "../h3.js"; -import store from "../store.js"; export default function AddTodoForm() { const addTodo = () => { const newTodo = document.getElementById("new-todo"); if (!newTodo.value) { - store.dispatch("error/set"); - store.dispatch("$update"); + h3.dispatch("error/set"); + h3.dispatch("$update"); document.getElementById("new-todo").focus(); return; } - store.dispatch("error/clear"); - store.dispatch("todos/add", { + h3.dispatch("error/clear"); + h3.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("$update"); + h3.dispatch("$update"); document.getElementById("new-todo").focus(); }; const addTodoOnEnter = (event) => {
M example/assets/js/components/emptyTodoError.jsexample/assets/js/components/emptyTodoError.js

@@ -1,11 +1,10 @@

import h3 from "../h3.js"; -import store from "../store.js"; export default function EmptyTodoError(data, actions) { - const emptyTodoErrorClass = store.get('displayEmptyTodoError') ? "" : ".hidden"; + const emptyTodoErrorClass = h3.state('displayEmptyTodoError') ? "" : ".hidden"; const clearError = () => { - store.dispatch('error/clear'); - store.dispatch('$update'); + h3.dispatch('error/clear'); + h3.dispatch('$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,13 +1,12 @@

import h3 from "../h3.js"; import Paginator from "./paginator.js"; -import store from "../store.js"; export default function NavigationBar() { // Set the todo filter. const setFilter = () => { let f = document.getElementById("filter-text"); - store.dispatch("todos/filter", f.value); - store.dispatch("$update"); + h3.dispatch("todos/filter", f.value); + h3.dispatch("$update"); f = document.getElementById("filter-text"); f.focus(); };
M example/assets/js/components/paginator.jsexample/assets/js/components/paginator.js

@@ -1,32 +1,31 @@

import h3 from "../h3.js"; -import store from "../store.js"; export default function Paginator() { const hash = window.location.hash; - let { page, pagesize, filteredTodos } = store.get(); + let { page, pagesize, filteredTodos } = h3.state(); 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); + h3.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() { - const page = store.get('page'); + const page = h3.state('page'); const newPage = page - 1; - store.dispatch("pages/set", newPage); + h3.dispatch("pages/set", newPage); window.location.hash = `/?page=${newPage}`; - store.dispatch("$update"); + h3.dispatch("$update"); } function setNextPage() { - const page = store.get('page'); + const page = h3.state('page'); const newPage = page + 1; - store.dispatch("pages/set", newPage); + h3.dispatch("pages/set", newPage); window.location.hash = `/?page=${newPage}`; - store.dispatch("$update"); + h3.dispatch("$update"); } return h3("div.paginator", [ h3(
M example/assets/js/components/todo.jsexample/assets/js/components/todo.js

@@ -1,15 +1,14 @@

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

@@ -1,10 +1,9 @@

import h3 from "../h3.js"; import Todo from "./todo.js"; -import store from "../store.js"; export default function TodoList() { - const { page, pagesize } = store.get(); - const filteredTodos = store.get('filteredTodos'); + const { page, pagesize } = h3.state(); + const filteredTodos = h3.state('filteredTodos'); const start = (page - 1) * pagesize; const end = Math.min(start + pagesize, filteredTodos.length); return h3(
M example/assets/js/store.jsexample/assets/js/modules.js

@@ -1,7 +1,5 @@

-import { createStore } from "./h3.js"; - const todos = (store) => { - store.on("init", () => ({ todos: [], filteredTodos: [], filter: "" })); + store.on("$init", () => ({ todos: [], filteredTodos: [], filter: "" })); store.on("todos/add", (state, data) => { let todos = state.todos; todos.unshift({

@@ -33,18 +31,18 @@ });

}; const error = (store) => { - store.on("init", () => ({ displayEmptyTodoError: false })); + store.on("$init", () => ({ displayEmptyTodoError: false })); 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("$init", () => ({ pagesize: 10, page: 1 })); store.on("pages/set", (state, page) => ({ page })); }; -const store = createStore([todos, error, pages]); - -store.dispatch("init"); - -export default store; +export default [ + todos, + error, + pages, +];
M h3.jsh3.js

@@ -324,19 +324,10 @@

return store; }; - -const createApp = (id, builder) => { - const vnode = builder(); - document.getElementById(id).appendChild(vnode.render()); - return () => { - vnode.update({vnode: builder()}); - }; -}; - class Route { - constructor({ path, route, query, parts, fallback }) { + constructor({ path, def, query, parts, fallback }) { this.path = path; - this.route = route; + this.def = def; this.query = query; this.parts = parts; this.fallback = fallback;

@@ -352,18 +343,18 @@ }

} class Router { - constructor({ id, fallback, element, routes }) { - this.id = id; - this.element = element || document.getElementById(id); + constructor({element, routes }) { + this.element = element; if (!this.element) { throw new Error( - `[Router] No view element specified, neither via element or id.` + `[Router] No view element specified.` ); } if (!routes || Object.keys(routes).length === 0) { throw new Error("[Router] No routes defined."); } - this.fallback = fallback || Object.keys(routes)[0]; + const defs = Object.keys(routes); + this.fallback = defs[defs.length-1]; this.routes = routes; }

@@ -374,8 +365,8 @@ const rawQuery = window.location.hash.match(/\?(.+)$/);

const query = rawQuery && rawQuery[1] ? rawQuery[1] : ""; const pathParts = path.split("/").slice(1); let parts = {}; - for (let route of Object.keys(this.routes)) { - let routeParts = route.split("/").slice(1); + for (let def of Object.keys(this.routes)) { + let routeParts = def.split("/").slice(1); let match = true; let index = 0; parts = {};

@@ -391,19 +382,19 @@ index++;

} if (match) { let fallback = false; - this.route = new Route({ query, path, route, parts, fallback }); + this.route = new Route({ query, path, def, parts, fallback }); } } if (!this.route) { - let route = this.fallback; + let def = this.fallback; let fallback = true; - this.route = new Route({ query, path, route, parts, fallback }); + this.route = new Route({ query, path, def, parts, fallback }); } // Display View while (this.element.firstChild) { this.element.removeChild(this.element.firstChild); } - this.element.appendChild(this.routes[this.route.route].render()); + this.element.appendChild(this.routes[this.route.def].render()); }; processPath(); window.addEventListener("hashchange", processPath);

@@ -418,13 +409,80 @@ window.location.hash = `#${path}${query}`;

} } -const createRouter = (data) => { - return new Router(data); -}; +// High Level API const h3 = (...args) => { return new VNode(...args); }; -export { createStore, createApp, createRouter }; +let store = null; +let router = null; +let updateFn = null; +let vnode = null; + +h3.init = ({element, routes, modules, component}) => { + if (!(element instanceof Element)) { + throw new Error('Invalid element specified.'); + } + // Initialize store + store = new Store(); + (modules || []).forEach((i) => { + if (i) i(store); + }); + store.dispatch("$init"); + // Initialize component + vnode = component(); + updateFn = () => { + vnode.update({vnode: component()}); + } + // Initialize router + //router = new Router({element, routes}) + // Render + //router.start(); + store.on('$update', updateFn); + element.appendChild(vnode.render()); +} + +h3.go = (path, params) => { + if (!router) { + throw new Error('No application initialized, unable to navigate.'); + } + return router.go(path, params); +} + +h3.route = () => { + if (!router) { + throw new Error('No application initialized, unable to retrieve current route.'); + } + return router.route; +} + +h3.state = (key) => { + if (!store) { + throw new Error('No application initialized, unable to retrieve current state.'); + } + return store.get(key); +} + +h3.on = (event, cb) => { + if (!store) { + throw new Error('No application initialized, unable to listen to events.'); + } + return store.on(event, cb); +} + +h3.dispatch = (event, data) => { + if (!store) { + throw new Error('No application initialized, unable to dispatch events.'); + } + return store.dispatch(event, data); +} + +h3.update = () => { + if (!updateFn) { + throw new Error('No application initialized, unable to update.'); + } + updateFn(); +} + export default h3;