Skip to content
On this page

Quick Start

Build a complete Todo app in 10 minutes. This is a hands-on introduction to Inglorious Web.

Project Setup

Create a new directory and initialize the project:

bash
mkdir todo-app
cd todo-app
npm init -y
npm install @inglorious/web
1
2
3
4

Create index.html:

html
<!DOCTYPE html>
<html>
  <head>
    <title>Todo App</title>
    <style>
      body {
        font-family: sans-serif;
        max-width: 600px;
        margin: 50px auto;
        padding: 20px;
      }
      .todo-item {
        padding: 10px;
        margin: 5px 0;
        border: 1px solid #ddd;
        border-radius: 4px;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      .todo-item.completed {
        background: #f0f0f0;
        text-decoration: line-through;
      }
      button {
        padding: 5px 10px;
        margin: 0 5px;
        cursor: pointer;
      }
      input {
        flex: 1;
        padding: 8px;
        font-size: 1rem;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./main.js"></script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

Define Your Types

Create main.js with your entity types:

javascript
import { createStore, mount, html } from "@inglorious/web"

// Define entity types
const types = {
  todoApp: {
    addTodo(entity, title) {
      if (title.trim()) {
        const id = Date.now()
        entity.todos[id] = { id, title, completed: false }
        entity.input = ""
      }
    },

    updateInput(entity, value) {
      entity.input = value
    },

    toggleTodo(entity, id) {
      entity.todos[id].completed = !entity.todos[id].completed
    },

    deleteTodo(entity, id) {
      delete entity.todos[id]
    },

    render(entity, api) {
      const todos = Object.values(entity.todos)
      const completed = todos.filter((t) => t.completed).length

      return html`
        <div class="todo-app">
          <h1>My Todos</h1>
          <p>${completed} of ${todos.length} completed</p>

          <div class="input-group">
            <input
              type="text"
              placeholder="Add a new todo..."
              value="${entity.input}"
              @input=${(e) =>
                api.notify("#todoApp:updateInput", e.target.value)}
              @keydown=${(e) => {
                if (e.key === "Enter") {
                  api.notify("#todoApp:addTodo", entity.input)
                }
              }}
            />
            <button
              @click=${() => api.notify("#todoApp:addTodo", entity.input)}
            >
              Add
            </button>
          </div>

          <ul class="todo-list">
            ${todos.map(
              (todo) => html`
                <li class="todo-item ${todo.completed ? "completed" : ""}">
                  <span>${todo.title}</span>
                  <div>
                    <button
                      @click=${() => api.notify("#todoApp:toggleTodo", todo.id)}
                    >
                      ${todo.completed ? "Undo" : "Done"}
                    </button>
                    <button
                      @click=${() => api.notify("#todoApp:deleteTodo", todo.id)}
                    >
                      Delete
                    </button>
                  </div>
                </li>
              `,
            )}
          </ul>
        </div>
      `
    },
  },
}

// Create store with initial state
const store = createStore({
  types,
  entities: {
    todoApp: {
      type: "todoApp",
      input: "",
      todos: {},
    },
  },
})

// Mount the app
mount(store, (api) => api.render("todoApp"), document.getElementById("root"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

Run It

Start a simple HTTP server:

bash
# Using Python
python -m http.server 8000

# Using Node (with http-server package)
npx http-server

# Using Deno
deno run --allow-net https://deno.land/std/http/file_server.ts
1
2
3
4
5
6
7
8

Then visit http://localhost:8000 and you have a working todo app! 🎉

What Just Happened?

1. Entity Type Definition

javascript
const types = {
  todoApp: {
    // Event handlers (mutate state)
    addTodo(entity, title) {
      /* ... */
    },
    toggleTodo(entity, id) {
      /* ... */
    },

    // Render method (returns template)
    render(entity, api) {
      /* ... */
    },
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

2. Store Creation

javascript
const store = createStore({
  types, // Type definitions
  entities: {
    todoApp: {
      // Entity with initial state
      type: "todoApp",
      input: "",
      todos: {},
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11

3. App Mounting

javascript
mount(store, (api) => api.render("todoApp"), document.getElementById("root"))
1

When state changes:

  • Render function runs completely
  • lit-html diffs the template
  • Only changed DOM nodes update

Extending the App

Add Filtering

javascript
const types = {
  todoApp: {
    // ... existing methods

    setFilter(entity, filter) {
      entity.filter = filter // 'all', 'active', 'completed'
    },

    render(entity, api) {
      // Filter todos based on current filter
      let todos = Object.values(entity.todos)
      if (entity.filter === "active") {
        todos = todos.filter((t) => !t.completed)
      } else if (entity.filter === "completed") {
        todos = todos.filter((t) => t.completed)
      }

      return html`
        <div>
          <!-- ... existing todo list ... -->

          <div class="filters">
            <button @click=${() => api.notify("#todoApp:setFilter", "all")}>
              All
            </button>
            <button @click=${() => api.notify("#todoApp:setFilter", "active")}>
              Active
            </button>
            <button
              @click=${() => api.notify("#todoApp:setFilter", "completed")}
            >
              Completed
            </button>
          </div>
        </div>
      `
    },
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

Add Persistence

javascript
const types = {
  todoApp: {
    create(entity) {
      // Load from localStorage
      const saved = localStorage.getItem("todos")
      if (saved) {
        entity.todos = JSON.parse(saved)
      }
    },

    addTodo(entity, title) {
      // ... existing code ...
      // Save after mutation
      localStorage.setItem("todos", JSON.stringify(entity.todos))
    },

    // Other methods also save...
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Key Patterns

Pattern 1: Event Dispatch

javascript
// Dispatch event from render
<button @click=${() => api.notify('#todoApp:addTodo', entity.input)}>
  Add
</button>

// Handler receives payload
addTodo(entity, payload) {
  entity.title = payload
}
1
2
3
4
5
6
7
8
9

Pattern 2: Conditional Rendering

javascript
render(entity, api) {
  if (entity.todos.length === 0) {
    return html`<p>No todos yet!</p>`
  }

  return html`<ul>${/* render todos */}</ul>`
}
1
2
3
4
5
6
7

Pattern 3: Entity Composition

For larger apps, break into multiple entities:

javascript
const types = {
  app: {
    render(entity, api) {
      return html`
        <div>
          ${api.render("header")} ${api.render("todoList")}
          ${api.render("footer")}
        </div>
      `
    },
  },
  header: {
    /* ... */
  },
  todoList: {
    /* ... */
  },
  footer: {
    /* ... */
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Next Steps

Happy building! 🚀

Released under the MIT License.