Skip to main content

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.

⚠️ Warning: AST Splicing (Host Tag Removal)

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.

HTML
Code copied!
<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>
New in V1: Explicit Imports

Framework utilities like parseHTML or transform are no longer available in the global scope. You must import them from coralite/utils. The defineComponent helper is available via coralite or coralite/plugins.

Properties: Build-time Evaluation #

In a static component, {{ data }} simply maps to an HTML attribute. In a dynamic component, you use the properties function to evaluate data at build time (during server-side rendering).

The properties function receives a pruned, localized context object containing properties (attributes), page metadata, the root node, and the module blueprint.

HTML
Code copied!
<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({
    properties: (context) => ({
      // 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!".

Top-level vs. Computed Properties #

In Coralite, the properties key can be defined in two ways: as a single top-level function or as an object containing computed property functions. The choice between them depends on three factors: Context Access, Reactivity, and Encapsulation.

1. Context Access #

2. Reactivity (Client-side Execution) #

3. Encapsulation and Efficiency #

Code Example: Side-by-Side Comparison #

HTML
Code copied!
<script type="module">  
  import { defineComponent } from 'coralite'
  
  // Example A: Top-level function (Server-only, full context)
  export const TopLevelExample = defineComponent({
    properties: async (context) => {
      const data = await fetch(`https://api.example.com/user/${context.properties.id}`)
      const user = await data.json()
      return {
        userName: user.name,
        userRole: user.role,
        currentPage: context.page.url.pathname
      }
    }
  })
  
  // Example B: Computed properties (Isomorphic, reactive)
  export const ComputedExample = defineComponent({
    properties: {
      // Only has access to props, runs on both server and client
      fullName: ({ firstName, lastName }) => {
        return `${firstName} ${lastName}`.trim()
      }
    }
  })
</script>

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 localized context. You can mutate tags, replace them, or map over them conditionally.

HTML
Code copied!
<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, context) => {
        // 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 Properties & Bridging Data #

If your component needs to fetch data from an API or read files before rendering, use the properties function. Top-level imports (like reading from the file system or a database) can only be used here on the server side.

The object returned by properties is injected directly into your component's declarative HTML template and merged into the client script's runtime context.properties. This makes it the perfect bridge across the serialization boundary. Keep in mind that anything returned by properties will be stringified using serialize-javascript before being sent to the browser.

HTML
Code copied!
<template id="my-data-component">
  <div class="message">
        Server says: {{ fetchedData }}
      </div>
</template>

<script type="module">  
  import { defineComponent } from 'coralite/plugins'
  // Top-level imports are safely allowed here!
  import myDatabase from 'my-database'
  
  export default defineComponent({
    // This runs ON THE SERVER during the build process.
    // It is marked async because we are awaiting a database call.
    properties: async (context) => {
      const data = await myDatabase.fetchData()
  
      // Return flat primitive data to bridge the serialization gap
      return {
        fetchedData: data.message
      }
    }
  })
</script>

Client-Side Interactivity (The Browser Script) #

The 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[cite: 48, 49].

The script receives a flattened context object containing things like id, properties, page, module and root (the top-level Light DOM node of the custom element)[cite: 50, 51]. The AST parser automatically detects dynamic imports inside this script and bundles them without requiring manual configuration[cite: 55, 61, 62].

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[cite: 66, 67].

Here is the standard pattern for declarative Reactivity:

HTML
Code copied!
<template id="child-element">
  <h3 ref="titleDisplay">{{ title }}</h3>
  <p>Status: {{ computedStatus }}</p>
</template>

<script type="module">  
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    // State: Runs on the server, bridging data to the client
    properties (context) {
      const title = context.properties.title || 'Default Title'
  
      // Return flat primitive data to safely cross the serialization boundary
      return {
        title,
        computedStatus: title === 'Active' ? 'Online' : 'Offline'
      }
    },
    // Behavior: Runs natively in the browser
    script ({ properties }) {
      console.log('Child mounted with title:', properties.title)
    }
  })
</script>
HTML
Code copied!
<template id="parent-element">
  <div ref="target"></div>
</template>

<script type="module">  
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    // Notice the beautifully flattened, destructured context
    script ({ refs }) {
      const target = refs('target')
  
      // Instantiate the component imperatively (AST auto-detects this!) [cite: 63, 64]
      const child = document.createElement('child-element')
      child.setAttribute('title', 'Pending')
  
      // Mount to DOM (Fires connectedCallback & renders DOM)
      target.replaceWith(child)
  
      // Reactivity test: Updating attribute triggers MutationObserver & child re-render [cite: 54, 55, 72]
      setTimeout(() => {
        child.setAttribute('title', 'Active')
      }, 1000)
    }
  })
</script>

Notice the clean destructuring of { refs } on the script context? This is Coralite's safe way to query the DOM. The refs plugin automatically queries the DOM using uniquely compiled IDs[cite: 112, 113]. Learn more in the Managing the DOM (Refs) guide.


For strict API type definitions, arguments, and return types, see the defineComponent API Reference.

Start Building with Coralite!

Use the scaffolding script to get jump started into your next project with Coralite

Copied commandline!