Skip to content

1. Project setup

We need a project to work in. Four commands and you'll have a dev server with hot reload and a working block on screen. After that, every chapter is just editing files.

Scaffold

bash
npm create legodom@latest task-tracker
cd task-tracker
npm install
npm run dev

Open http://localhost:5173. You should see the sample LegoDOM app. Click the count button. It increments. That's a .lego block in action, and it's about as straightforward as it looks.

What you got

task-tracker/
├── index.html              ← entry HTML
├── vite.config.js          ← Vite + LegoDOM plugin
├── package.json
└── src/
    ├── app.js              ← entry script (registers blocks, calls Lego.init)
    ├── style.css           ← global CSS (light DOM only, doesn't enter blocks)
    └── my-app.lego         ← sample block

Two things to remember about this layout:

  1. .lego files live anywhere under src/. The Vite plugin discovers them via glob.
  2. Filename becomes the tag. user-card.lego registers as <user-card>. UserCard.lego and user_card.lego both kebab-case to the same tag. Any filename that doesn't produce a hyphenated name is rejected (browser custom-element rules).

src/app.js

js
import { Lego } from 'lego-dom';
import registerBlocks from 'virtual:lego-blocks';
import './style.css';

registerBlocks();
Lego.init();

Two imports do the heavy lifting. virtual:lego-blocks is generated by the plugin; importing it pulls in every .lego file in src/, so they all register on startup. Lego.init() then boots the engine.

index.html

html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Task Tracker</title>
  </head>
  <body>
    <my-app></my-app>
    <script type="module" src="/src/app.js"></script>
  </body>
</html>

Notice what's not there: no mount API, no virtual root, no ReactDOM.createRoot. The <my-app> tag goes in the HTML as if it were a built-in element, because as far as the browser is concerned, it is one.

Verify HMR

Open src/my-app.lego, change the heading text, save.

The change appears with no page reload. The existing <my-app> element is unsnapped, the new template is registered, and the same element is re-snapped in place. Routes, scroll position, persistent state (anything you'll add in later chapters) survive the swap. This is HMR done right, and you'll appreciate it more once you have an app big enough that a full reload would lose context.

What Lego.init() is doing under the hood

Briefly, in case you're curious. You don't need to memorize this:

  1. Pre-loads any stylesheets you registered (none yet).
  2. Runs an initial route match against the URL so $route is correct on first paint (only matters once you have routes).
  3. Registers any <template b-id> it finds in your HTML as custom elements (your .lego files have already done their own registration via the plugin).
  4. Wires up popstate, <a b-link|b-target> clicks, and <form b-action> submits.
  5. Starts cross-tab $db sync.

Most of these don't matter to you yet. They exist so that the things you'll add in later chapters all just work.

Ready

A project, a dev server, a sample block. In the next chapter you'll throw the sample away and build the first real component of the task tracker.

Next: Your first block →

Released under the MIT License.