Core Concepts: Dynamic Components
While Static Components are great for simple layouts, modern web development often requires more power. When your component needs data processing, dynamic attribute binding, custom slot processing, or client-side interactivity, you must create a Dynamic Component.
Dynamic components are powered by Coralite's core built-in plugin: defineComponent.
Just like static components, Coralite's server completely deletes the host tag of declarative dynamic components and replaces it with the template's inner HTML.
If you try to style the host tag (e.g., <my-component class="foo">) in CSS or use it as a flex container, it will break because the tag simply won't exist in the final HTML.
Upgrading a Component #
To turn a static component into a dynamic one, you simply append a <script type="module"> tag below your <template> and export defineComponent.
<template id="my-dynamic-component">
<div>...</div>
</template>
<!-- The addition of this script block makes it dynamic -->
<script type="module">
import { defineComponent } from 'coralite'
export default defineComponent({
// Configuration goes here
})
</script>
Tokens: Build-time Evaluation #
In a static component, {{ data }} simply maps to an HTML attribute. In a dynamic component, you can define Tokens. Tokens evaluate data at build time (during server-side rendering).
Tokens can be static strings or computed functions. Computed functions receive a values object, which contains all the attributes passed to the component, plus page metadata.
<template id="greeting-card">
<div class="card">
<h2>{{ formattedGreeting }}</h2>
<p>Status: {{ status }}</p>
</div>
</template>
<script type="module">
import { defineComponent } from 'coralite'
export default defineComponent({
tokens: {
// Static string token
status: 'Active',
// Computed function token receiving 'name' attribute
formattedGreeting: ({ name }) => {
const cleanName = name ? name.trim().toUpperCase() : 'GUEST'
return `Welcome, ${cleanName}!`
}
}
})
</script>
When used as <greeting-card name=" alice "></greeting-card>, the output will be "Welcome, ALICE!".
Best Practices for Data Binding #
Developers should not pass complex objects or arrays as stringified JSON attributes to components. Passing data='[{"id": 1}, {"id": 2}]' into an HTML attribute is a common anti-pattern in vanilla HTML/JS, but Coralite actively discourages it.
Instead, the framework expects developers to handle complex data structures in their server-side script logic (like mapping over an array) and pass only decomposed primitive values (strings, numbers, booleans) as individual attributes to their components.
Slots: Server-Side Processing #
While static components use the standard <slot> tag blindly, defineComponent allows you to intercept and process slot content on the server before it's rendered.
Slot functions receive an array of parsed HTML nodes (slotNodes) and the current values. You can mutate tags, replace them, or map over them conditionally.
<template id="smart-list">
<ul class="list">
<slot name="items"></slot>
</ul>
</template>
<script type="module">
import { defineComponent } from 'coralite'
export default defineComponent({
slots: {
// The slot name matches <slot name="items"> in the template
items: (slotNodes, values) => {
// Transform the content passed into the slot
return slotNodes.map(node => {
// If the user passed <li> elements, automatically add a class
if (node.type === 'tag' && node.name === 'li') {
node.attributes.class = 'list-item-styled'
}
return node
})
}
}
})
</script>
Server-Side Setup #
If your component needs to fetch data from an API or read files before rendering, use the client.setup function.
The object returned by setup is seamlessly merged into the component's values object, making that data available to tokens and client scripts. Keep in mind that anything returned by setup will be stringified using serialize-javascript before being sent to the client.
While setup is used for data fetching on the server, if you need true Top-Level Await (TLA) for client-side functionality (like awaiting a module initialization in the browser), you must use client.imports which is designed to handle TLA natively for the client.
<script type="module">
import { defineComponent } from 'coralite'
export default defineComponent({
client: {
// This runs ON THE SERVER during the build process
setup: async (values) => {
const response = await fetch('https://api.example.com/data')
const data = await response.json()
// Merge this data into the component's values
return {
fetchedData: data.message
}
}
},
tokens: {
// Now we can use the fetched data in our template
apiMessage: ({ fetchedData }) => fetchedData
}
})
</script>
Client-Side Interactivity (The Browser Script) #
The client.script function is what makes Coralite components interactive. This function is serialized and bundled to run in the user's browser after the page loads.
The script receives a context object (CoraliteScriptContent) containing things like id, values, root, and helpers. The root property has been removed. Plugins and scripts should query the document directly. Coralite strictly renders into the Light DOM, not the Shadow DOM, so elements inherit global CSS.
Reactivity & Imperative Web Components #
Coralite embraces a strictly SSR first approach. Statically placed HTML tags in your templates are not reactive. To use a true Web Component with a client-side lifecycle that reacts to attribute changes, you must utilize the Imperative Requirement.
- The Imperative Requirement: A parent component must imperatively instantiate the child component via
document.createElement()and append it to the DOM. - Bundler Declaration: Crucially, the parent must declare the dynamically created child's tag name in its
client.componentsarray so the bundler knows to include the child's logic in the build. - How it Works: Once mounted, updating attributes on the child element (via
setAttribute()) triggers Coralite's internalMutationObserver. This observer automatically updates thevaluescontext, re-evaluates computed token functions, and updates the DOM.
Here is the standard pattern for declarative Reactivity:
<!-- 1. The Child Component (child-element.html) -->
<template id="child-element">
<h3 ref="titleDisplay">{{ title }}</h3>
<p>Status: {{ computedStatus }}</p>
</template>
<script type="module">
import { defineComponent } from 'coralite'
export default defineComponent({
tokens: {
title: 'Default Title',
computedStatus: (values) => (values.title === 'Active' ? 'Online' : 'Offline')
},
client: {
// Setup runs computed tokens on the client
setup: () => ({
computedStatus: (values) => (values.title === 'Active' ? 'Online' : 'Offline')
}),
script: (context) => {
console.log('Child mounted with title:', context.values.title)
}
}
})
</script>
<!-- 2. The Parent Component (parent-element.html) -->
<template id="parent-element">
<div ref="target"></div>
</template>
<script type="module">
import { defineComponent } from 'coralite'
export default defineComponent({
client: {
// MUST declare the dynamically created component for the bundler
components: ['child-element'],
script: (context) => {
const target = context.helpers.refs('target')
// 1. Instantiate the component imperatively
const child = document.createElement('child-element')
child.setAttribute('title', 'Pending')
// 2. Mount to DOM (Fires connectedCallback & renders DOM)
target.replaceWith(child)
// 3. Reactivity test: Updating attribute triggers MutationObserver & re-render
setTimeout(() => {
child.setAttribute('title', 'Active')
}, 1000)
}
}
})
</script>
Notice the use of ref="target" and context.helpers.refs('target')? This is Coralite's safe way to query the DOM. The refs plugin automatically queries the DOM using uniquely compiled IDs. Learn more in the Managing the DOM (Refs) guide.
For strict API type definitions, arguments, and return types, see the defineComponent API Reference.