@lithium-ts/core
v0.1.0
Published
A lightweight, modern web framework built on top of [Lit](https://lit.dev) with reactive state management, dependency injection, and powerful routing.
Downloads
280
Readme
🔋 LithiumJS
A lightweight, modern web framework built on top of Lit with reactive state management, dependency injection, and powerful routing.
Table of Contents
- Installation
- Core Concepts
- State Management (EventBus)
- Routing
- Decorators
- Services & Dependency Injection
- WebSocket Services
- API Reference
Installation
npm install @lithium-ts/coreCore Concepts
LithiumElement
The base class for all components. Extends Lit's LitElement with reactive signals and EventBus integration.
import { LithiumElement, defineElement, html } from "@lithium-ts/core";
@defineElement({ tag: "my-button" })
export class MyButton extends LithiumElement {
render() {
return html`<button><slot></slot></button>`;
}
}Key Methods
| Method | Description |
| --------------------------- | ------------------------------------ |
| channel(name, config) | Create/access a reactive channel |
| publish(name, data) | Publish data to a channel |
| subscribe(name, callback) | Subscribe to channel changes |
| emit(event, data) | Emit instant event (fire-and-forget) |
| on(event, callback) | Listen to instant events |
| navigate(path, options) | Navigate to a route |
| output(event, detail) | Emit a DOM CustomEvent |
| clear(name) | Remove a channel |
| clearAll(storage?) | Remove all channels |
Pages
Pages are components with automatic title management. The tag name is auto-generated from the class name.
import { LithiumElement, definePage, html } from "@lithium-ts/core";
import styles from "./home.page.css?inline";
@definePage({
title: "Home - My App",
styles: [styles],
})
export class HomePage extends LithiumElement {
render() {
return html`
<h1>Welcome!</h1>
<button @click=${() => this.navigate("/about")}>Go to About</button>
`;
}
}
// Auto-registered as <home-page>Modules
Modules group routes with optional layouts. Perfect for feature modules (admin, auth, etc.).
import { LithiumModule, defineModule, html } from "@lithium-ts/core";
const adminRoutes = [
{ path: "/", render: () => html`<admin-dashboard></admin-dashboard>` },
{ path: "/users", render: () => html`<admin-users></admin-users>` },
];
@defineModule({ routes: adminRoutes })
export class AdminModule extends LithiumModule {
// Optional: Custom layout
render() {
return html`
<nav>Admin Navigation</nav>
<main>${this._routes.outlet()}</main>
`;
}
}
// Auto-registered as <admin-module>LithiumApp
The root application component with main routing.
import { LithiumApp, defineApp, html } from "@lithium-ts/core";
const routes = [
{ path: "/", render: () => html`<home-page></home-page>` },
{ path: "/about", render: () => html`<about-page></about-page>` },
{ path: "/admin/*", render: () => html`<admin-module></admin-module>` },
];
@defineApp({
tag: "main-app",
routes,
})
export class MainApp extends LithiumApp {
render() {
return html`
<header>My App</header>
${this._router.outlet()}
<footer>© 2026</footer>
`;
}
}State Management (EventBus)
LithiumJS uses a reactive EventBus with Signals for state management.
Channels
Channels are reactive state containers that automatically update components.
// In any component
export class MyComponent extends LithiumElement {
// Create a channel (reactive!)
private user = this.channel<User>("user:current", {
initialValue: null,
storage: "session",
});
render() {
const user = this.user.get(); // Reactive - auto re-renders on change
return html`<p>Hello, ${user?.name ?? "Guest"}</p>`;
}
login(userData: User) {
this.publish("user:current", userData); // All subscribers update
}
}Channel Configuration
this.channel<T>("channel-name", {
// Value
initialValue: null,
// Storage type
storage: "memory" | "session" | "local" | "page",
storageKey: "custom-key", // Default: lithium:channel-name
// Auto-cleanup
ttl: 60000, // Time-to-live in ms
autoCleanup: true, // Delete when no subscribers
// For 'page' storage only
persist: true, // Survive page refresh
// Lifecycle hooks
onChange: (newVal, oldVal) => console.log("Changed"),
onInit: (value) => console.log("First value set"),
onClear: () => console.log("Channel cleared"),
// Transformation
validate: (value) => value.id > 0, // Return false to cancel update
transform: (value) => normalize(value),
// Rate limiting
debounce: 500, // Debounce onChange
throttle: 1000, // Throttle onChange
});Storage Types
| Type | Persistence | Use Case |
| --------- | -------------------- | ----------------------- |
| memory | Lost on refresh | Temporary UI state |
| session | Browser tab lifetime | User session data |
| local | Forever | User preferences |
| page | Until route change | Form drafts, page state |
Page Storage Example
// Form data that clears when navigating away
private formDraft = this.channel('form:draft', {
initialValue: { name: '', email: '' },
storage: 'page',
persist: false // Clears on navigation
});
// Notes that survive navigation but still page-scoped
private notes = this.channel('page:notes', {
initialValue: '',
storage: 'page',
persist: true // Survives navigation, stored in sessionStorage
});Instant Events
Fire-and-forget events without persistence.
// Emit (sender)
this.emit('toast:show', {
message: 'Saved!',
type: 'success'
});
// Listen (receiver)
connectedCallback() {
super.connectedCallback();
this.on('toast:show', (data) => {
this.showToast(data.message, data.type);
});
}Middleware
Intercept all EventBus actions for logging, validation, etc.
import { EventBus } from "@lithium-ts/core";
// Add middleware
EventBus.use((action, channelOrEvent, data, next) => {
console.log(`[${action}] ${channelOrEvent}:`, data);
next(); // Must call next() to continue
});
// Enable dev mode for detailed logging
EventBus.setDevMode(true);Routing
LithiumRouter
A wrapper component that intercepts navigation for guards.
import { LithiumApp, defineApp, html } from "@lithium-ts/core";
@defineApp({ tag: "main-app", routes })
export class MainApp extends LithiumApp {
render() {
return html`
<lithium-router
.beforeRoute=${this.authGuard}
.afterRoute=${this.analytics}
>
${this._router.outlet()}
</lithium-router>
`;
}
}Route Guards
Guards control access to routes.
beforeRoute
Runs before navigation. Can block or redirect.
import { RouteLocation, RouteGuardResult } from "@lithium-ts/core";
const authGuard = async (
to: RouteLocation,
from: RouteLocation,
): Promise<boolean | RouteGuardResult> => {
const isAuthenticated = !!localStorage.getItem("token");
// Protected routes
if (to.path.startsWith("/admin") && !isAuthenticated) {
return { continue: false, redirect: "/login" };
}
// Allow navigation
return true;
};afterRoute
Runs after successful navigation. For analytics, logging, etc.
const analyticsGuard = (to: RouteLocation, from: RouteLocation) => {
analytics.track("page_view", {
path: to.path,
referrer: from.path,
});
};RouteLocation Object
interface RouteLocation {
path: string; // /users/123
query: Record<string, string>; // { tab: 'settings' }
hash: string; // 'section-1'
fullPath: string; // /users/123?tab=settings#section-1
}Programmatic Navigation
Navigate from anywhere using static methods or component methods.
import { LithiumRouter } from "@lithium-ts/core";
// From anywhere (static)
LithiumRouter.navigate("/dashboard");
LithiumRouter.navigate("/search", { query: { q: "hello" } });
LithiumRouter.navigate("/docs", { hash: "installation" });
LithiumRouter.navigate("/login", { replace: true }); // No history entry
LithiumRouter.back();
LithiumRouter.forward();
LithiumRouter.go(-2);
// From a component
class MyComponent extends LithiumElement {
goToDashboard() {
this.navigate("/dashboard");
this.navigate("/profile", { state: { from: "home" } });
}
}Decorators
@defer
Loads content after initial render. Perfect for non-critical UI.
import { defer, html } from "@lithium-ts/core";
class MyPage extends LithiumElement {
// Basic - loads on next microtask
@defer()
get analytics() {
return html`<analytics-widget></analytics-widget>`;
}
// With placeholder
@defer({ placeholder: html`<loading-spinner></loading-spinner>` })
get charts() {
return html`<heavy-charts></heavy-charts>`;
}
// With delay
@defer({ delay: 2000 })
get lowPriority() {
return html`<recommendations></recommendations>`;
}
// With dynamic import (real code-splitting!)
@defer({
loader: () => import("./heavy-component.js"),
placeholder: html`<skeleton-loader></skeleton-loader>`,
})
get heavyContent() {
return html`<heavy-component></heavy-component>`;
}
render() {
return html`
<main>Important content first</main>
${this.analytics} ${this.charts}
`;
}
}@lazy
Loads content when it enters the viewport using IntersectionObserver.
import { lazy, html } from "@lithium-ts/core";
class MyPage extends LithiumElement {
// Basic lazy loading
@lazy()
get comments() {
return html`<comments-section></comments-section>`;
}
// With options
@lazy({
threshold: 0.5, // 50% visible
rootMargin: "100px", // Load 100px before visible
placeholder: html`<skeleton></skeleton>`,
minHeight: "200px", // Prevent layout shift
})
get heavySection() {
return html`<data-visualization></data-visualization>`;
}
// With dynamic import
@lazy({
loader: () => import("./gallery.js"),
placeholder: html`<gallery-skeleton></gallery-skeleton>`,
})
get gallery() {
return html`<photo-gallery></photo-gallery>`;
}
render() {
return html`
<main>Above the fold</main>
<div style="height: 200vh;">Scroll down...</div>
${this.comments}
<!-- Loads when scrolled into view -->
${this.gallery}
`;
}
}@conditional
Renders content only when a condition is true. Supports lazy loading.
import { conditional, html } from "@lithium-ts/core";
import { state } from "lit/decorators.js";
class MyPage extends LithiumElement {
@state() isAdmin = false;
@state() showAdvanced = false;
// Basic conditional
@conditional("isAdmin")
get adminPanel() {
return html`<admin-panel></admin-panel>`;
}
// With placeholder
@conditional("showAdvanced", {
placeholder: html`<p>Enable advanced mode to see more options</p>`,
})
get advancedOptions() {
return html`<advanced-settings></advanced-settings>`;
}
// With lazy loading (only imports when condition is true!)
@conditional("isAdmin", {
loader: () => import("./admin-dashboard.js"),
placeholder: html`<p>Admin only</p>`,
})
get adminDashboard() {
return html`<admin-dashboard></admin-dashboard>`;
}
render() {
return html`
<main>${this.adminPanel}</main>
<div>${this.advancedOptions}</div>
<button @click=${() => (this.isAdmin = !this.isAdmin)}>
Toggle Admin
</button>
`;
}
}Services & Dependency Injection
Creating Services
Services are singletons with HTTP helpers and EventBus integration.
import {
LithiumService,
service,
inject,
ServiceInterceptor,
} from "@lithium-ts/core";
// Optional: Service-specific interceptor
const cacheInterceptor: ServiceInterceptor = {
name: "cache",
response: (res, data) => {
localStorage.setItem(`cache:${res.url}`, JSON.stringify(data));
return data;
},
};
@service({
baseUrl: "https://api.example.com",
headers: { "X-App-Version": "1.0.0" },
interceptors: [cacheInterceptor],
})
export class UserService extends LithiumService {
async getUsers(): Promise<User[]> {
return this.get<User[]>("/users", {
onSuccess: "users:loaded",
onError: "users:error",
});
}
async createUser(user: User): Promise<User> {
return this.post<User>("/users", user);
}
async updateUser(id: number, data: Partial<User>): Promise<User> {
return this.put<User>(`/users/${id}`, data);
}
async deleteUser(id: number): Promise<void> {
return this.delete(`/users/${id}`);
}
}Using Services in Components
import { inject } from "@lithium-ts/core";
@definePage({ title: "Users" })
export class UsersPage extends LithiumElement {
@inject(UserService) private userService!: UserService;
async connectedCallback() {
super.connectedCallback();
const users = await this.userService.getUsers();
this.publish("users:list", users);
}
}HTTP Methods
class MyService extends LithiumService {
// GET
getItems() {
return this.get<Item[]>("/items");
}
// POST
createItem(data: Item) {
return this.post<Item>("/items", data);
}
// PUT
updateItem(id: number, data: Item) {
return this.put<Item>(`/items/${id}`, data);
}
// PATCH
patchItem(id: number, data: Partial<Item>) {
return this.patch<Item>(`/items/${id}`, data);
}
// DELETE
deleteItem(id: number) {
return this.delete(`/items/${id}`);
}
// Custom fetch with options
customRequest() {
return this.fetch<Data>("/endpoint", {
method: "POST",
body: JSON.stringify(data),
onSuccess: "data:loaded",
onError: "data:error",
transform: async (res) => res.text(), // Custom response transform
skipGlobalInterceptors: true,
});
}
}Interceptors
Interceptors modify requests/responses globally or per-service.
Global Interceptors
Applied to ALL services.
import { LithiumService } from "@lithium-ts/core";
// Auth interceptor
LithiumService.addInterceptor({
name: "auth",
request: (ctx) => {
const token = localStorage.getItem("token");
if (token) {
ctx.options.headers = {
...ctx.options.headers,
Authorization: `Bearer ${token}`,
};
}
return ctx;
},
error: (error, ctx) => {
if (error.status === 401) {
EventBus.emit("auth:expired");
LithiumRouter.navigate("/login");
}
return error;
},
});
// Logging interceptor
LithiumService.addInterceptor({
name: "logging",
request: (ctx) => {
console.log(`[API] ${ctx.method} ${ctx.url}`);
return ctx;
},
response: (res, data, ctx) => {
console.log(`[API] Response:`, data);
return data;
},
});Service-Specific Interceptors
Applied only to one service.
@service({
baseUrl: "https://api.example.com",
interceptors: [
{
name: "retry",
error: async (error, ctx) => {
if (error.status === 503) {
// Retry logic
await delay(1000);
return fetch(ctx.url, ctx.options);
}
return error;
},
},
],
})
export class MyService extends LithiumService {}Interceptor Context
interface InterceptorContext {
url: string; // Full URL
options: RequestInit; // Fetch options
endpoint: string; // Just the endpoint path
method: string; // HTTP method
}WebSocket Services
For real-time features like chat, notifications, etc.
import { LithiumSocketService, socketService, inject } from "@lithium-ts/core";
@socketService()
export class ChatService extends LithiumSocketService {
constructor() {
super({
url: "wss://api.example.com/chat",
reconnect: true,
reconnectInterval: 3000,
maxReconnectAttempts: 5,
autoConnect: false,
protocols: ["v1.json"],
auth: () => localStorage.getItem("token"),
heartbeatInterval: 30000,
heartbeatMessage: "ping",
});
// Register message handlers
this.on("message:new", (data) => {
this.publishToChannel("chat:messages", data);
});
this.on("user:typing", (data) => {
this.emit("chat:typing", data); // Instant event
});
}
// Public methods
sendMessage(content: string) {
this.send("message:send", { content });
}
joinRoom(roomId: string) {
this.send("room:join", { roomId });
}
// Lifecycle hooks (override as needed)
protected onConnected() {
console.log("Connected to chat");
this.emit("chat:connected", {});
}
protected onDisconnected() {
console.log("Disconnected from chat");
this.emit("chat:disconnected", {});
}
protected onReconnectFailed() {
this.emit("chat:error", { message: "Connection failed" });
}
}Using in Components
@definePage({ title: "Chat" })
export class ChatPage extends LithiumElement {
@inject(ChatService) private chat!: ChatService;
private messages = this.channel<Message[]>("chat:messages", {
initialValue: [],
storage: "page",
});
connectedCallback() {
super.connectedCallback();
this.chat.connect();
this.on("chat:typing", (data) => {
this.showTypingIndicator(data.username);
});
}
disconnectedCallback() {
super.disconnectedCallback();
this.chat.disconnect();
}
render() {
return html`
<div class="messages">
${this.messages
.get()
.map((msg) => html` <div class="message">${msg.content}</div> `)}
</div>
<input @keyup=${this.handleSend} />
`;
}
}SocketServiceConfig
| Option | Type | Default | Description |
| ---------------------- | ------------------ | -------- | ---------------------------- |
| url | string | required | WebSocket server URL |
| reconnect | boolean | true | Auto-reconnect on disconnect |
| reconnectInterval | number | 3000 | Base reconnect delay (ms) |
| maxReconnectAttempts | number | 5 | Max attempts (0 = infinite) |
| autoConnect | boolean | false | Connect on instantiation |
| protocols | string[] | [] | WebSocket sub-protocols |
| auth | () => string | - | Auth token provider |
| heartbeatInterval | number | 30000 | Heartbeat interval (ms) |
| heartbeatMessage | string \| object | 'ping' | Heartbeat payload |
API Reference
Exports
// Core
export { LithiumElement, defineElement, StorageType } from "@lithium-ts/core";
export { definePage } from "@lithium-ts/core";
export { LithiumModule, defineModule } from "@lithium-ts/core";
export { LithiumApp, defineApp } from "@lithium-ts/core";
export { html, unsafeCSS, property } from "@lithium-ts/core";
// EventBus
export {
EventBus,
ChannelConfig,
ListenerOptions,
ChannelInfo,
EventBusMiddleware,
} from "@lithium-ts/core";
// Router
export {
LithiumRouter,
NAVIGATE_EVENT,
RouteGuardResult,
RouteLocation,
NavigateEventData,
NavigateOptions,
BeforeRouteCallback,
AfterRouteCallback,
} from "@lithium-ts/core";
// Decorators
export { defer, DeferOptions } from "@lithium-ts/core";
export { lazy, LazyOptions } from "@lithium-ts/core";
export { conditional } from "@lithium-ts/core";
// Services
export {
LithiumService,
ServiceContainer,
service,
inject,
ServiceRequestOptions,
ServiceConfig,
ServiceInterceptor,
InterceptorContext,
} from "@lithium-ts/core";
// WebSocket Services
export {
LithiumSocketService,
socketService,
SocketServiceConfig,
SocketState,
SocketMessage,
} from "@lithium-ts/core";License
MIT © 2026
