all repos — h3 @ ba38ad5b41611b5e2ca05614a0acecfca29350f3

A tiny, extremely minimalist JavaScript microframework.

Fixes routing update.
h3rald h3rald@h3rald.com
Tue, 14 Apr 2020 16:53:24 +0200
commit

ba38ad5b41611b5e2ca05614a0acecfca29350f3

parent

c8fe1b23eac3be7beb33b222731bf5e65214424c

M example/assets/css/style.cssexample/assets/css/style.css

@@ -21,7 +21,7 @@ margin: auto;

text-align: center; } -.todo-list-container { +.container { padding: 15px; }

@@ -30,6 +30,7 @@ margin: 10px 0;

display: flex; justify-content: center; } + #new-todo { font-size: 2rem; height: 3rem;

@@ -37,6 +38,7 @@ width: 100%;

display: flex; justify-content: space-around; } + .submit-todo { height: 3rem; width: 4rem;

@@ -46,10 +48,27 @@ display: flex;

justify-content: space-around; cursor: pointer; } + +.options { + padding: 20px 0; +} + .navigation-bar { display: flex; justify-content: center; } + +.navigation-bar .nav-link { + font-size: 1.5em; + display: flex; + margin-right: 5px; + font-weight: bold; +} + +.nav-link { + cursor: pointer; +} + #filter-text { display: flex; justify-content: space-around;
M example/assets/js/app.jsexample/assets/js/app.js

@@ -1,42 +1,15 @@

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 modules from "./modules.js"; - -let initialized = false; - -const MainView = () => { - 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.todo-list-container", [ - h3("h1", "To Do List"), - h3("main", [AddTodoForm, EmptyTodoError, NavigationBar, TodoList]), - ]); -}; - -const SettingsView = () => { - return h3("div.settings", [ - h3("h1", "Settings"), - h3( - "a.nav-link", - { - onclick: () => h3.go("/"), - }, - "← Go Back" - ), - ]); -}; +import SettingsView from "./components/SettingsView.js"; +import MainView from "./components/MainView.js"; h3.init({ element: document.getElementById("app"), modules, + onInit: () => { + h3.dispatch("app/load"); + h3.dispatch("settings/set", h3.state("settings")); + }, routes: { "/settings": SettingsView, "/": MainView,
A example/assets/js/components/MainView.js

@@ -0,0 +1,15 @@

+import h3 from "../h3.js"; +import AddTodoForm from "./AddTodoForm.js"; +import EmptyTodoError from "./EmptyTodoError.js"; +import NavigationBar from "./NavigationBar.js"; +import TodoList from "./TodoList.js"; + +export default function () { + const { todos, filter } = h3.state(); + h3.dispatch("todos/filter", filter); + h3.dispatch("app/save", { todos: todos, settings: h3.state("settings") }); + return h3("div.container", [ + h3("h1", "To Do List"), + h3("main", [AddTodoForm, EmptyTodoError, NavigationBar, TodoList]), + ]); +}
A example/assets/js/components/SettingsView.js

@@ -0,0 +1,36 @@

+import h3 from "../h3.js"; + +export default function () { + const toggleLogging = () => { + const value = document.getElementById("options-logging").checked; + h3.dispatch("settings/set", { logging: value }); + h3.dispatch("app/save"); + }; + const attrs = { + type: "checkbox", + onclick: toggleLogging, + }; + if (h3.state("settings").logging) { + attrs.checked = true; + } + return h3("div.settings.container", [ + h3("h1", "Settings"), + h3("div.options", [ + h3("input#options-logging", attrs), + h3( + "label#options-logging-label", + { + for: "logging", + }, + "Logging" + ), + ]), + h3( + "a.nav-link", + { + onclick: () => h3.go("/"), + }, + "← Go Back" + ) + ]); +}
M example/assets/js/components/addTodoForm.jsexample/assets/js/components/addTodoForm.js

@@ -33,6 +33,7 @@ }),

h3( "span.submit-todo", { + title: "Add Todo", onclick: addTodo, }, "+"
M example/assets/js/components/navigationBar.jsexample/assets/js/components/navigationBar.js

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

import h3 from "../h3.js"; -import Paginator from "./paginator.js"; +import Paginator from "./Paginator.js"; export default function NavigationBar() { // Set the todo filter.

@@ -15,6 +15,7 @@ return h3("div.navigation-bar", [

h3( "a.nav-link", { + title: "Settings", onclick: () => h3.go("/settings"), }, "⚙"
M example/assets/js/components/todoList.jsexample/assets/js/components/todoList.js

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

import h3 from "../h3.js"; -import Todo from "./todo.js"; +import Todo from "./Todo.js"; export default function TodoList() { const { page, pagesize } = h3.state();
M example/assets/js/modules.jsexample/assets/js/modules.js

@@ -1,3 +1,30 @@

+const app = (store) => { + store.on("app/load", () => { + const storedData = localStorage.getItem("h3_todo_list"); + const { todos, settings } = storedData ? JSON.parse(storedData) : {todos: [], settings: {}}; + return { todos, settings }; + }); + store.on("app/save", (state, data) => { + localStorage.setItem( + "h3_todo_list", + JSON.stringify({ todos: state.todos, settings: state.settings }) + ); + }); +}; + +const settings = (store) => { + let removeSubscription; + store.on("$init", () => ({ settings: {} })); + store.on("settings/set", (state, data) => { + if (data.logging) { + removeSubscription = store.on("$log", (state, data) => console.log(data)); + } else { + removeSubscription && removeSubscription(); + } + return { settings: data }; + }); +}; + const todos = (store) => { store.on("$init", () => ({ todos: [], filteredTodos: [], filter: "" })); store.on("todos/add", (state, data) => {

@@ -6,11 +33,6 @@ 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) => {

@@ -41,8 +63,4 @@ store.on("$init", () => ({ pagesize: 10, page: 1 }));

store.on("pages/set", (state, page) => ({ page })); }; -export default [ - todos, - error, - pages, -]; +export default [app, todos, error, pages, settings];
M h3.jsh3.js

@@ -56,7 +56,6 @@ };

// Virtual Node Implementation with HyperScript-like syntax class VNode { - from(data) { this.type = data.type; this.value = data.value;

@@ -72,15 +71,15 @@ this.element = null;

this.attributes = {}; this.children = []; this.classList = []; - if (typeof args[0] !== 'string' && !args[1] && !args[2]) { + if (typeof args[0] !== "string" && !args[1] && !args[2]) { if (Object.prototype.toString.call(args[0]) === "[object Object]") { if (args[0] instanceof VNode) { - this.from(args[0]); - return + this.from(args[0]); + return; } else { this.type = args[0].type; this.value = args[0].value; - return + return; } } else if (typeof args[0] === "function") { const vnode = args[0]();

@@ -333,24 +332,27 @@ }

} class Router { - constructor({element, routes }) { + constructor({ element, routes }) { this.element = element; if (!this.element) { - throw new Error( - `[Router] No view element specified.` - ); + throw new Error(`[Router] No view element specified.`); } if (!routes || Object.keys(routes).length === 0) { throw new Error("[Router] No routes defined."); } const defs = Object.keys(routes); - this.fallback = defs[defs.length-1]; + this.fallback = defs[defs.length - 1]; this.routes = routes; } start() { const processPath = (data) => { - const hash = (data && data.newURL && data.newURL.match(/(#.+)$/) && data.newURL.match(/(#.+)$/)[1] || window.location.hash); + const hash = + (data && + data.newURL && + data.newURL.match(/(#.+)$/) && + data.newURL.match(/(#.+)$/)[1]) || + window.location.hash; const path = hash.replace(/\?.+$/, "").slice(1); const rawQuery = hash.match(/\?(.+)$/); const query = rawQuery && rawQuery[1] ? rawQuery[1] : "";

@@ -387,6 +389,7 @@ while (this.element.firstChild) {

this.element.removeChild(this.element.firstChild); } this.element.appendChild(this.routes[this.route.def]().render()); + defineUpdateFn(this.element); // TODO Refactor this }; processPath(); window.addEventListener("hashchange", processPath);

@@ -411,9 +414,19 @@ let store = null;

let router = null; let updateFn = null; -h3.init = ({element, routes, modules}) => { +// TODO Refactor this +const defineUpdateFn = (element) => { + // Configure update function + let vnode = router.routes[router.route.def](); + updateFn = () => { + const fn = router.routes[router.route.def]; + vnode.update({ node: element.childNodes[0], vnode: fn() }); + }; +}; + +h3.init = ({ element, routes, modules, onInit }) => { if (!(element instanceof Element)) { - throw new Error('Invalid element specified.'); + throw new Error("Invalid element specified."); } // Initialize store store = new Store();

@@ -422,56 +435,56 @@ if (i) i(store);

}); store.dispatch("$init"); // Initialize router - router = new Router({element, routes}) + router = new Router({ element, routes }); + onInit && onInit(); router.start(); - // Configure update function - const vnode = router.routes[router.route.def](); - updateFn = () => { - const fn = router.routes[router.route.def]; - vnode.update({node: element.childNodes[0], vnode: fn()}) - } -} + defineUpdateFn(); // TODO Refactor this +}; h3.go = (path, params) => { if (!router) { - throw new Error('No application initialized, unable to navigate.'); + 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.'); + 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.'); + 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.'); + 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.'); + 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.'); + throw new Error("No application initialized, unable to update."); } updateFn(); -} +}; export default h3;