-
Notifications
You must be signed in to change notification settings - Fork 0
🧩 Embedding Components
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.
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.
ccm.component( component [, config ] ) → Promise<Component>| 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. |
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><!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
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.
- 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()orcomponent.start()is called. - Component URLs must follow the ccmjs component filename convention (see ccmjs Conventions).
Creates a concrete instance from a registered ccmjs component.
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. |
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.
<!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>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.
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().
ccm.start( component [, config ] [, area ] ) → Promise<Instance>The syntax and parameters are identical to ccm.instance().
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.
// 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();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:
- Component registration (if necessary)
- Configuration preparation and merging
- Runtime dependency resolution
- Instance creation
- Lifecycle execution
-
init()is invoked top-down across the dependency tree -
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.
-
ccm.start()is ideal for standalone apps and simple embeddings. - It offers the least manual control, but the most concise usage.
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.
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):
- Component default configuration (
component.config) - Configuration update passed to
ccm.component() - Instance configuration passed to
ccm.instance()orccm.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.
The component defines a default configuration:
export const component = {
name: "quiz",
config: {
feedback: false,
shuffle: false
}
};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.
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"
}-
feedbackfrom the component default configuration is overridden during instance configuration -
shufflefrom the component default configuration is overridden during component registration -
questionscomes from the base configuration -
themefrom 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
configattribute
| 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.