-
Notifications
You must be signed in to change notification settings - Fork 0
π ccmjs Conventions
The ccmjs framework intentionally keeps its core small and flexible. Instead of introducing additional APIs for many common patterns, ccmjs relies on conventions that define optional meanings for certain properties in configurations and instance API.
These conventions are not strictly required by the framework, but when followed they enable automatic behavior and interoperability between components. Many ccmjs ecosystem components rely on these conventions.
Components follow a strict filename convention to enable automatic parsing, versioning, and efficient component reuse.
The following format applies to component filenames:
ccm.<name>-<major>.<minor>.<patch>[.min].[m]js
The following rules apply to this format:
- The filename must start with
ccm. -
<name>must:- start with a lowercase letter
- contain only lowercase letters, digits, and underscores
- not contain hyphens
-
<major>.<minor>.<patch>follows semantic versioning (three numeric segments) -
.minis optional and indicates a minified build -
.mjsis the standard file extension (.jsis also supported for backward compatibility)
Valid:
ccm.quiz-1.0.0.mjs
ccm.user_profile-2.3.1.min.mjs
ccm.chat-0.1.0.js
Invalid:
ccm.quiz.mjs (missing version)
ccm.my-component-1.0.0.mjs (hyphen in name)
ccm.Quiz-1.0.0.mjs (uppercase letter)
This convention allows ccmjs to:
- Extract component name and version directly from the filename
- Generate unique component identifiers
- Avoid reloading already registered component versions
- Ensure consistent caching behavior
The filename is therefore considered part of the component's public interface.
Certain properties in instance configurations follow established conventions and are interpreted by ccmjs with specific semantics and processing behavior. This allows configuration to remain declarative while still enabling structured behavior.
This property allows overriding the framework version used by the component instance.
Example:
ccm.start("https://ccmjs.github.io/quiz/ccm.quiz.mjs", {
ccm: "https://ccmjs.github.io/framework/ccm.js",
feedback: true
});A configuration may include a config property that references a base configuration.
This base configuration is resolved first and then merged with the local configuration. Properties defined locally override those from the base configuration.
Example:
{
config: ["ccm.get", { name: "quiz_configs" }, "default"], // Referenced base configuration
feedback: false
}Assume the referenced base configuration resolves to:
{
feedback: true,
questions: [
"2+2?",
"3+3?"
]
}After resolving the base configuration and merging it with the local configuration, the final configuration becomes:
{
feedback: false,
questions: [
"2+2?",
"3+3?"
]
}The local configuration overrides properties from the base configuration (feedback),
while all other properties (questions) are inherited unchanged.
The merge is performed before dependency resolution and mapper execution.
A base configuration may itself contain another config property.
In this case, the resolution process continues recursively until a configuration without a config property is reached.
This mechanism enables reusable and composable configurations, including:
- Inheritance
- Shared templates
- External configuration sources
Because base configurations can be loaded via dependencies, they may originate from:
- Local files
- Remote datastores
- Other components
The config property is not part of the final instance configuration, but is only used during configuration resolution.
This property can be used to exclude parts of a configuration from dependency resolution.
Any value inside the ignore object is left unchanged, even if it contains a dependency.
Example:
{
ignore: {
hello: ["ccm.start", "https://ccmjs.github.io/hello/ccm.hello.mjs", { name: "Mika" }]
}
}After instance creation, the value remains unresolved:
this.ignore.hello
// ["ccm.start", "https://ccmjs.github.io/hello/ccm.hello.mjs", { name: "Mika" }]This also applies when the value itself is a dependency (not wrapped in an object).
A configuration may include a mapper that transforms the configuration
before it is applied to the instance.
This enables declarative adaptation of configuration structures, for example when connecting components with different schemas.
The mapper is applied after dependency resolution and before instance initialization. A mapper can be defined as an object that maps source paths to target paths:
ccm.start("ccm.statistics.mjs", {
result: ["ccm.get", { name: "quiz_results" }, "quiz1"],
mapper: {
"result.score": "points",
"result.max": "total"
}
});Resulting configuration:
{
result: {...},
points: 8,
total: 10
}Alternatively, the mapper can be a function:
mapper: config => ({
...config,
percentage: Math.round(
config.result.score / config.result.max * 100
)
})The mapper itself may also be provided via a dependency:
mapper: ["ccm.load", "./mapper.mjs#transform"]A configuration may include an onaction callback that is invoked
whenever an action or state transition occurs during instance execution.
This pattern allows components to remain decoupled while exposing their internal state transitions.
The callback receives an event object with the following properties:
-
instanceβ Reference to the component instance -
typeβ Unique name of the action (e.g."start","next","finish") -
dataβ Optional data associated with the action
Additional properties may be provided depending on the component and the specific action.
Example:
onaction: event => {
switch (event.type) {
case "start":
console.log("Started");
break;
case "next":
console.log("Next step");
break;
case "finish":
console.log("Result:", event.data);
break;
}
}The available action types and additional event data depend on the implementation of the respective component.
Component developers can trigger actions by calling the configured callback and passing an event object.
Example (component implementation):
if (this.onaction) {
this.onaction({
instance: this,
type: "finish",
data: result
});
}The instance reference also provides access to the rendered DOM via event.instance.element.
This allows external logic to extend or adapt the UI without modifying the component itself.
Component developers are encouraged to emit meaningful actions for all relevant state transitions.
The root property controls how the component's DOM is encapsulated and isolated.
Possible values:
| Value | Behavior |
|---|---|
"open" (default)
|
Creates an open Shadow DOM |
"closed" |
Creates a closed Shadow DOM |
false |
Disables Shadow DOM |
If Shadow DOM is disabled, this.host directly contains this.element, without an intermediate Shadow Root.
The user property is a convention used for authentication in remote datastores.
If a component instance provides a user property, it is automatically discovered by child instances via the component hierarchy.
This is used by ccm.store() when working with a remote datastore to attach authentication information to server requests.
The user instance is resolved using:
ccm.helper.findInAncestors(this, "user")This allows authentication to be defined once at a higher level and reused by all nested components.
A user instance is expected to provide the following methods:
-
isLoggedIn()β Returns whether a user is currently authenticated -
login()β Initiates a login process -
logout()β Logs out the current user -
getAppState()β Returns an object containing at least atokenproperty
The token is automatically attached to remote datastore requests:
params.token = this.user.getAppState().tokenIf authentication fails (e.g. HTTP 401/403), the datastore may attempt to re-authenticate automatically.
Note:
- The user property is optional and only relevant when using remote datastores.
- Authentication logic is fully encapsulated in the user component.
- Other components remain independent of authentication details.
Certain properties and methods on component instances follow established conventions and are provided by ccmjs with defined semantics and behavior. This ensures a consistent and predictable interaction model across all components.
Every ccmjs instance exposes the configuration used for its creation via the property this.config.
It contains the fully resolved configuration as a JSON string.
console.log(this.config);Example:
'{"feedback":true,"questions":[...]}'The configuration is stored after dependency resolution and represents the exact state that was used to create the instance.
Since the value is serialized as JSON, JavaScript functions are not preserved. If function behavior should survive serialization (e.g. for sharing or restoring an app), it is recommended to include functions via declarative dependencies instead.
This snapshot can be used for debugging, reproducing application states, exporting configurations, or even sharing complete apps (e.g. via bookmarklets), since both the component and its configuration are known.
Interactive components typically expose a method getAppState().
This method returns the current application state resulting from user interaction. The returned object contains all user-dependent data that represents the logical state of the app.
In addition to application state, components may also maintain UI state.
UI state is typically managed internally and is not necessarily part of getAppState().
Depending on the requirements, UI state may be:
- kept in memory (default)
- included in the app state (
getAppState()) - stored in a datastore (
ccm.store()) - persisted in browser storage (localStorage / sessionStorage)
- encoded in routing state
ccmjs deliberately does not prescribe how UI state should be managed.
The following property names are reserved by the framework and should not be used in component configurations because they are used internally by ccmjs instances.
childrencomponentelementhostinitinstanceparentreadyrootstart
If these properties are present in a configuration, they are automatically removed by ccmjs to prevent conflicts with internal instance properties.
Conventions in ccmjs follow three principles:
-
Optional
Components may ignore them completely. -
Composable
Conventions allow independent components to interoperate. -
Minimal
The core framework does not enforce app architecture.
Instead, conventions provide lightweight coordination mechanisms for component ecosystems.
ccmjs apps are composed by orchestrating independent components through shared datastores, lifecycle callbacks, and runtime DOM composition.