Skip to content

🧩 Embedding Components

Andre Kless edited this page Mar 28, 2026 · 26 revisions

Embedding in ccmjs follows a strict separation between component definitions and component instances. A component defines what can be instantiated, while an instance represents a concrete app created from a component and a specific configuration.

Component + Configuration → Instance → App

This principle forms the foundation of composition in ccmjs.

The ccmjs framework provides three closely related API functions for embedding:

  • ccm.component() – Register a component definition
  • ccm.instance() – Create a component instance
  • ccm.start() – Create and run an instance

They build upon each other and differ mainly in how much control they give over instantiation and execution.

⚙️ ccm.component()

Registers a ccmjs component definition and prepares it for instantiation.

ccm.component() operates purely on the definition level. It does not create app instances but prepares a reusable component definition for creating instances bound to a specific framework version.

Syntax

ccm.component( component [, config ] )  Promise<Component>

Parameters

Parameter Type Description
component Object | string Component definition object, or URL to a component file.
config Object (optional) Priority configuration that overrides or extends the component’s default configuration. This configuration is merged into the component’s default configuration and used as the base for all instances created from this component.

Return Value

Returns a Promise that resolves to a clone of the registered component definition. The returned component object provides the following methods, which can be used to create instances manually:

component.instance( config [, area ] )  Promise<Instance>
component.start( config [, area ] )  Promise<Instance>

Example

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Registering a Component Definition</title>
    <script src="https://ccmjs.github.io/framework/ccm.js"></script>
    <script type="module">
      // Load and register the component once
      const component = await ccm.component("https://ccmjs.github.io/hello/ccm.hello.mjs", {
        name: "Mika", // Merged into the component's default instance configuration
      });

      // Create and start multiple instances with different configurations
      component.start({ name: "Jane" }, document.getElementById("div1"));
      component.start({ name: "John" }, document.getElementById("div2"));
      component.start({}, document.getElementById("div3"));
    </script>
  </head>
  <body>
    <div id="div1"></div>
    <div id="div2"></div>
    <div id="div3"></div>
  </body>
</html>

The web page then contains:

Hello Jane
Hello John
Hello Mika

Description

ccm.component() is the lowest-level entry point for working with component definitions. It loads a component definition, resolves the required ccmjs framework version, and registers the component internally. If the component depends on a different ccmjs version than the currently active one, the required version is automatically loaded and the operation is delegated to that framework instance. As a result, every component is always bound to its own compatible ccmjs version.

The component definition is registered once per version and stored in a private internal registry. External JavaScript code only receives a cloned copy of the component object. This ensures that once a component version is registered, it cannot be modified accidentally or intentionally by other scripts.

The optional config parameter is merged with the component’s default configuration and serves as the base configuration for all instances created from this component object.

Notes

  • Calling ccm.component() multiple times for the same component does not reload or re-register the component.
  • The returned component object is not an instance.
  • No DOM elements are created until component.instance() or component.start() is called.
  • Component URLs must follow the ccmjs component filename convention (see ccmjs Conventions).

⚙️ ccm.instance()

Creates a concrete instance from a registered ccmjs component.

Syntax

ccm.instance( component [, config ] [, area ] )  Promise<Instance>
Parameter Type Description
component Object | string Component definition object, or URL to a component file. If the component is not yet registered, it is registered implicitly via ccm.component().
config Object (optional) Instance-specific configuration. This configuration is merged with the component’s prepared default configuration and fully integrated into the instance.
area Element (optional) DOM element into which the instance will be embedded. If omitted, an on-the-fly <div> is created as the instance host and can be inserted into the DOM later via instance.host.

Return Value

Returns a Promise that resolves to the created ccmjs instance.

The returned instance exposes the following relevant properties:

  • instance.host – Host DOM element of the instance
  • instance.root – Shadow DOM root (if enabled)
  • instance.element – Content element rendered by the component
  • instance.parent – Parent instance (if embedded as a dependency)
  • instance.children – Child instances created via declarative dependencies

The instance reference is returned exclusively to the caller. No other global access path exists, ensuring encapsulation and preventing unintended external interaction.

Example

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Creating a Component Instance</title>
    <script src="https://ccmjs.github.io/framework/ccm.js"></script>
    <script type="module">
      const instance = await ccm.instance('https://ccmjs.github.io/hello/ccm.hello.mjs', {
        name: 'Mika'
      });

      // Insert the instance into the DOM manually
      document.body.appendChild(instance.host);

      // Start the instance explicitly
      await instance.start();
    </script>
  </head>
  <body></body>
</html>

Description

ccm.instance() creates a concrete instance from a component and a specific configuration. If the component has not been registered yet, it is registered automatically using ccm.component().

During instance creation, the configuration is fully prepared and merged, and all declarative dependencies are resolved at runtime. This may include:

  • Loading additional components, data, functions, resources
  • Creating nested instances, datastore accessors
  • Embedding complete apps

As a result, instance creation can trigger the construction of an entire dependency tree.

For each instance, a dedicated DOM structure is created. This includes a host element (instance.host), a Shadow DOM (instance.root, open by default), and a content element (instance.element) that the component renders into. Shadow DOM usage can be controlled via the config.root property ("open", "closed", or false). The resulting structure is conceptually as follows:

instance.host
└─ #shadow-root (open)
   └─ instance.element

During instance creation, a loading indicator is rendered in the target DOM area to provide visual feedback while dependencies are resolved.

Each instance participates in a well-defined lifecycle that spans the entire dependency tree. After an instance is created, optional init() methods are invoked top-down, starting from the root instance and continuing recursively through all dependent child instances. This phase is intended for early setup work such as preparing state or wiring internal references. This top-down execution ensures that parent instances can adjust configuration or state before child instances depend on it.

Once all instances have been initialized, optional ready() methods are executed bottom-up, beginning with the deepest child instances and propagating back to the root. At this point, all dependencies are fully available and the instance hierarchy is stable.

As a rule of thumb:

  • init() is used for dynamic pre-configuration.
  • ready() is used for the first meaningful actions that require all dependencies to be present.

The following simplified example illustrates this lifecycle for a linear dependency chain of three instances:

A
└─ B
   └─ C

The resulting execution order is:

init():  A → B → C
ready(): C → B → A
// Component A
this.init  = async () => console.log("init A");
this.ready = async () => console.log("ready A");

// Component B
this.init  = async () => console.log("init B");
this.ready = async () => console.log("ready B");

// Component C
this.init  = async () => console.log("init C");
this.ready = async () => console.log("ready C");

// Embedding with declarative dependencies
ccm.instance("A", {
  child: [ "ccm.instance", "B", {
    child: [ "ccm.instance", "C" ]
  }]
});

When multiple dependencies exist on the same level, the relative execution order between sibling instances is intentionally undefined. After lifecycle initialization has completed, the instance is fully prepared but not yet running. At this point, it is important to distinguish between ready() and start().

The ready() method is part of the initialization lifecycle and is guaranteed to run at most once for a given instance. It finalizes preparation but does not represent the execution of application logic.

In contrast, start() marks the execution phase of an instance. It is not part of the initialization lifecycle. It is responsible for rendering, activating behavior, or otherwise running the application logic and may be called multiple times during the lifetime of an instance, for example when an app is restarted or re-rendered.

For instances created via ccm.start(), this final step happens automatically. For instances created via ccm.instance(), calling start() is controlled explicitly by the caller.

⚙️ ccm.start()

Creates, initializes, and immediately starts a ccmjs component instance.

ccm.start() does not introduce additional lifecycle behavior. It is equivalent to calling ccm.instance() followed by instance.start().

Syntax

ccm.start( component [, config ] [, area ] )  Promise<Instance>

The syntax and parameters are identical to ccm.instance().

Return Value

Returns a Promise that resolves to the created and started ccmjs instance.

The returned instance is identical to one created via ccm.instance() and exposes the same properties and guarantees.

Example

// Fully automatic: create, initialize, and run
ccm.start('./ccm.hello.js', { name: 'Mika' }, document.body);

Equivalent manual control using ccm.instance():

const instance = await ccm.instance('./ccm.hello.js', { name: 'Mika' });
await instance.start();

Description

ccm.start() is the highest-level and most convenient entry point for embedding components.

Both ccm.instance() and ccm.start() perform the same steps up to lifecycle initialization:

  1. Component registration (if necessary)
  2. Configuration preparation and merging
  3. Runtime dependency resolution
  4. Instance creation
  5. Lifecycle execution
    1. init() is invoked top-down across the dependency tree
    2. ready() is invoked bottom-up once all instances are initialized

After the ready() phase, the instance is fully prepared but not yet running. Application logic is executed only when the instance’s start() method is called. ccm.start() performs this final step automatically and always returns a running instance. When using ccm.instance(), starting the instance is an explicit decision made by the caller.

Notes

  • ccm.start() is ideal for standalone apps and simple embeddings.
  • It offers the least manual control, but the most concise usage.

📄 Embedding via <ccm-app>

In addition to programmatic embedding via ccm.start(), ccmjs also supports a declarative way of embedding components directly in HTML using the <ccm-app> custom element.

Internally, this mechanism simply translates the declarative definition into a call to ccm.start() using the provided component and configuration.

This feature is optional and intended as a convenience mechanism. The primary and interaction model of ccmjs remains explicit embedding via JavaScript, which enables dynamic composition, runtime configuration, and full programmatic control.

This approach allows components to be defined and configured without writing JavaScript code explicitly. The component URL and its configuration are defined declaratively using HTML attributes:

<ccm-app
  component="https://ccmjs.github.io/hello/ccm.hello.mjs"
  config='{"name":"Mika"}'>
</ccm-app>

Alternatively, configuration can be provided via an inline <script type="application/json"> element:

<ccm-app component="https://ccmjs.github.io/hello/ccm.hello.mjs">
  <script type="application/json">
    {
      "name": "Mika"
    }
  </script>
</ccm-app>

If both the config attribute and an inline JSON script are present, the inline configuration takes precedence and overrides values from the attribute.

🔀 Config Resolution Order

When creating a component instance, configuration values may originate from multiple sources. ccmjs merges these sources in a defined priority order. Lower layers provide default values, while higher layers override them. This merging process is deterministic and consistent across all instances.

Configuration Source Priority (low → high):

  1. Component default configuration (component.config)
  2. Configuration update passed to ccm.component()
  3. Instance configuration passed to ccm.instance() or ccm.start()

The instance configuration (3) may itself reference a base configuration via the config property. If present, this base configuration is resolved first (recursively if necessary) and then overridden by properties defined in instance configuration.

The following example shows how the different configuration sources interact and how higher layers override lower layers.

1️⃣ Component Definition

The component defines a default configuration:

export const component = {
   name: "quiz",
   config: {
      feedback: false,
      shuffle: false
   }
};

2️⃣ Component Registration

When registering the component, the default configuration can be updated:

const comp = await ccm.component("./ccm.quiz.mjs", {
  shuffle: true  // updates component default configuration
});

Resulting configuration after component registration:

{
  feedback: false,
  shuffle: true
}

This becomes the effective default configuration used for all instances created from the registered component, unless overridden by the instance configuration.

3️⃣ Instance Creation

When creating a component instance, the configuration may reference a base configuration via the config property:

await comp.start({
  // base configuration loaded from datastore
  config: ["ccm.get", { name: "quiz_configs" }, "default"],
  feedback: true,
  theme: "dark"
});

Assume the datastore returns the following base configuration:

{
  questions: ["2+2?", "3+3?"],
  theme: "light"
}

After resolving the base configuration and merging it with the instance configuration:

{
  feedback: true,
  shuffle: true,
  questions: ["2+2?", "3+3?"],
  theme: "dark"
}

Explanation

  • feedback from the component default configuration is overridden during instance configuration
  • shuffle from the component default configuration is overridden during component registration
  • questions comes from the base configuration
  • theme from the base configuration is overridden during instance configuration

This example illustrates how each configuration layer overrides the previous ones:

  • Component defaults provide initial values
  • Component registration updates modify defaults
  • Base configuration extends the configuration
  • Instance configuration overrides base values
  • Inline HTML configuration overrides values defined in the config attribute

🎯 Choosing Entry Point

Entry Point When to Use
ccm.component() When you want to prepare a component with shared configuration for multiple instances
ccm.instance() When you need explicit control over when an instance starts running
ccm.start() When you want to quickly embed and run an app
<ccm-app> When you want to embed and configure an app declaratively in HTML without JavaScript

All entry points are fully compatible and interoperable. They differ only in their level of abstraction and in how much control they give over instantiation and execution.

The following behavior applies uniformly to all entry points: If a component depends on a different ccmjs version than the currently active one, the call is transparently delegated to the appropriate framework instance, and the resulting component or instance is forwarded back to the caller.