Now.js Framework Documentation
ComponentManager
ComponentManager
Overview
ComponentManager is the component management system in the Now.js Framework that enables you to create reusable components with lifecycle management, state management, and event handling.
When to use:
- Need to create reusable UI components
- Need component lifecycle (created, mounted, destroyed)
- Need internal state management per component
- Need declarative event handling
Why use it:
- ✅ Component lifecycle hooks (8 hooks)
- ✅ Reactive state for auto-update
- ✅ Template processing with data binding
- ✅ Event delegation and custom events
- ✅ Props extraction from attributes
- ✅ Virtual DOM patching
Basic Usage
Creating a Component
// js/components/counter.js
ComponentManager.define('counter', {
// Template HTML
template: `
<div class="counter">
<span class="count">{{ count }}</span>
<button data-ref="increment">+</button>
<button data-ref="decrement">-</button>
</div>
`,
// Initial state
state: {
count: 0
},
// Methods
methods: {
increment() {
this.state.count++;
this.render();
},
decrement() {
this.state.count--;
this.render();
}
},
// Event handlers
events: {
'click [data-ref="increment"]': function(e) {
this.methods.increment();
},
'click [data-ref="decrement"]': function(e) {
this.methods.decrement();
}
},
// Lifecycle hooks
mounted() {
console.log('Counter mounted!', this.element);
}
});Using in HTML
<!-- Basic usage -->
<div data-component="counter"></div>
<!-- With props -->
<div data-component="counter" data-count="10"></div>
<!-- With custom template -->
<div data-component="counter">
<div class="custom-counter">
<span>{{ count }}</span>
<button data-ref="increment">Add</button>
</div>
</div>Lifecycle Hooks
ComponentManager has 8 lifecycle hooks in execution order:
graph TD
A[define] --> B[beforeCreate]
B --> C[created]
C --> D[beforeMount]
D --> E[processTemplate]
E --> F[mounted]
F --> G[State Change]
G --> H[beforeUpdate]
H --> I[updated]
G --> J[Component Removal]
J --> K[beforeDestroy]
K --> L[destroyed]1. beforeCreate
Called before instance creation (no state yet)
ComponentManager.define('example', {
beforeCreate() {
console.log('Before create - this.state:', this.state); // {}
}
});2. created
Called after instance creation (state available)
ComponentManager.define('example', {
state: { count: 0 },
created() {
console.log('Created - count:', this.state.count); // 0
// Good: fetch initial data
this.fetchData();
},
methods: {
async fetchData() {
const data = await fetch('/api/data').then(r => r.json());
this.state.data = data;
}
}
});3. beforeMount
Called before template is rendered to DOM
ComponentManager.define('example', {
beforeMount() {
// Adjust state before render
this.state.timestamp = Date.now();
}
});4. mounted
Called after component is inserted into DOM
ComponentManager.define('chart', {
template: '<canvas id="chart"></canvas>',
mounted() {
// Good: work with DOM elements
const canvas = this.element.querySelector('#chart');
this.chart = new Chart(canvas, this.state.chartConfig);
}
});5. beforeUpdate
Called before re-render
ComponentManager.define('example', {
beforeUpdate() {
console.log('About to update...');
this.state.lastUpdate = Date.now();
}
});6. updated
Called after re-render completes
ComponentManager.define('example', {
updated() {
console.log('Update complete');
// Good: scroll to new content
this.element.scrollIntoView({ behavior: 'smooth' });
}
});7. beforeDestroy
Called before component removal
ComponentManager.define('timer', {
state: { timer: null },
mounted() {
this.state.timer = setInterval(() => {
this.state.count++;
this.render();
}, 1000);
},
beforeDestroy() {
// Good: cleanup resources
clearInterval(this.state.timer);
}
});8. destroyed
Called after component is removed from DOM
ComponentManager.define('example', {
destroyed() {
console.log('Component destroyed');
// Notify parent or analytics
EventManager.emit('component:destroyed', this.id);
}
});Component Definition Options
All Options
ComponentManager.define('complete-example', {
// Template
template: '<div class="example">{{ title }}</div>',
// Initial state
state: {
title: 'Hello',
items: []
},
// Props validation (optional)
props: {
title: { type: String, default: 'Default Title' },
count: { type: Number, default: 0 }
},
// Methods
methods: {
doSomething() {
this.state.title = 'Changed';
this.render();
}
},
// Computed properties
computed: {
fullTitle() {
return `${this.state.title} - App`;
}
},
// Watch state changes
watch: {
'state.count': function(newVal, oldVal) {
console.log(`count changed: ${oldVal} -> ${newVal}`);
}
},
// Reactive mode (auto re-render on state change)
reactive: true,
// Render strategy: 'auto', 'manual', 'batch'
renderStrategy: 'auto',
// Event handlers
events: {
'click button': function(e) { },
'submit form': function(e) { },
'custom:event': function(data) { }
},
// ARIA attributes
aria: {
role: 'region',
label: 'Example component'
},
// Custom element validation
validElement: (element) => element.tagName === 'DIV',
// Custom element setup
setupElement: (element, state) => {
element.classList.add('initialized');
},
// Error boundary
errorBoundary: true,
errorCaptured: (error) => {
console.error('Component error:', error);
return false; // Don't propagate
},
// Lifecycle hooks
beforeCreate() { },
created() { },
beforeMount() { },
mounted() { },
beforeUpdate() { },
updated() { },
beforeDestroy() { },
destroyed() { }
});State and Reactivity
Basic State
ComponentManager.define('user-card', {
state: {
name: 'Guest',
email: '',
isActive: false
},
methods: {
updateName(newName) {
this.state.name = newName;
this.render(); // Manual re-render
}
}
});Reactive State
When reactive: true is enabled, state changes auto-trigger re-render:
ComponentManager.define('reactive-counter', {
reactive: true, // Enable reactivity
state: {
count: 0
},
events: {
'click [data-action="increment"]': function() {
this.state.count++; // Auto re-renders!
}
}
});Watch
Monitor state changes:
ComponentManager.define('watcher-example', {
reactive: true,
state: {
query: '',
results: []
},
watch: {
'state.query': async function(newVal, oldVal) {
if (newVal.length >= 3) {
this.state.results = await this.methods.search(newVal);
}
}
},
methods: {
async search(query) {
const response = await fetch(`/api/search?q=${query}`);
return response.json();
}
}
});Event Handling
Event Syntax
events: {
// Basic: 'eventType selector'
'click button': function(e) { },
// Multiple events
'mouseenter .card': function(e) { },
'mouseleave .card': function(e) { },
// Form events
'submit form': function(e) {
e.preventDefault();
},
'input [name="search"]': function(e) {
this.state.query = e.target.value;
},
// Custom events (via EventManager)
'user:login': function(userData) { },
'cart:updated': function(items) { }
}Event Delegation
Events are bound to root element using delegation:
ComponentManager.define('todo-list', {
template: `
<ul class="todos">
<li data-for="item in items">
<template>
<span data-text="item.title"></span>
<button data-action="delete" data-id="{{ item.id }}">Delete</button>
</template>
</li>
</ul>
`,
events: {
// Handles click on any [data-action="delete"] inside component
'click [data-action="delete"]': function(e) {
const id = e.target.dataset.id;
this.methods.deleteItem(id);
}
}
});Props
Receiving Props
Props are extracted from data-* attributes:
<div data-component="user-card"
data-user-id="123"
data-name="John Doe"
data-count="5"
data-active="true">
</div>ComponentManager.define('user-card', {
created() {
console.log(this.props);
// { userId: "123", name: "John Doe", count: 5, active: true }
}
});Auto Type Conversion
- Numbers:
"123"→123 - Booleans:
"true"→true,"false"→false - Arrays:
"[1,2,3]"→[1, 2, 3]
Refs
Access DOM elements via refs:
<div data-component="form-example">
<input data-ref="nameInput" type="text">
<button data-ref="submitBtn">Submit</button>
</div>ComponentManager.define('form-example', {
mounted() {
// Access via refs proxy
this.refs.nameInput.focus();
this.refs.submitBtn.disabled = true;
},
methods: {
validate() {
const value = this.refs.nameInput.value;
this.refs.submitBtn.disabled = value.length < 3;
}
}
});API Reference
ComponentManager.define(name, definition)
Register a component definition
| Parameter | Type | Description |
|---|---|---|
name |
string | Component name (must be unique) |
definition |
object | Component definition object |
Returns: Object - Processed definition
const definition = ComponentManager.define('my-component', {
template: '<div>Hello</div>',
state: { count: 0 }
});ComponentManager.mount(element, name, props)
Mount component to element
| Parameter | Type | Description |
|---|---|---|
element |
HTMLElement | Target element |
name |
string | Component name |
props |
object | Initial props |
Returns: Promise<Object> - Component instance
const element = document.querySelector('#container');
const instance = await ComponentManager.mount(element, 'counter', { count: 10 });ComponentManager.destroy(element)
Remove component from element
| Parameter | Type | Description |
|---|---|---|
element |
HTMLElement | Element with component |
await ComponentManager.destroy(document.querySelector('#counter'));ComponentManager.has(name)
Check if component is registered
| Parameter | Type | Description |
|---|---|---|
name |
string | Component name |
Returns: boolean
if (ComponentManager.has('counter')) {
console.log('Counter component is registered');
}ComponentManager.get(name)
Get component definition
| Parameter | Type | Description |
|---|---|---|
name |
string | Component name |
Returns: Object|undefined - Component definition
const definition = ComponentManager.get('counter');
console.log(definition.state); // { count: 0 }ComponentManager.cleanup(container)
Remove all components in container
| Parameter | Type | Description |
|---|---|---|
container |
HTMLElement | Container element |
await ComponentManager.cleanup(document.querySelector('#app'));ComponentManager.forceUpdate(instance)
Force re-render component
| Parameter | Type | Description |
|---|---|---|
instance |
Object | Component instance |
ComponentManager.forceUpdate(instance);Advanced Examples
Component with API Data Loading
ComponentManager.define('user-list', {
template: `
<div class="user-list">
<div data-if="loading" class="loading">Loading...</div>
<div data-if="error" class="error" data-text="error"></div>
<ul data-if="!loading && !error" data-for="user in users">
<template>
<li>
<img data-attr="src: user.avatar" alt="">
<span data-text="user.name"></span>
</li>
</template>
</ul>
</div>
`,
state: {
users: [],
loading: false,
error: null
},
async created() {
await this.methods.fetchUsers();
},
methods: {
async fetchUsers() {
this.state.loading = true;
this.state.error = null;
try {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch');
this.state.users = await response.json();
} catch (error) {
this.state.error = error.message;
} finally {
this.state.loading = false;
this.render();
}
}
}
});Nested Components
<div data-component="dashboard">
<div data-component="stats-card" data-title="Users" data-value="150"></div>
<div data-component="stats-card" data-title="Sales" data-value="$12,500"></div>
<div data-component="chart" data-type="line"></div>
</div>ComponentManager.define('stats-card', {
template: `
<div class="stats-card">
<h3 data-text="title"></h3>
<div class="value" data-text="value"></div>
</div>
`,
created() {
this.state.title = this.props.title;
this.state.value = this.props.value;
}
});Common Pitfalls
⚠️ 1. Component Names Must Be Unique
// ❌ Will warn and overwrite
ComponentManager.define('counter', { /* ... */ }); // file1.js
ComponentManager.define('counter', { /* ... */ }); // file2.js
// ✅ Use unique names
ComponentManager.define('page-counter', { /* ... */ });
ComponentManager.define('cart-counter', { /* ... */ });⚠️ 2. Always Cleanup Resources
// ❌ Memory leak
ComponentManager.define('timer', {
mounted() {
setInterval(() => { /* ... */ }, 1000);
}
});
// ✅ Cleanup properly
ComponentManager.define('timer', {
mounted() {
this.state.timer = setInterval(() => { /* ... */ }, 1000);
},
beforeDestroy() {
clearInterval(this.state.timer);
}
});⚠️ 3. Use this.render() for Manual Updates
// ❌ No effect - DOM doesn't update
ComponentManager.define('counter', {
methods: {
increment() {
this.state.count++;
// missing render()
}
}
});
// ✅ Call render() after state changes
ComponentManager.define('counter', {
methods: {
increment() {
this.state.count++;
this.render(); // Update DOM
}
}
});
// ✅ Or use reactive: true
ComponentManager.define('counter', {
reactive: true, // Auto renders on state change
methods: {
increment() {
this.state.count++; // Auto renders!
}
}
});Related Documentation
- TemplateManager - Template and data binding
- ReactiveManager - Reactive state management
- EventManager - Event handling
- ApiComponent - API-driven components