'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>
<!-- 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 -->
<template>
<h1>Hello [[ name ]]!</h1>
</template>
<script>
export default {
name: 'Alice'
}
</script>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>Note:
b-datais supported as a backward-compatible alias forb-logic.
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 is an alias for Lego.block
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)
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