htmspa
v0.0.1
Published
A lightweight, functional JavaScript library for building single-page applications with templating, routing, and state management.
Maintainers
Readme
htmspa
A lightweight, functional JavaScript library for building single-page applications with templating, routing, and state management.
Features
- Functional Programming Approach: Built with functional programming principles using curry, pipe, and compose
- Template Engine: Handlebars-like templating with variables, loops, and conditionals
- Router: Client-side routing with parameter extraction and navigation hooks
- State Management: Reactive state management with subscriptions and computed values
- Lifecycle Hooks: Component lifecycle management with onLoad and onUnload
- Minimal Dependencies: Pure JavaScript with no external dependencies
Installation
Browser (CDN)
<script src="path/to/htmspa.js"></script>CommonJS
const htmspa = require('./htmspa');AMD
define(['htmspa'], function(htmspa) {
// Your code here
});Quick Start
// Create an app instance
const app = htmspa.createApp({ container: '#app' });
// Set up routes
app.route('/', () => {
app.render('<h1>Welcome to {{title}}!</h1>', { title: 'htmspa' });
});
app.route('/users/:id', (params) => {
app.render('<h1>User: {{userId}}</h1>', { userId: params.id });
});
// Initialize the app
app.init();Core Concepts
1. Template Engine
The template engine supports Handlebars-like syntax for dynamic content rendering.
Variables
const template = '<h1>Hello {{name}}!</h1>';
const data = { name: 'World' };
const result = app.template(template, data);
// Result: <h1>Hello World!</h1>Nested Properties
const template = '<p>{{user.name}} - {{user.email}}</p>';
const data = {
user: {
name: 'John Doe',
email: '[email protected]'
}
};Loops with {{#each}}
const template = `
<ul>
{{#each items}}
<li>{{@index}}: {{this.name}} - {{this.price}}</li>
{{/each}}
</ul>
`;
const data = {
items: [
{ name: 'Apple', price: '$1.00' },
{ name: 'Banana', price: '$0.50' }
]
};Conditionals with {{#if}} and {{#unless}}
const template = `
{{#if isLoggedIn}}
<p>Welcome back!</p>
{{/if}}
{{#unless isLoggedIn}}
<p>Please log in</p>
{{/unless}}
`;
const data = { isLoggedIn: true };2. Routing
Basic Routes
// Simple route
app.route('/', () => {
app.render('<h1>Home Page</h1>');
});
// Route with parameters
app.route('/users/:id', (params) => {
app.render('<h1>User {{userId}}</h1>', { userId: params.id });
});
// Multiple parameters
app.route('/users/:id/posts/:postId', (params) => {
console.log(params.id, params.postId);
});Navigation
// Programmatic navigation
app.navigate('/users/123');
// HTML links (automatically handled)
<a href="/users/123">View User</a>Route Hooks
// Before route change
app.beforeRoute((path) => {
console.log('Navigating to:', path);
// Return false to cancel navigation
return true;
});
// After route change
app.afterRoute((path, params) => {
console.log('Navigated to:', path, params);
});Base Path Support
The router automatically detects base paths for nested applications:
// For URLs like /p/user/project/counter
// The base path /p/user/project is automatically detected3. State Management
Basic State Operations
// Set state
app.setState('counter', 0);
// Get state
const counter = app.getState('counter');
// Update state
app.updateState('counter', current => current + 1);State Subscriptions
// Subscribe to state changes
const unsubscribe = app.subscribe('counter', (newValue, key) => {
console.log(`${key} changed to:`, newValue);
});
// Unsubscribe
unsubscribe();Computed State
// Computed values based on multiple state keys
const getTotal = app.computed(['price', 'quantity'], (price, quantity) => {
return price * quantity;
});
console.log(getTotal()); // Automatically recalculates when price or quantity changesState Binding with Auto Re-render
// Bind template to state - auto re-renders when state changes
app.setState('user', { name: 'John', age: 30 });
const renderUser = app.bindState('user', `
<div>
<h1>{{user.name}}</h1>
<p>Age: {{user.age}}</p>
</div>
`);
// Initial render
renderUser();
// State changes will automatically trigger re-render
app.updateState('user', user => ({ ...user, age: 31 }));4. Lifecycle Hooks
app.route('/dashboard', () => {
// Setup lifecycle hooks
app.onLoad(() => {
console.log('Dashboard loaded');
// Initialize components, fetch data, etc.
});
app.onUnload(() => {
console.log('Dashboard unloading');
// Cleanup timers, remove event listeners, etc.
});
app.render('<h1>Dashboard</h1>');
});5. Components
Create reusable components:
// Define a component
const UserCard = app.component('user-card', `
<div class="card">
<h3>{{name}}</h3>
<p>{{email}}</p>
{{#if isAdmin}}
<span class="badge">Admin</span>
{{/if}}
</div>
`);
// Use the component
const userData = { name: 'John', email: '[email protected]', isAdmin: true };
const cardHTML = UserCard(userData);API Reference
App Instance Methods
createApp(config)
Creates a new htmspa application instance.
const app = htmspa.createApp({
container: '#app' // CSS selector or DOM element
});Routing Methods
route(path, handler)- Define a routenavigate(path)- Navigate to a pathbeforeRoute(callback)- Add before route hookafterRoute(callback)- Add after route hook
State Methods
setState(key, value)- Set state valuegetState(key)- Get state valueupdateState(key, updater)- Update state with functionsubscribe(key, callback)- Subscribe to state changescomputed(keys, computer)- Create computed value
Rendering Methods
render(template, data)- Render template to DOMtemplate(templateStr, data)- Compile template stringcomponent(name, templateStr)- Create reusable componentbindState(keys, template)- Auto re-render on state change
Lifecycle Methods
onLoad(callback)- Add onLoad hookonUnload(callback)- Add onUnload hookinit()- Initialize the application
Utilities
Functional Programming
// Available functional utilities
const { pipe, compose, curry, partial } = htmspa;
// Example usage
const addTax = x => x * 1.1;
const formatPrice = x => `$${x.toFixed(2)}`;
const processPrice = pipe(addTax, formatPrice);
console.log(processPrice(100)); // "$110.00"Helpers
const { helpers } = htmspa;
// Event handling
helpers.on('click', '.button', (e) => {
console.log('Button clicked');
});
// HTTP requests
helpers.http.get('/api/users').then(users => {
console.log(users);
});
helpers.http.post('/api/users', { name: 'John' }).then(response => {
console.log(response);
});
// DOM utilities
const element = helpers.dom.qs('.my-class');
helpers.dom.addClass('active', element);
// Debounce and throttle
const debouncedFn = helpers.debounce(() => {
console.log('Debounced!');
}, 300);
const throttledFn = helpers.throttle(() => {
console.log('Throttled!');
}, 1000);Complete Example
// Create app
const app = htmspa.createApp({ container: '#app' });
// Initialize state
app.setState('todos', []);
app.setState('filter', 'all');
// Components
const TodoItem = app.component('todo-item', `
<li class="{{#if completed}}completed{{/if}}">
<input type="checkbox" {{#if completed}}checked{{/if}}
onchange="toggleTodo({{id}})">
<span>{{text}}</span>
<button onclick="deleteTodo({{id}})">Delete</button>
</li>
`);
const TodoApp = `
<div class="todo-app">
<h1>Todo App</h1>
<input type="text" id="new-todo" placeholder="Add new todo...">
<button onclick="addTodo()">Add</button>
<ul class="todo-list">
{{#each filteredTodos}}
${TodoItem}
{{/each}}
</ul>
<div class="filters">
<button onclick="setFilter('all')">All</button>
<button onclick="setFilter('active')">Active</button>
<button onclick="setFilter('completed')">Completed</button>
</div>
</div>
`;
// Computed filtered todos
const getFilteredTodos = app.computed(['todos', 'filter'], (todos, filter) => {
if (filter === 'active') return todos.filter(t => !t.completed);
if (filter === 'completed') return todos.filter(t => t.completed);
return todos;
});
// Routes
app.route('/', () => {
const renderApp = app.bindState(['todos', 'filter'], TodoApp);
renderApp();
});
app.route('/filter/:type', (params) => {
app.setState('filter', params.type);
app.navigate('/');
});
// Global functions for event handlers
window.addTodo = () => {
const input = document.getElementById('new-todo');
const text = input.value.trim();
if (text) {
const todos = app.getState('todos');
const newTodo = { id: Date.now(), text, completed: false };
app.setState('todos', [...todos, newTodo]);
input.value = '';
}
};
window.toggleTodo = (id) => {
app.updateState('todos', todos =>
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
window.deleteTodo = (id) => {
app.updateState('todos', todos => todos.filter(todo => todo.id !== id));
};
window.setFilter = (filter) => {
app.setState('filter', filter);
};
// Initialize
app.init();Browser Support
htmspa works in all modern browsers that support:
- ES6+ features (arrow functions, template literals, destructuring)
- Map and Set
- History API
- addEventListener
License
MIT License
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
