UNPKG

tacit-dom

Version:

A React-like library with reactive signals and computed values for building dynamic web applications—without the need for JSX

1,015 lines (767 loc) 35.6 kB
<div align="center"> ![Tacit-DOM Banner](docs/images/tacit-dom_banner.svg) > A React-like library with reactive signals and computed values for building dynamic web applications—without the need for JSX. </div> ## 🤔 Why the Name "Tacit-DOM"? _Tacit knowledge_—the kind of understanding you possess intuitively, without needing explicit instructions or formal training. Tacit DOM is designed to be so intuitive you should be able to build apps with it in minutes. The name is inspired by the Northrop Grumman stealth demonstrator aircraft Tacit Blue, developed for the U.S. Air Force’s research programme. This experimental platform pioneered advances, which directly influenced the next generation of stealth aircraft, including the Nighthawk and B-2 Spirit.” ## 🚫 Why Not Just Continue Using React? React Has Peaked: It Solved the DOM Problem — But Not Today’s Problems. FaxJS was an internal Facebook prototype by Jordan Walke in 2011 that introduced the virtual DOM concept, serving as the foundation for what became ReactJS. React’s virtual DOM was revolutionary when browsers were slow and UI updates were costly. But now, with modern DOM APIs and frameworks like Solid, Svelte, and signals-based architectures, the VDOM is an expensive abstraction. React solved yesterday’s bottlenecks, not today’s. ### Community Innovation Is Moving Elsewhere The fastest-moving ideas (signals, resumability, fine-grained reactivity) are not coming from React anymore. Solid, Qwik, Vue 3’s composition API, and Svelte are demonstrating more elegant and performant models. React is becoming the “legacy default”, not the future driver. ### Hooks and Providers Add Cognitive Complexity React initially promised simplicity — components as functions. Over time, hooks, contexts, and providers have piled up layers of implicit state, dependency rules, and fragile lifecycles. It’s now harder to reason about state than in simpler reactive models. That’s a sign of maturity tipping into decline. #### Why Providers Are Terrible Deep Nesting Hell: Providers lead to “provider pyramids” — readability nightmare where the app tree is wrapped in multiple contexts just to pass config/state around. Over-Re-rendering: Context updates cause every consumer to re-render, even if only a small slice of the state changed. That’s wasteful. Global State Masquerading as Local: Context is meant for “infrequent global config” but is now misused for business logic and state sharing, making reasoning about boundaries harder. Opaque Performance: Developers often don’t realise performance pitfalls until too late. Debugging unnecessary renders in provider-based systems is painful. #### Why Hooks Are Error-Prone Rules of Hooks Are a Runtime Tax: Needing linters and mental discipline just to ensure hooks aren’t called in the wrong order shows the API is fragile. - **Hidden Dependencies**: useEffect dependencies are error-prone by design. Either you forget dependencies (leading to bugs), or you add everything (causing infinite loops). - **Boilerplate Instead of Clarity**: Complex hook composition often obscures intent. Instead of simple, declarative state, you end up juggling effect cleanup, stale closures, and race conditions. Concurrency Makes It Worse: React 18’s concurrent features amplify these issues — async rendering often exposes subtle bugs in hooks logic. ### Why Redux and Similar State Managers Are Hacks - **Boilerplate Explosion**: Reducers, actions, dispatchers — a ceremony to model state that should just be reactive data. - **Single Store Centralisation**: One giant object pretending to solve state management leads to brittle dependencies and unnecessary coupling. Extra Layer of Indirection: You don’t manipulate state directly — you must describe it, dispatch it, reduce it, then subscribe to it. This indirection adds complexity but doesn’t remove the fundamental re-rendering inefficiency. Ecosystem Gravity: Redux exists largely to patch React’s weaknesses around state propagation. The fact it was even necessary shows React’s core model was incomplete. ### The Positive Case for Building Something Better - **Fine-Grained Reactivity**: State changes should propagate only where needed, without full component re-renders. (Think Svelte or Solid.) - **Explicit, Declarative State Models**: Move away from implicit lifecycles and fragile hooks to clear reactive primitives. - **Tree-Shakeable, Lightweight Runtime**: Build frameworks that compile away boilerplate rather than ship a heavy runtime. - **Resumability & Edge-Readiness**: Future apps need frameworks optimised for streaming, islands, and instant hydration — things React is bolting on, but not designed for. Developer Experience First: Simpler mental models. No “rules of hooks”. No endless provider pyramids. Just state and UI, directly connected. </div> [![NPM Version](https://img.shields.io/npm/v/tacit-dom.svg)](https://www.npmjs.com/package/tacit-dom) [![License](https://img.shields.io/npm/l/tacit-dom.svg)](https://github.com/mjbeswick/tacit-dom/blob/main/LICENSE) [![TypeScript](https://img.shields.io/badge/TypeScript-5.9.2-blue.svg)](https://www.typescriptlang.org/) [![Build Status](https://img.shields.io/github/actions/workflow/status/mjbeswick/tacit-dom/test-examples.yml?branch=main)](https://github.com/mjbeswick/tacit-dom/actions) ## 📋 Table of Contents - [Project Status](#-project-status) - [Features](#-features) - [Why Tacit-DOM?](#-why-tacit-dom) - [Installation](#-installation) - [Quick Start](#-quick-start) - [Components](#-components) - [Conditional and List Rendering](#-conditional-and-list-rendering) - [Examples](#-examples) - [Documentation](#-documentation) - [API Reference](#-api-reference) - [Development](#-development) - [License](#-license) ## ⚠️ Project Status **Current Status**: Experimental Proof of Concept - **🚧 Experimental**: APIs are evolving and may change without notice - **🧪 Proof of Concept**: Designed to explore reactive programming patterns - **⚠️ Not Production Ready**: Limited test coverage and may break with non-trivial use cases - **🔄 Work in Progress**: Subject to significant changes and improvements > **Note**: If you need a stable, production-ready solution, consider established alternatives like React, Vue, or Svelte. ## ✨ Features ### 🚀 Core Reactivity - **⚡ Reactive Signals**: Create reactive state that automatically updates when dependencies change - **🧮 Computed Values**: Derive values from signals with automatic dependency tracking - **🌍 Global State Management**: Create global state anywhere without providers, context, or complex setup - **🧹 Automatic Cleanup**: Prevents memory leaks with smart cleanup ### 🎨 DOM & Components - **🚫 No Virtual DOM**: Direct DOM updates without the overhead of virtual DOM reconciliation - **🧩 Component Pattern**: Build components using a familiar JSX-like syntax - **🎭 Conditional Rendering**: Built-in `when` function for reactive conditional content - **📋 List Rendering**: Powerful `map` function with optional filtering for dynamic lists - **🧩 Conditional Rendering**: `when` function for reactive conditional content without wrappers - **🎯 Event Handling**: Comprehensive DOM event support including mouse, keyboard, touch, pointer, clipboard, selection, composition, animation, transition, media, and drag & drop events - **🎨 Style Support**: React-like style props with reactive updates ### 🛠️ Developer Experience - **🔒 TypeScript Support**: Full TypeScript support with type safety - **📦 Zero Dependencies**: Lightweight with no external dependencies - **⚡ Optimized Bundles**: Multiple formats (ESM, UMD, CJS) with Rollup - **🎯 Tree-shaking**: Individual modules for optimal bundling ## 🚀 Why Tacit-DOM? React has transformed web development, but **state management complexity remains a significant pain point**. Tacit-DOM offers a simpler, signal-first approach that addresses these fundamental issues: ### 🎯 Key Advantages | Feature | React | Tacit-DOM | | --------------------- | --------------------------------------------------- | ---------------------------------------- | | **State Management** | `useState`, `useContext`, `useReducer`, Redux, etc. | Simple signals, anywhere | | **Re-renders** | Component-level re-renders | Granular DOM updates only | | **Dependencies** | Manual dependency arrays (`useEffect`, `useMemo`) | Automatic dependency tracking | | **Virtual DOM** | Yes (reconciliation overhead) | No (direct DOM updates) | | **Bundle Size** | ~42KB (React 18 + ReactDOM) | ~15KB (zero dependencies) | | **Learning Curve** | Complex (hooks rules, render cycles, patterns) | Simple (just signals) | | **Global State** | Context providers, prop drilling, or external libs | Create signals anywhere, use anywhere | | **Async State** | Complex patterns with `useEffect` + loading flags | Built-in loading states in signals | | **Computed Values** | `useMemo` with manual dependencies | Automatic dependency tracking | | **Side Effects** | `useEffect` with cleanup and dependency arrays | Simple `effect()` with automatic cleanup | | **Component Updates** | Entire component re-executes on state change | Only affected DOM nodes update | ### 🧠 Simpler Mental Model **React Hooks Complexity:** ```typescript // React - Complex async state management function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [posts, setPosts] = useState([]); const [postsLoading, setPostsLoading] = useState(false); // Multiple useEffect hooks with dependency arrays useEffect(() => { setLoading(true); fetchUser(userId) .then(setUser) .catch(setError) .finally(() => setLoading(false)); }, [userId]); // Don't forget dependencies! useEffect(() => { if (user) { setPostsLoading(true); fetchUserPosts(user.id) .then(setPosts) .finally(() => setPostsLoading(false)); } }, [user]); // Another dependency array const displayName = useMemo(() => { return user ? `${user.firstName} ${user.lastName}` : ''; }, [user]); // More manual dependencies if (loading) return <div>Loading user...</div>; if (error) return <div>Error: {error.message}</div>; if (!user) return <div>User not found</div>; return ( <div> <h1>{displayName}</h1> <p>Email: {user.email}</p> {postsLoading ? ( <div>Loading posts...</div> ) : ( <ul> {posts.map(post => <li key={post.id}>{post.title}</li>)} </ul> )} </div> ); } ``` **Tacit-DOM Simplicity:** ```typescript // Tacit-DOM - Simple reactive state const userProfile = component(({}, { signal, computed, effect }) => { const user = signal(null); const posts = signal([]); // Computed values automatically track dependencies const displayName = computed(() => { const u = user.value; return u ? `${u.firstName} ${u.lastName}` : ''; }); // Effects automatically track dependencies and clean up effect(() => { user.setLoading(true); fetchUser(userId).then((fetchedUser) => (user.value = fetchedUser)); }); effect(() => { const u = user.value; if (u) { posts.setLoading(true); fetchUserPosts(u.id).then((fetchedPosts) => (posts.value = fetchedPosts)); } }); return div( user.loading ? div('Loading user...') : null, user.error ? div('Error: ', user.error.message) : null, user.value ? div( h1(displayName), p('Email: ', user.value.email), posts.loading ? div('Loading posts...') : ul(...posts.value.map((post) => li(post.title))), ) : div('User not found'), ); }); ``` ### 🎭 No More Re-render Roulette In React, changing any state re-renders the entire component, including expensive child components. With Tacit-DOM, only DOM elements that depend on a specific signal will update: ```typescript // React: Changing count re-renders EVERYTHING const dashboard = component(({}, { signal, computed }) => { const [count, setCount] = useState(0); const [user, setUser] = useState({ name: 'John' }); const [expensiveData, setExpensiveData] = useState([]); // This expensive computation runs on EVERY render const processedData = useMemo(() => { return expensiveData.map(item => processExpensiveItem(item)); }, [expensiveData]); return ( <div> <div>Count: {count}</div> {/* Changing count re-renders entire component */} <div>User: {user.name}</div> <ExpensiveChart data={processedData} /> {/* Re-renders even when count changes */} <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }); // Tacit-DOM: Surgical DOM updates const dashboard = component(({}, { signal, computed }) => { const count = signal(0); const user = signal({ name: 'John' }); const expensiveData = signal([]); // Computed value only recalculates when expensiveData changes const processedData = computed(() => expensiveData.value.map(item => processExpensiveItem(item)) ); return div( div('Count: ', count), // Only this element updates when count changes div('User: ', user.value.name), // Only updates when user changes ExpensiveChart({ data: processedData }), // Only updates when processedData changes button({ onclick: () => count.value = count.value + 1 }, 'Increment') ); }); ``` ### 🌍 Global vs Local Signals Tacit-DOM provides two types of signals: **global signals** and **local signals** scoped to components. #### Global Signals - Shared State Global signals can be created anywhere and accessed from any component: ```typescript // Global signals - accessible everywhere const theme = signal('light'); const user = signal({ name: 'John', email: 'john@example.com' }); const shoppingCart = signal([]); // Any component can use these signals const Header = component(({}, { signal }) => { return header( { className: theme }, span('Welcome ', user.value.name), span('Cart items: ', shoppingCart.value.length), ); }); const Settings = component(({}, { signal }) => { return div(button({ onclick: () => (theme.value = theme.value === 'light' ? 'dark' : 'light') }, 'Toggle Theme')); }); ``` #### Local Signals - Component-Scoped State Use the `component()` function to create signals that are scoped to a specific component instance: ```typescript // Component with local signals - must use camelCase name const counter = component(({}, { signal, computed }) => { // These signals are local to this counter instance // Use the signal function from component utilities const count = signal(0); const isEven = computed(() => count.value % 2 === 0); return div( span('Count: ', count), span(isEven.value ? ' (even)' : ' (odd)'), button({ onclick: () => (count.value = count.value + 1) }, 'Increment'), ); }); // Each counter instance has its own local state function App() { return div( h1('Multiple Counters'), counter(), // Counter #1 with its own count signal counter(), // Counter #2 with its own count signal counter(), // Counter #3 with its own count signal ); } ``` #### Key Differences | Aspect | Global Signals | Local Signals | | ------------- | ------------------------------ | ----------------------------------------------------------------------- | | **Scope** | Application-wide | Component instance | | **Creation** | `const signal = signal(value)` | Inside `component(({}, { signal }) => { const count = signal(value) })` | | **Sharing** | Accessible everywhere | Only within component | | **Lifecycle** | Persist until manually cleaned | Cleaned up when component unmounts | | **Use Cases** | Theme, user data, app state | Form state, local counters, component-specific state | #### React Comparison **React - Complex state management:** ```typescript // Global state requires Context/Redux const ThemeContext = createContext(); const UserContext = createContext(); function App() { return ( <ThemeProvider> <UserProvider> <Header /> <MainContent /> </UserProvider> </ThemeProvider> ); } // Local state requires hooks with rules function Counter() { const [count, setCount] = useState(0); const [isEven, setIsEven] = useState(true); useEffect(() => { setIsEven(count % 2 === 0); }, [count]); // Manual dependency return <div>...</div>; } ``` **Tacit-DOM - Simple and intuitive:** ```typescript // Global state - just create signals const theme = signal('light'); const user = signal(null); // Local state - use component function const counter = component(({}, { signal, computed }) => { const count = signal(0); const isEven = computed(() => count.value % 2 === 0); // Automatic dependency return div(/* ... */); }); // No providers, no hooks rules, no manual dependencies ``` #### Best Practices **Use Global Signals for:** - Application theme/settings - User authentication state - Shopping cart contents - API data that needs sharing - Route parameters **Use Local Signals for:** - Form input values - Component-specific toggles - Local counters/timers - Modal open/closed state - Temporary UI state This approach gives you the best of both worlds: simple global state management without the complexity of Context providers, and encapsulated local state without hooks rules. #### 🆕 Alternative Value API Tacit-DOM provides two ways to read and write signal values: **Traditional API: `get()` and `set()`** ```typescript const count = signal(0); // Reading values const currentValue = count.value; // Writing values count.set(10); ``` **Modern API: `value` Property** ```typescript const count = signal(0); // Reading values (getter) const currentValue = count.value; // Writing values (setter) count.value = 10; ``` **Benefits of the `value` property:** - **Cleaner Syntax**: `count.value = 10` vs `count.set(10)` - **Property-like Access**: `count.value` for both reading and writing - **Full Compatibility**: Works alongside `get()` and `set()` methods - **Reactive by Default**: Automatically tracks dependencies when accessed **Both APIs work together seamlessly:** ```typescript const count = signal(0); // Mix and match - both work identically count.set(5); console.log(count.value); // 5 count.value = 10; console.log(count.value); // 10 // Both trigger the same reactive updates count.set(15); count.value = 20; ``` ## 📦 Installation ```bash npm install tacit-dom ``` ### Requirements - **Node.js**: 16.0.0 or higher - **TypeScript**: 4.5.0 or higher (recommended) - **Modern Browsers**: ES2020+ support ### Bundle Options Tacit-DOM provides multiple bundle options to optimize for your use case: ```bash # Full library (default) import { signal, component, div } from 'tacit-dom'; # Signals only (lightweight - ~1.1KB) import { signal, computed, effect } from 'tacit-dom/signals'; # DOM system only (~15KB) import { component, div, render } from 'tacit-dom/dom'; ``` **Bundle Sizes:** - **Full Library**: ~19KB (ESM/UMD/CJS) - **Signals Only**: ~1.1KB (94% smaller than full bundle) - **DOM Only**: ~15KB (21% smaller than full bundle) **Use Cases:** - **Signals Only**: When you only need reactive state management - **DOM Only**: When you need DOM manipulation without signals - **Full Bundle**: When you need the complete feature set ## 🚀 Quick Start Ready to build reactive apps without the React complexity? Dive in! 🏊‍♂️ _No virtual DOM, no reconciliation, no provider hell - just pure, simple reactivity!_ ## 🧩 Components Tacit-DOM provides a simple component system using the `component` function. The key benefit is that **signals created inside a component are local to that component instance**, providing automatic state encapsulation. ### Local Signals with Component Function ```typescript import { component, div, h1, p, button, render } from 'tacit-dom'; // Component function creates local signal scope - must use camelCase name const counter = component(({}, { signal }) => { // This signal is LOCAL to this counter instance // Use signal from component utilities parameter const count = signal(0); return div( { className: 'counter' }, h1('Counter'), p('Count: ', count), // Signal used directly button({ onclick: () => (count.value = count.value + 1) }, 'Increment'), ); }); // Each render creates a separate instance with its own signals render(counter, document.getElementById('app1')); // Counter #1 render(counter, document.getElementById('app2')); // Counter #2 (independent) ``` **Why use `component()`?** - **Automatic Cleanup**: Local signals are cleaned up when component unmounts - **State Encapsulation**: Each instance has its own state - **No State Leakage**: Signals don't interfere between instances - **Memory Management**: Prevents memory leaks from orphaned signals ### Component with Props ```typescript const greeting = component<{ name: string; greeting?: string }>((props) => { return div({ className: 'greeting' }, h1(`${props?.greeting || 'Hello'}, ${props?.name || 'World'}!`)); }); // Usage render(greeting({ name: 'Alice', greeting: 'Welcome' }), document.getElementById('greeting')); ``` ### Component with Local State ```typescript const userProfile = component(({}, { signal }) => { const user = signal({ name: 'John', email: 'john@example.com' }); const isEditing = signal(false); const toggleEdit = () => (isEditing.value = !isEditing.value); return div( { className: 'profile' }, h1('User Profile'), isEditing.value ? div( input({ value: user.value.name, oninput: (e) => (user.value = { ...user.value, name: e.target.value }), }), input({ value: user.value.email, oninput: (e) => (user.value = { ...user.value, email: e.target.value }), }), button({ onclick: toggleEdit }, 'Save'), ) : div(p(`Name: ${user.value.name}`), p(`Email: ${user.value.email}`), button({ onclick: toggleEdit }, 'Edit')), ); }); ``` ### Component with Conditional Rendering using `when` ```typescript const conditionalCounter = component(({}, { signal, computed }) => { const count = signal(0); const isPositive = computed(() => count.value > 0); const isEven = computed(() => count.value % 2 === 0); return div( { className: 'conditional-counter' }, h1('Conditional Counter'), p(`Count: ${count.value}`), // Use when() for conditional rendering when(isPositive, () => div({ className: 'positive-message' }, '✅ Count is positive!')), when(isEven, () => div({ className: 'even-message' }, '🔢 Count is even!')), when( computed(() => count.value === 0), () => div({ className: 'zero-message' }, '🎯 Count is zero!'), ), button({ onclick: () => (count.value = count.value + 1) }, 'Increment'), button({ onclick: () => (count.value = count.value - 1) }, 'Decrement'), ); }); ``` ### Complex Component with `when` ```typescript const dashboard = component(({}, { signal, computed }) => { const user = signal({ name: 'Alice', role: 'admin', isOnline: true }); const notifications = signal([]); const isLoading = signal(false); const isAdmin = computed(() => user.value.role === 'admin'); const hasNotifications = computed(() => notifications.value.length > 0); return div( // Header section header( { className: 'dashboard-header' }, h1(`Welcome, ${user.value.name}`), when(isOnline, () => span({ className: 'status online' }, '🟢 Online')), when(!isOnline, () => span({ className: 'status offline' }, '🔴 Offline')), ), // Main content main( { className: 'dashboard-content' }, // Admin panel - only visible to admins when(isAdmin, () => section( { className: 'admin-panel' }, h2('Admin Panel'), button({ onclick: () => console.log('Admin action') }, 'Admin Action'), ), ), // Notifications - only visible when there are notifications when(hasNotifications, () => section( { className: 'notifications' }, h2('Notifications'), ...notifications.value.map((notification) => div({ className: 'notification' }, notification.message)), ), ), // Loading state when(isLoading, () => div({ className: 'loading' }, 'Loading...')), ), // Footer footer({ className: 'dashboard-footer' }, p('Dashboard v1.0')), ); }); ``` ```typescript import { signal, computed, component, div, h1, p, button, render } from 'tacit-dom'; // Create global reactive signals - accessible anywhere in your app const count = signal(0); const user = signal({ name: 'John', email: 'john@example.com' }); // Create a reactive component without props const counter = component(({}, { computed }) => { // Create a local computed value const doubleCount = computed(() => count.value * 2); // Create a reactive element return div( { className: 'counter' }, h1('Counter Example'), p('Count: ', count), p('Double Count: ', doubleCount), p('User: ', user.value.name), button( { onclick: () => (count.value = count.value + 1), }, 'Increment', ), ); }); // Create a component with typed props const greeting = component<{ name: string; greeting?: string }>((props) => { return div( { className: 'greeting' }, h1(`${props?.greeting || 'Hello'}, ${props?.name || 'World'}!`), p('User: ', user.value.name), p('Email: ', user.value.email), ); }); // Another component can access the same global state const userProfile = component(() => { return div( { className: 'profile' }, h1('User Profile'), p('Name: ', user.value.name), p('Email: ', user.value.email), ); }); // Render components to DOM render(counter, document.getElementById('app')); render(greeting({ name: 'Alice', greeting: 'Welcome' }), document.getElementById('greeting')); render(userProfile, document.getElementById('profile')); ``` ## 🎭 Conditional and List Rendering Tacit-DOM provides powerful utilities for conditional rendering and list management that automatically update when signals change. ### Conditional Rendering with `when` ```typescript import { when, signal, div, h1, computed, component } from 'tacit-dom'; // Basic conditional rendering const isVisible = signal(true); const element = when(isVisible, div('This is visible')); // With computed values const count = signal(0); const isPositive = computed(() => count.value > 0); const element = when(isPositive, div(`Count is positive: ${count.value}`)); // Inside components - perfect for conditional UI elements const statusIndicator = component(({}, { signal }) => { const status = signal('loading'); return div( when(status === 'loading', div('⏳ Loading...')), when(status === 'success', div('✅ Success!')), when(status === 'error', div('❌ Error occurred')), ); }); // Complex conditions with computed values const userCard = component(({}, { signal, computed }) => { const user = signal({ name: 'John', age: 25, isVerified: true }); const isAdult = computed(() => user.value.age >= 18); const showVerification = computed(() => user.value.isVerified && isAdult.value); return div( h1(user.value.name), when(isAdult, p('Adult user')), when(showVerification, div({ className: 'verified-badge' }, '✓ Verified')), ); }); ``` ### List Rendering with `map` and `mapArray` ```typescript import { map, mapArray, signal, div, li } from 'tacit-dom'; // Basic array mapping with map (returns array of elements) const items = signal(['a', 'b', 'c']); const listElements = map(items, (item) => div(item)); // listElements is an array: [div('a'), div('b'), div('c')] // With filtering const numbers = signal([1, 2, 3, 4, 5]); const evenNumbers = map( numbers, (num) => div(num), (num) => num % 2 === 0, ); // evenNumbers is an array: [div(2), div(4)] // Using mapArray for reactive DOM updates const fruits = signal(['apple', 'banana', 'cherry']); const fruitList = mapArray(fruits, (fruit, index) => li({ className: `fruit-${index}` }, fruit)); // fruitList is a container element that updates when fruits changes // mapArray with filtering const colors = signal(['red', 'blue', 'green', 'yellow']); const warmColors = mapArray( colors, (color) => div({ className: 'warm-color' }, color), (color) => ['red', 'yellow'].includes(color), ); // warmColors is a container element showing only warm colors ``` ### Multiple Elements with `when` ```typescript import { div, h1, p, signal, when, component } from 'tacit-dom'; // Return multiple elements without a wrapper using when const myComponent = component(({}, { signal }) => { const showHeader = signal(true); const showFooter = signal(true); return div(when(showHeader, h1('Header')), div('Main content'), when(showFooter, div('Footer'))); }); // Using when for conditional navigation elements const navigation = component(({}, { signal }) => { const isLoggedIn = signal(false); return div( nav( { className: 'main-nav' }, a({ href: '/' }, 'Home'), a({ href: '/about' }, 'About'), when(isLoggedIn, a({ href: '/profile' }, 'Profile')), when(!isLoggedIn, a({ href: '/login' }, 'Login')), ), // Conditional user info when(isLoggedIn, div({ className: 'user-info' }, 'Welcome back!')), ); }); ``` ## 🛠️ Development For development setup, building, testing, and project structure, see [DEVELOPMENT.md](DEVELOPMENT.md). ## 📚 Documentation Tacit-DOM provides comprehensive documentation covering all aspects of the library. The documentation is organized into logical sections to help you find what you need quickly. ### 🚀 Getting Started - **[📖 API Reference](docs/API.md)**: Complete API documentation with examples - **[🔄 Signals Guide](docs/SIGNALS.md)**: Learn about reactive signals, the foundation of Tacit-DOM - **[💡 Signals Usage Guide](docs/SIGNAL_USAGE_GUIDE.md)**: Practical examples and common patterns ### 🎨 DOM & Components - **[🌐 DOM Internals](docs/DOM_INTERNALS.md)**: Deep dive into DOM manipulation and reactive updates - **[🎨 ClassName Utility](docs/CLASSNAMES.md)**: Dynamic CSS class management (recommended) ### 🔧 Advanced Features - **[🌐 Router Guide](docs/ROUTER.md)**: Advanced client-side routing with object map routes, nested paths, optional parameters, and error handling - **Object Map Routes**: Cleaner syntax with `{ '/path': { component } }` structure - **Optional Parameters**: Flexible routing with `:?param` syntax - **Enhanced Component Props**: Direct access to `path`, `params`, `search`, and `data` - **Nested Route Patterns**: Natural hierarchical route organization - **Link Component**: `link()` function for creating navigation links with automatic routing ### 🛠️ Development & Internals - **[⚙️ Development Guide](docs/DEVELOPMENT.md)**: Setup, building, testing, and contributing - **[🔍 Signal Internals](docs/SIGNAL_INTERNALS.md)**: Technical implementation details ### 📚 Component Naming Convention Tacit-DOM uses a clean, intuitive naming convention: | Function | Type | Description | | ------------------ | -------------- | -------------------------- | | **`component<P>`** | `Component<P>` | Create reactive components | ```typescript import { component, Component, div } from 'tacit-dom'; // Component without props const simpleCounter = component(() => { return div('Hello World'); }); // Component with typed props const greeting = component<{ name: string }>((props) => { return div(`Hello, ${props?.name || 'World'}!`); }); ``` ## 🛠️ API Reference For detailed API documentation, see [API.md](docs/API.md). ### Core Functions #### `signal<T>(initialValue: T): Signal<T>` Creates a reactive signal with an initial value. ```typescript const count = signal(0); count.value = 5; // Update value console.log(count.value); // Get current value ``` #### `computed<T>(fn: () => T): Computed<T>` Creates a computed value that automatically updates when dependencies change. ```typescript const doubleCount = computed(() => count.value * 2); ``` #### `render(element: HTMLElement, container: HTMLElement): void` Renders a reactive element into a DOM container. ```typescript render(counter(), document.getElementById('app')); ``` #### `cleanup(element: HTMLElement): void` Removes an element from the DOM and cleans up any associated resources. ```typescript const element = div('Hello World'); render(element, container); // Later, clean up the element cleanup(element); ``` #### `link(props: { to: string; className?: string; children: any; [key: string]: any }): HTMLElement` Creates a navigation link that integrates with the router system. ```typescript import { link } from 'tacit-dom'; const navigation = nav( link({ to: '/', className: 'nav-link' }, 'Home'), link({ to: '/about', className: 'nav-link' }, 'About'), link({ to: '/contact', className: 'nav-link' }, 'Contact'), ); ``` ### DOM Elements All HTML elements are available as factory functions: ```typescript import { div, h1, h2, h3, p, button, input, label, span, a } from 'tacit-dom'; const element = div( { className: 'container' }, h1('Hello World'), h2('Subtitle'), h3('Section'), p('This is a paragraph'), button({ onclick: handleClick }, 'Click me'), input({ type: 'text', placeholder: 'Enter text' }), label({ for: 'input-id' }, 'Input Label'), ); ``` ### Styling Tacit-DOM supports React-like style props with both static and reactive styles: ```typescript // String-based styles div({ style: 'background-color: red; color: white;' }, 'Content'); // Object-based styles (React-like) div( { style: { backgroundColor: 'red', color: 'white', fontSize: 16, padding: 15, }, }, 'Content', ); // Reactive styles const colorSignal = signal('red'); div({ style: { backgroundColor: colorSignal } }, 'Content'); // Computed styles const dynamicStyle = computed(() => ({ backgroundColor: colorSignal.value, fontSize: sizeSignal.value, })); div({ style: dynamicStyle }, 'Content'); ``` **Style Features:** - **CamelCase to kebab-case**: Properties like `backgroundColor` automatically convert to `background-color` - **Automatic units**: Numeric values for properties like `fontSize` automatically get `px` units - **Mixed types**: Support for both string and numeric values - **Reactive updates**: Styles automatically update when signals change