This project is a simple to-do application built using a custom JavaScript framework all implemented from scratch without importing any libraries like React or Vue. This project as a foundation for exploring how frontend frameworks work under the hood, or as a base for building more advanced applications using your own tooling.
-
Virtual DOM System
- Uses createVNode(tag, attrs, children) to define virtual nodes.
- Nodes can represent HTML tags, attributes, event handlers, and children.
- Supports optional key for diffing optimization.
-
Custom Renderer
- renderElement(vnode) recursively transforms VNodes into real DOM elements.
- Handles attributes, event listeners, and nested children.
-
DOM Event Handling
- Automatically binds events through virtual DOM (onClick, onChange, etc.).
- Event listeners are parsed from VNode attributes.
-
Manual DOM Event Binding Support
- Adds listeners post-render for specific DOM targets (e.g., .todo-list, .new-todo).
- Separate file (events.js) manages additional manual handlers.
-
Element Nesting
- Child elements are created by passing an array of VNodes in the children argument.
- Deep nesting supported (e.g., headers containing inputs and headings).
- Rendering function processes child arrays recursively.
Creating an element in the mini-framework involves defining a virtual DOM node using the createVNode function. This function allows you to specify the tag, attributes, and children of the element you want to create. Below is a step-by-step guide on how to use this function effectively.
Before you can create an element, ensure that you import the createVNode function from the framework:
import { createVNode } from 'mini-framework';You can create a virtual DOM node by calling the createVNode function. The function takes the following parameters:
- tag: A string representing the HTML tag (e.g., 'div', 'span', 'button').
- attrs: An object containing the attributes you want to set on the element (e.g.,
class,id,onClick). - children: An array of child nodes, which can be strings (text nodes) or other virtual DOM nodes.
- key (optional): A unique key for the node, useful for diffing.
Here’s an example of creating a simple button element:
createVNode(
"button",
{
class: "clear-completed",
style: hasCompleted ? "display: block" : "display: none;",
},
hasCompleted ? ["Clear completed"] : []
),In this example:
- The tag is
'button'. - The attributes include a class and an event handler for the click event.
- The children consist of a single text node,
'Clear Completed'.
Once you have defined your virtual DOM node, you can render it into the real DOM using the renderElement function:
const appRoot = document.getElementById('app');
const renderedButton = renderElement(button);
appRoot.appendChild(renderedButton);This mini-framework creates and handles events in two main ways:
In vdom.js, when rendering elements, the framework checks for attributes that start with on (like onclick, onchange, etc.) and automatically attaches them as event listeners:
Here's an example:
for (const [key, value] of Object.entries(node.attrs || {})) {
if (key.startsWith("on") && typeof value === "function") {
const eventName = key.slice(2).toLowerCase();
el.addEventListener(eventName, value);
} else {
el.setAttribute(key, value);
}
}In events.js, the framework sets up event listeners directly on DOM elements after rendering. For example, it attaches handlers for clicks, keydowns, changes, etc., on elements like .todo-list and .new-todo:
Here's an example:
document.querySelector('.new-todo').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
}
});These handlers then call state management functions or update the UI.
Nesting elements in this mini-framework is handled through the children property of the virtual node objects, as defined in createVNode and rendered by the renderElement function in vdom.js.
You create a virtual node (VNode) for each element. To nest elements, pass an array of child VNodes as the children argument:
// Example: Nesting a <header> with <h1> children
const headerVNode = createVNode("header", { class: "header" }, [
createVNode("h1", {}, ["todos"]),
createVNode("input", {
class: "new-todo",
placeholder: "What needs to be done?",
autofocus: "",
}),
]);The renderElement function in vdom.js recursively renders each node and its children:
if (node.children && Array.isArray(node.children)) {
node.children.forEach((child) => {
if (child === undefined || child === null) return;
el.appendChild(renderElement(child));
});
}
return el;A global state object holds the entire application state. Any action that modifies the state triggers a re-render.
let state = {
todos: [],
filter: "all",
editingId: null,
};
let listeners = [];
export function getState() {
return state;
}Simple routing is achieved by syncing the app state with the URL using the hashchange event. This lets users navigate to #/active, #/completed, etc., and updates the visible todos based on the URL.
handleRoute() {
const path = window.location.hash.slice(1) || '/';
this.currentRoute = path;
const handler = this.routes[path];
if (handler) {
handler();
}
}npm start
# or
python3 -m http.server 8080Then open http://localhost:8080/todoMVC/