Skip to content

Todo Tutorial

Build a simple Todo app using the following core concepts:

  • Reactivity
  • Templating
  • Events

Start by creating signals to hold our todos and input value. We don’t need to import computed when creating or derived signals inside a component, just wrap the logic in a function.

import { signal, effect } from "@hellajs/core";
function Todo() {
const todos = signal([]);
const inputValue = signal("");
const todoCount = () => todos().length;
const hasTodos = () => todoCount() > 0;
const isDisabled = () => !inputValue();
effect(() => {
console.log("Todo count:", todoCount());
});
// ...rest of the code will go here...
}

Now let’s create some functions to setInput and addTodo. For this simple example we’re not doing any extra checks like checking for duplicate todos or null event targets.

// ....continued...
const setInput = (event) => {
inputValue.set(event.target.value);
};
const addTodo = () => {
todos.set([...todos(), inputValue()]);
inputValue.set("");
};

Create the UI using a simple function templating system. Update the imports to include the ergonomic html proxy helper, we can destructure the elements if desired to use div instead of html.div. We also need forEach to loop over the todos and show for conditional rendering.

import { signal, effect } from "@hellajs/core";
import { forEach, show, html } from "@hellajs/dom";
const { div, input, button, ul, li, p } = html;

For dynamic attributes and text we need to pass a function reference. There are a couple of ways to handle dynamic string as shown below

// ...continued...
return div(
// use template strings
p(() => `Todo List (${todoCount()})`),
// or the p(...children) spread pattern
p("Todo List (", todoCount, ")"),
div(
input({
type: "text",
value: inputValue,
oninput: setInput,
}),
button({ onclick: addTodo, disabled: isDisabled }, "Add"),
),
show(
hasTodos,
ul(forEach(todos, (todo) => li(todo))),
div("No todos yet!"),
),
);

Add mount to your imports and pass the Todo function reference. By default, mount looks for #app element but you can pass any element id as a querySelector.

import { signal, effect } from "@hellajs/core";
import { forEach, show, html, mount } from "@hellajs/dom";
const { div, input, button, ul, li, p } = html;
function Todo() {
// Todo implementation...
}
mount(Todo, '#todo');
import { signal, effect } from "@hellajs/core";
import { forEach, show, html, mount } from "@hellajs/dom";
const { div, input, button, ul, li, p } = html;
function Todo() {
const todos = signal([]);
const inputValue = signal("");
const todoCount = () => todos().length;
const hasTodos = () => todoCount() > 0;
const isDisabled = () => !inputValue();
effect(() => {
console.log("Todo count:", todoCount());
});
const setInput = (event) => {
inputValue.set(event.target.value);
};
const addTodo = () => {
todos.set([...todos(), inputValue()]);
inputValue.set("");
};
return div(
p("Todo List (", todoCount, ")"),
div(
input({
type: "text",
value: inputValue,
oninput: setInput,
}),
button({ onclick: addTodo, disabled: isDisabled }, "Add"),
),
show(
hasTodos,
ul(forEach(todos, (todo) => li(todo))),
div("No todos yet!"),
),
);
}
mount(Todo, '#todo');