'B' is for Blocks
Learn how to create and use blocks in LegoDOM.
What is a Block?
A block is a reusable, self-contained piece of UI with its own template, styles, and logic. In other frameworks (and previous versions of LegoDOM), these are called "components". We call them Blocks because you use them to build your application.
Note That
style tags outside Lego Files are inside the <template> tag. And outside the tag i.e. on the same level as the <template> tag in Lego Files.
<template b-id="user-badge">
<style>
self {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: #f0f0f0;
border-radius: 20px;
}
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
</style>
<img class="avatar" src="[[ avatarUrl ]]" alt="[[ name ]]">
<span>[[ name ]]</span>
</template>Creating Blocks
Method 1: HTML Templates
Define blocks directly in your HTML with <template b-id>:
<template b-id="hello-world" b-logic="{ name: 'Default User' }">
<h1>Hello [[ name ]]!</h1>
</template>
<!-- Uses the default "Default User" -->
<hello-world></hello-world>
> **Note:** Once initialized, the block will automatically receive a `b-id="hello-world"` attribute. This is useful for [preventing FOUC](/tutorial/faq#how-do-i-prevent-flash-of-unstyled-content-fouc).
<!-- Overrides the default with "Alice" -->
<hello-world b-logic="{ name: 'Alice' }"></hello-world>Method 2: JavaScript
Use Lego.block() for programmatic block creation:
Lego.block('hello-world', `
<h1>Hello [[ name ]]!</h1>
`, {
name: 'Alice'
});Method 3: Lego Files / Single File Blocks (.lego)
With Vite, use .lego files:
<!-- hello-world.lego -->
<script>
export default {
name: 'Alice'
}
</script>
<template>
<h1>Hello [[ name ]]!</h1>
</template>Block State (Logic)
State is defined in the block's logic object via the b-logic attribute:
{
// Data properties
count: 0,
username: 'Alice',
items: ['apple', 'banana'],
// Methods
increment() {
this.count++;
},
addItem(item) {
this.items.push(item);
}
}Access state in templates using [[ ]]:
<p>Count: [[ count ]]</p>
<button @click="increment()">+1</button>Passing Data
Via b-logic Attribute
<user-card b-logic="{
name: 'Bob',
email: 'bob@example.com',
role: 'admin'
}"></user-card>Data Merging (The Three Tiers)
LegoDOM uses a sophisticated three-tier merging strategy to initialize block state. This allows you to define defaults at the block level, customize them in templates, and then override them for specific instances.
The priority is as follows (last one wins):
- Tier 1: Script Logic - Data defined in
Lego.block()or exported from a.legofile. - Tier 2: Template Defaults - Data defined on the
<template b-logic="...">attribute. - Tier 3: Instance Overrides - Data defined on the actual block tag
<my-comp b-logic="...">.
Example of Merging
<!-- 1. Script Logic (Defined in JS) -->
<script>
Lego.block('user-card', `...`, { role: 'guest', theme: 'light' });
</script>
<!-- 2. Template Defaults (Defined in HTML) -->
<template b-id="user-card" b-logic="{ role: 'member', name: 'Anonymous' }">
...
</template>
<!-- 3. Instance Overrides -->
<user-card b-logic="{ name: 'Alice' }"></user-card>In the example above, the final state for the block will be:
role:'member'(Template override beats Script)theme:'light'(Only defined in Script)name:'Alice'(Instance override beats Template)
Declaring Props (b-props)
b-logic accepts any keys, which is flexible but easy to typo. Declare a contract on the outer <template> to get dev-time warnings when a parent passes an unexpected key or omits a required one:
<template b-id="user-card" b-props="{ user, onSelect, title }">
<h2>[[ title ]]</h2>
<p>[[ user.name ]]</p>
</template>b-props is pure metadata, it does not introduce a new data channel. Parents still pass values via b-logic exactly as before:
<user-card b-logic="{ user: currentUser, onSelect: pick }"></user-card>What it gets you:
- Typo guard: passing
b-logic="{ usre: ... }"warns thatusreis not declared. - Missing-prop warning: declaring
{ user }and not providing it (and no script default) warns at mount. - Tooling boundary: editor extensions and devtools can read
b-propsto know which keys are external-by-contract.
Defaults still come from the script tier, if title: 'User' is in the script, title is satisfied even when the parent omits it.
Syntax rules
- Identifiers only:
b-props="{ age, name, dateOfBirth }". Whitespace is tolerant; outer braces are optional. - No values, defaults, or renames.
b-props="{ name: 'Alice' }"is a parse error, useb-logicfor values. - Omitting
b-propskeeps existing behavior: anyb-logickey merges in, no warnings.
Block Communication
Parent → Child (Scoped Props)
Pass data via b-logic naturally. The evaluation context includes the parent block's state, effectively making b-logic behave like "scoped props".
<!-- `user` comes from the parent's state! -->
<child-block b-logic="{ user: user }"></child-block>Variables inside b-logic are resolved in this order:
- Parent Block State: (e.g.
this.user) - Global Scope: (e.g.
window.user,global.xxx)
Accessing Parent ($parent)
For tighter coupling, you can access the immediate block ancestor using $parent:
In Template:
<span>Parent is: [[ $parent.tagName ]]</span>In Script:
mounted() {
console.log(this.$parent); // Returns parent Block element (or undefined)
}NOTE
$parent automatically skips non-block elements (like <div> wrappers) to find the nearest Lego Block ancestor.
Child → Parent (Events)
Use $emit() to dispatch custom events:
<!-- Child block -->
<button @click="$emit('save', { id: 123 })">Save</button>// Parent listens
document.querySelector('child-block')
.addEventListener('save', (e) => {
console.log('Saved:', e.detail); // { id: 123 }
});Accessing Ancestors
Use $ancestors() to get a parent block:
<!-- In nested block -->
<p>App title: [[ $ancestors('app-root').state.title ]]</p>Read-Only
$ancestors() should be used for reading parent state, not mutating it.
Block Composition
Nesting Blocks
<template b-id="app-layout">
<header>
<app-header></app-header>
</header>
<main>
<app-sidebar></app-sidebar>
<app-content></app-content>
</main>
</template>Using Slots
Standard Web Components slots work:
<template b-id="card-container">
<div class="card">
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
</template>
<!-- Usage -->
<card-container>
<h2 slot="header">Title</h2>
<p>Main content</p>
<button slot="footer">Action</button>
</card-container>Shadow DOM
All blocks use Shadow DOM for style encapsulation.
Benefits
✅ Scoped Styles - CSS doesn't leak in or out
✅ No Naming Conflicts - ID/class names are isolated
✅ Composability - Blocks work without side effects
Styling the Host
Use self keyword (converts to :host):
<style>
self {
display: block;
padding: 1rem;
}
self:hover {
background: #f5f5f5;
}
</style>Lifecycle
Blocks have three lifecycle hooks:
{
mounted() {
// Block added to DOM
this.fetchData();
},
updated() {
// State changed and re-rendered
console.log('New count:', this.count);
},
unmounted() {
// Block removed from DOM
clearInterval(this.timer);
}
}See Lifecycle Hooks for details.
Best Practices
1. Keep Blocks Small
Each block should have a single responsibility.
✅ Good: user-avatar, user-name, user-bio
❌ Bad: entire-user-profile-page
2. Use Semantic Names
Name blocks after what they represent:
✅ Good: product-card, search-bar
❌ Bad: blue-box, flex-container
3. Avoid Deep Nesting
Keep block trees shallow (3-4 levels max):
app-root
├── app-header
│ └── nav-menu
├── app-main
│ └── content-area
└── app-footer4. Initialize State in mounted()
Fetch data or set up timers in mounted():
{
data: null,
mounted() {
this.fetchData();
},
async fetchData() {
this.data = await fetch('/api/data').then(r => r.json());
}
}5. Clean Up in unmounted()
Clear timers, remove listeners:
{
timer: null,
mounted() {
this.timer = setInterval(() => this.tick(), 1000);
},
unmounted() {
clearInterval(this.timer);
}
}Next Steps
- Learn about Reactivity in depth
- Explore Templating features
- See complete examples