Client-Side JavaScript
Coralite's component system allows you to bundle client-side logic directly with your HTML templates using the
defineComponent helper. This guide explains how to create interactive components, manage data flow,
and access DOM elements using the "Flat Options" API.
In Coralite, standard .html pages should remain as declarative consumers. All imperative logic, DOM manipulation, and state management should be reserved for the client block within a component. Avoid using <script> tags in pages to interact with components.
The defineComponent Helper #
The defineComponent function is the core of Coralite's component logic. It separates server-side data
fetching and attribute coercion from client-side interactivity using four strictly isolated blocks.
import { defineComponent } from 'coralite'
import db from 'node:database' // Stripped from client bundle
export default defineComponent({
// Inputs: Coerced from HTML attributes
attributes: {
initialCount: { type: Number, default: 0 }
},
// Server-Side: Asynchronous data fetching (runs at build-time)
async server(context) {
const user = await db.users.findFirst();
return { userName: user.name }
},
// Derived State: Pure functions (Read-Only)
getters: {
greeting: (state) => `Hello, ${state.userName}!`
},
// Client-Side: Controller (Read/Write)
client({ state, signal, refs, observe, emit, ...pluginContext }) {
console.log('Component mounted for user:', state.userName)
// Register an observer to react to count changes
observe('initialCount', (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})
const btn = refs('btn')
btn.addEventListener('click', () => {
state.initialCount++; // Reactive mutation
emit('count-updated', { count: state.initialCount }); // Dispatch custom event
}, { signal });
}
})
The Client Context #
The client block is executed in the browser when the component mounts. It receives a flattened context object via destructuring:
state: A Read/Write Reactive Proxy. This unified state contains state fromattributes,serverstate, andgetters. Any mutation here triggers a re-render and re-evaluates getters.observe: A functionobserve(key, callback)to register explicit side-effects on state changes. The callback receives(newValue, oldValue). Observers are automatically cleaned up on component unmount.emit: A helper functionemit(name, detail?, options?)to dispatch custom DOM events from the host custom element. Configured withbubbles: trueandcomposed: trueby default.refs: A helper function to query DOM elements within the component's root that have arefattribute.signal: AnAbortSignaltied to the component's lifecycle. Always use this to clean up event listeners or abort pending fetch requests when the component unmounts.root: The root HTML element instance of the Web Component.instanceId: A unique identifier string for this specific component instance.
State Observation & Side-Effects (The observe Pattern) #
While Coralite components update their templates automatically when state changes, you often need to perform imperative side-effects (such as updating external DOM libraries, sending analytics, or making network requests) in response to state updates. The observe function provides a clean, declarative pattern for handling these state changes.
Registering Observers #
Inside the client function, you can call observe(key, callback). The callback is invoked synchronously whenever the state property matching the key is modified and its new value differs from its old value.
export default defineComponent({
client({ state, observe }) {
// Observe pre-defined properties (e.g. from attributes or server)
observe('score', (newVal, oldVal) => {
console.log(`Score changed from ${oldVal} to ${newVal}`)
})
// Observe dynamic properties set at runtime
observe('status', (newVal) => {
console.log(`Status is now: ${newVal}`)
})
}
})
Best Practices & Safeguards #
-
Avoid Mutating State Inside Observers: Modifying state properties inside an observer callback can trigger infinite reactivity loops. If you need to compute derived state, always use the
gettersblock ofdefineComponentinstead.In development or testing environments, Coralite will log a warning if it detects a state mutation during observer execution:
[Coralite Warning]: State mutation detected inside an observe() callback. This can cause infinite reactivity loops. Use getters for derived state instead. - Automatic Lifecycle Cleanup: You do not need to manually unregister observers. Coralite automatically cleans up all registered observers when the component is disconnected from the DOM to prevent memory leaks.
Native Reactivity & Performance #
Coralite uses a Lazy Deep Proxy system. Unlike frameworks that recursively make your entire data structure reactive upfront, Coralite only pays the performance cost for nested objects the exact moment they are accessed.
Additionally, Coralite follows the Smart State, Dumb Template philosophy. Instead of a Virtual DOM, Coralite performs direct token replacement ({{ key }}) in the template, making updates extremely fast and memory-efficient.
Using External Libraries #
Because Coralite leverages standard ESM, you can use dynamic import() statements directly inside your client function to load client-side dependencies. These are automatically detected by the build tool and bundled for the browser.
export default defineComponent({
async client({ state, signal, refs, ...pluginContext }) {
const { default: confetti } = await import('https://esm.sh/canvas-confetti@1.6.0')
const btn = refs('celebrateBtn')
btn.addEventListener('click', () => {
confetti();
}, { signal })
}
})
Component Communication & Event Dispatching #
Coralite provides a built-in emit(name, detail?, options?) helper on CoraliteScriptContext to send custom events from child components to parent components or external DOM listeners.
When calling emit('user-updated', { name: 'Alice' }), Coralite automatically constructs a native CustomEvent and dispatches it from the component's host element with bubbles: true and composed: true enabled by default. This ensures the event bubbles seamlessly out of Shadow/Light DOM boundaries to parent containers.
// Inside a child component's client block
client({ state, emit }) {
// Emits a 'user-updated' CustomEvent from host element with bubbles: true & composed: true
emit('user-updated', { id: state.userId, name: state.userName });
}
Parent components can listen for child events by targeting child element references using refs inside their own client block:
// Inside a parent component's client block
client({ signal, refs }) {
const childComponent = refs('childUserCard');
childComponent.addEventListener('user-updated', (event) => {
console.log('User updated in child component:', event.detail.name);
}, { signal });
}