The Compomint template-based component engine is a lightweight JavaScript framework for creating web applications with a focus on component-based architecture. It provides a powerful template-based system that allows you to create, combine, and reuse UI components efficiently.
You can explore live code examples and try out Compomint features in the interactive playground available on the Compomint Website.
Compomint is designed to simplify web application development by providing a lightweight component system. It focuses on high performance and ease of use, allowing developers to create reusable components with HTML, CSS, and JavaScript in a single template.
The framework is particularly suitable for:
You can find example applications demonstrating Compomint in action:
Compomint Core Greeting (ESM) Demo: Code | Demo |
Compomint Core Greeting (UMD) Demo: Code | Demo |
Component Examples: Code | Demo |
Simple Counter: Code | Demo |
Todo List: Code | Demo |
Calculator: Code | Demo |
You can add Compomint to your project using several methods:
compomint.umd.js
(UMD build) or compomint.esm.js
(ESM build) file from the Compomint GitHub releases page.For the UMD build (compomint.umd.js
):
<!-- index.html -->
<script src="path/to/your/compomint.umd.js"></script>
<!-- OR -->
<script src="path/to/your/compomint.umd.min.js"></script>
<!-- Minified version for production -->
<script>
Compomint.compomint.addTmpl("greeting-npm", "<span>##=data.message##</span>");
const greeting = Compomint.tmpl.greeting.npm({
message: "Hello from Compomint via NPM!",
});
document.body.appendChild(greeting.element);
</script>
For the ESM build (compomint.esm.js
), use type="module"
:
import { compomint, tmpl } from "path/to/your/compomint.esm.js";
// OR
import { compomint, tmpl } from "path/to/your/compomint.esm.min.js"; // Minified version for production
compomint.addTmpl("greeting-npm", "<span>##=data.message##</span>");
const greeting = tmpl.greeting.npm({
message: "Hello from Compomint via NPM!",
});
document.body.appendChild(greeting.element);
Replace path/to/your/
with the actual path to the file in your project.
Include the library directly from a Content Delivery Network (CDN). This is the quickest way to get started.
For the UMD build (compomint.js
):
<script src="https://cdn.jsdelivr.net/gh/kurukona/compomint@latest/dist/compomint.umd.js"></script>
<script src="https://cdn.jsdelivr.net/gh/kurukona/compomint@latest/dist/compomint.umd.min.js"></script>
<!-- Minified version for production -->
For the ESM build (compomint.esm.js
), use type="module"
:
import {
compomint,
tmpl,
} from "https://cdn.jsdelivr.net/gh/kurukona/compomint@latest/dist/compomint.esm.js";
import {
compomint,
tmpl,
} from "https://cdn.jsdelivr.net/gh/kurukona/compomint@latest/dist/compomint.esm.min.js"; // Minified version for production
Using importmap:
<!-- index.html -->
<script type="importmap">
{
"imports": {
"compomint": "https://unpkg.com/compomint@1.0.0/dist/compomint.esm.min.js"
}
}
</script>
// main.js
import { compomint, tmpl } from "compomint";
const greeting = tmpl.greeting.npm({
message: "Hello from Compomint via NPM!",
});
document.body.appendChild(greeting.element);
Using @latest
will load the most recent version. You can replace @latest
with a specific version number (e.g., @1.0.0
) for better stability in production.
Compomint is available on NPM, making it easy to integrate into your projects using a module bundler or Node.js.
npm install compomint
or
yarn add compomint
Usage: Once installed, you can import Compomint into your JavaScript files.
If you are using ESM (ECMAScript Modules) (common in modern frontend projects with bundlers like Webpack, Rollup, or Parcel):
import { compomint, tmpl } from "compomint";
// Start using Compomint:
compomint.addTmpl("greeting-npm", "<span>##=data.message##</span>");
const greeting = tmpl.greeting.npm({
message: "Hello from Compomint via NPM!",
});
document.body.appendChild(greeting.element);
const { compomint, tmpl } = require('compomint');
// Start using Compomint:
compomint.addTmpl('greeting-npm', '<span>##=data.message##</span>');
onst greeting = tmpl.greeting.npm({ message: 'Hello from Compomint via NPM!' });
document.body.appendChild(greeting.element);
The NPM package includes builds suitable for various environments, and your build tool will typically select the appropriate one.
UMD vs. ESM Builds:
import
/export
). It’s ideal for modern JavaScript development workflows using bundlers like Webpack, Rollup, or Parcel.There are several ways to define templates in Compomint:
compomint.addTmpl("do-Simple-Label", "<span>##=data.label##</span>");
Define templates in your HTML or JavaScript using the <template>
tag:
const templateString = `
<template id="do-Simple-Label">
<span class="do-Simple-Label">
##=data.label##
</span>
</template>
<template id="do-Simple-Button">
<style id="style-do-Simple-Button">
.do-Simple-Button {
background-color: #4CAF50;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
cursor: pointer;
border-radius: 4px;
}
.do-Simple-Button:hover {
background-color: #45a049;
}
</style>
<button class="do-Simple-Button"
style="color: ##=data.color || 'white'##"
data-co-event="##:data.onClick##">
##=data.label##
</button>
</template>
`;
compomint.addTmpls(templateString);
When using the template tag approach:
<!DOCTYPE html>
<html>
<body>
<template id="do-Simple-Label">
<span class="do-Simple-Label"> ##=data.label## </span>
</template>
<template id="do-Simple-Button">
<style id="style-do-Simple-Button">
.do-Simple-Button {
background-color: #4caf50;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
cursor: pointer;
border-radius: 4px;
}
</style>
<button
class="do-Simple-Button"
style="color: ##=data.color || 'white'##"
data-co-event="##:data.onClick##"
>
##=data.label##
</button>
</template>
</body>
</html>
Then load all templates at once:
// Second parameter (true) removes template tags after loading
compomint.addTmpls(document.body.innerHTML, true);
// Load templates from external file with callback
compomint.addTmplByUrl("templates.html", function () {
console.log("Templates loaded successfully!");
// Initialize your application here
const mainScreen = tmpl.do.MainScreen({});
document.body.appendChild(mainScreen.element);
});
// Promise-based loading (NEW: Version 1.0.3+)
compomint
.addTmplByUrl("templates.html")
.then(() => {
console.log("Templates loaded successfully!");
const mainScreen = tmpl.do.MainScreen({});
document.body.appendChild(mainScreen.element);
})
.catch((error) => {
console.error("Failed to load templates:", error);
});
// Async/await support
async function loadAndInitialize() {
try {
await compomint.addTmplByUrl("templates.html");
console.log("Templates loaded successfully!");
const mainScreen = tmpl.do.MainScreen({});
document.body.appendChild(mainScreen.element);
} catch (error) {
console.error("Failed to load templates:", error);
}
}
// With options for loading
compomint.addTmplByUrl(
{
url: "templates.html",
option: {
loadScript: true, // Load and execute script tags
loadStyle: true, // Load style tags
loadLink: true, // Load link tags
},
},
callback // Optional: if provided, uses callback; otherwise returns Promise
);
// Load multiple template files
compomint
.addTmplByUrl([
"templates/header.html",
"templates/main.html",
"templates/footer.html",
])
.then(() => {
console.log("All templates loaded successfully!");
// All templates are now available
const header = tmpl.ui.Header({ title: "My App" });
const main = tmpl.ui.Main({ content: "Welcome!" });
const footer = tmpl.ui.Footer({ year: 2024 });
document.body.appendChild(header.element);
document.body.appendChild(main.element);
document.body.appendChild(footer.element);
});
Once templates are defined, you can create components and add them to the DOM:
tmpl
NamespaceWhen you add templates using compomint.addTmpl
or compomint.addTmpls
, Compomint automatically creates and updates a convenient namespace object called tmpl
. This object provides easy access to component creation functions based on their template IDs.
If a template ID contains hyphens (-
), they are converted into a nested object structure. For example, a template with the ID do-Simple-Label
becomes accessible as tmpl.do.SimpleLabel
. This can be a more structured and often more readable way to instantiate components compared to using compomint.tmpl('template-id')
.
// Create a simple label component using the tmpl namespace
// The 'do-Simple-Label' ID is converted to tmpl.do.SimpleLabel
const label = tmpl.do.SimpleLabel({ label: "Hello World" });
// Add it to the DOM
document.body.appendChild(label.element);
Result:
<span class="do-Simple-Label">Hello World</span>
compomint.tmpl()
functionInstead of using the tmpl
namespace, you can also create components by directly calling the compomint.tmpl()
function and passing the template ID as a string. This approach is useful in the following scenarios:
tmpl
.// Create a button with click handler
const button = compomint.tmpl("do-Simple-Button")({
label: "Click Me",
color: "white",
onClick: (
event,
{ data, customData, element, componentElement, component, compomint }
) => {
alert("Button clicked!");
},
});
// Add it to the DOM
document.body.appendChild(button.element);
Result:
<button class="do-Simple-Button" style="color: white">Click Me</button>
<!-- With click event handler attached -->
The data you pass to the component (like {label: 'Click Me', color: 'white'}
) is available inside the template as the data
object.
One of Compomint’s strengths is component composition. You can easily combine components to build complex UIs:
compomint.addTmpls(`
<template id="do-Counter">
<style id="style-do-Counter">
.do-Counter {
border: 1px solid #ddd;
padding: 16px;
margin: 8px;
border-radius: 4px;
width: 200px;
text-align: center;
}
.do-Counter .count {
font-size: 24px;
margin: 8px 0;
}
</style>
##
// Initialize state
let count = data.initialCount || 0;
// Create child components
const displayLabel = tmpl.do.SimpleLabel({
label: 'Count: ' + count
});
const incrementButton = tmpl.do.SimpleButton({
label: 'Increment',
color: 'white',
onClick: (event, {data}) => {
count++;
displayLabel.refresh({label: 'Count: ' + count});
}
});
const decrementButton = tmpl.do.SimpleButton({
label: 'Decrement',
color: 'white',
onClick: (event, {data}) => {
count--;
displayLabel.refresh({label: 'Count: ' + count});
}
});
##
<div class="do-Counter">
<h3>##=data.title || 'Counter'##</h3>
<div class="count">##%displayLabel##</div>
<div class="buttons">
##%decrementButton##
##%incrementButton##
</div>
</div>
</template>
`);
// Create a counter component with initial count of 5
const counter = tmpl.do.Counter({
title: "My Counter",
initialCount: 5,
});
document.body.appendChild(counter.element);
Result:
<div class="do-Counter">
<h3>My Counter</h3>
<div class="count">
<span class="do-Simple-Label">Count: 5</span>
</div>
<div class="buttons">
<button class="do-Simple-Button" style="color: white">Decrement</button>
<button class="do-Simple-Button" style="color: white">Increment</button>
</div>
</div>
Compomint uses a special syntax for template expressions and data binding.
Templates can include styles, which are automatically extracted and added to the document head:
compomint.addTmpls(`
<template id="ui-Card">
<style id="style-ui-Card">
.ui-Card {
border: 1px solid #ccc;
border-radius: 4px;
padding: 16px;
margin: 8px;
}
.ui-Card h3 {
color: #333;
margin-top: 0;
}
</style>
<div class="ui-Card">
<h3>##=data.title##</h3>
<div>##=data.content##</div>
</div>
</template>
`);
// Create multiple card instances
document.body.appendChild(
tmpl.ui.Card({
title: "Card 1",
content: "This is the first card",
}).element
);
document.body.appendChild(
tmpl.ui.Card({
title: "Card 2",
content: "This is the second card",
}).element
);
When included in a template, style elements with IDs are automatically extracted and added to the document head, ensuring that styles are defined only once regardless of how many component instances you create.
These variables are available within templates:
data
Contains the data passed when creating the component.
// In template: ##=data.userName##
tmpl.user.Profile({ userName: "John Doe" });
status
Object for storing component state information that persists across refreshes.
<!-- In template -->
## status.count = status.count || 0; ##
<button
data-co-event="##:{
click: function(event, {data, component}) {
status.count++;
component.refresh();
}
}##"
>
Clicked ##=status.count## times
</button>
component
Reference to the template scope, providing access to:
component.element
- The rendered DOM elementcomponent.data
- The data used to render the templatecomponent.status
- The status object for template statecomponent.render(data)
- Re-renders with new datacomponent.refresh(data)
- Updates with partial data<!-- In template -->
<button
data-co-event="##:{
click: function(event, {data, component}) {
component.refresh({message: 'Updated!'});
}
}##"
>
Update
</button>
i18n
Internationalization object for localized text.
// In template: ##=i18n.greeting##
// After setting up translations:
compomint.addI18n("user-profile.greeting", {
en: "Welcome",
fr: "Bienvenue",
es: "Bienvenido",
});
tmpl
Reference to the template registry, providing access to: // Create a component instance like
tmpl.ui.Button({label: 'Click Me'});
compomint
Reference to the global compomint object, providing access to:
compomint.tools.genElement('div', {class: 'message'}, 'Hello');
- Creates a DOM elementcompomint.tools.props({class: 'button', disabled: true});
- Creates HTML attribute stringcompomint.tools.genId('my-component');
- Generates a unique IDcompomint.tools.escapeHtml.escape('Hello World');
- Escapes HTML characters<textarea>
##=compomint.tools.escapeHtml.escape(data.userInput)##
</textarea>
Compomint uses special delimiters for different types of expressions:
##= ##
- Data InterpolationOutputs string content. HTML tags will be interpreted as HTML.
<span>##=data.userName##</span>
<!-- With a default value -->
<span>##=data.userName || 'Guest'##</span>
<!-- conditional formatting -->
<span class="##=data.isActive ? 'active' : 'inactive'##">
##=data.status##
</span>
If the property is a function, it will be called automatically:
<!-- In template -->
<span>##=data.getFormattedName##</span>
<!-- When creating the component -->
tmpl.user.NameTag({ getFormattedName: function() { return 'Dr. John Smith, PhD';
} });
##- ##
- HTML EscapingOutputs the escaped value, preventing HTML injection:
<!-- Unsafe content will be escaped -->
<div>##-data.userComment##</div>
<!--
If data.userComment = '<script>alert("XSS")</script>'
Output: <script>alert("XSS")</script>
-->
##% ##
- Element InsertionUsed to include other components, HTML elements, or strings:
<!-- Insert a single component -->
<div class="container">##%childComponent##</div>
<!-- Insert an array of components -->
<div class="container">
##%data.items.map(item => tmpl.ui.ListItem({text: item}))##
</div>
<!-- With optional non-blocking (async) insertion -->
<div class="heavy-content">##%heavyComponent::true##</div>
The second parameter after ::
indicates that insertion should be non-blocking.
##! ##
- Pre-EvaluationCode that runs when the template is first loaded (not on each render):
<template id="user-profile">
##! // This code runs once when the template is loaded compomint.addI18ns({
'user-profile': { 'greeting': { 'en': 'Welcome', 'fr': 'Bienvenue', 'es':
'Bienvenido' } } }); ##
<div>##=i18n.greeting##, ##=data.name##!</div>
</template>
## ##
- JavaScript Code BlockAllows you to write JavaScript code that runs during rendering:
<template id="product-list">
## // Process data before rendering const sortedProducts =
data.products.sort((a, b) => a.price - b.price ); // Create a formatted price
function function formatPrice(price) { return '$' + price.toFixed(2); } ##
<div class="product-list">
<h2>##=data.title##</h2>
<ul>
##sortedProducts.forEach(product => {##
<li>
<strong>##=product.name##</strong>
<span>##=formatPrice(product.price)##</span>
</li>
##})##
</ul>
</div>
</template>
### ##
- Lazy EvaluationCode that runs after the template is rendered:
<template id="chart-component">
<div class="chart-container" id="chart-##=data.id##"></div>
### // This code runs after the element is in the DOM const chartElement =
document.getElementById('chart-' + data.id); // Initialize a chart library new
Chart(chartElement, { type: 'bar', data: data.chartData, options:
data.chartOptions }); ##
</template>
##* ##
- Comment Areas##* This is a comment area that won't be rendered. Use it for documenting your
template code. ##
#\# #\#
- Escape SyntaxUsed to display template syntax literally without processing it:
<!-- Show template syntax as text -->
<div>This shows template syntax: #\#=data.example#\#</div>
<!-- Output: This shows template syntax: ##=data.example## -->
<!-- Compare escaped vs processed syntax -->
<div>Raw syntax: #\#=data.message#\# vs processed: ##=data.message##</div>
<!-- Output: Raw syntax: ##=data.message## vs processed: Hello -->
<!-- Escape different types of syntax -->
<div>Escaped escape: #\#-data.html#\# vs real escape: ##-data.html##</div>
<div>Escaped code: #\#let x = 5;#\# shows as literal text</div>
<div>Escaped element: #\#%data.content#\# won't insert elements</div>
<div>Escaped comment: #\#* This is a comment *#\# becomes visible</div>
This is useful for:
Compomint provides special HTML attributes for handling events, references, and dynamic content:
data-co-event="##:handler##"
- Event HandlingAttaches event handlers to HTML elements:
<!-- Simple click handler -->
<button data-co-event="##:handleClick##">Click Me</button>
<!-- Multiple event types -->
<input
data-co-event="##:{
focus: handleFocus,
blur: handleBlur,
input: handleInput
}##"
/>
<!-- With inline function -->
<button
data-co-event="##:{
click: function(event, {data, customData, element, componentElement, component, compomint}) {
console.log('Clicked:', componentElement.textContent);
alert('Hello, ' + data.userName + '!');
}
}##"
>
Greet
</button>
<!-- With custom data parameter -->
<button data-co-event="##:handleItemClick::data.item##">
View ##=data.item.name##
</button>
Event handlers receive these parameters:
event
- The DOM event objecteventData
- An object containing:
data
- The component’s data objectcustomData
- The optional custom data passed after ::
element
- The element that triggered the eventcomponentElement
- The main component elementcomponent
- The element that triggered the eventcompomint
- Reference to the global compomint objectdata-co-named-element="##:variable##"
- Element ReferencesCreates a named reference to an element in the template scope:
<input type="text" data-co-named-element="##:'nameInput'##" />
<!-- Later in the template or another handler -->
<button
data-co-event="##:{
click: function(event, {component}) {
console.log('Input value:', component.nameInput.value);
}
}##"
>
Get Value
</button>
data-co-element-ref="##:variable##"
- Element VariablesBinds a DOM element to a variable in the template code:
<template id="form-component">
## let formData = {}; function submitForm() { formData.name = nameInput.value;
formData.email = emailInput.value; console.log('Submitted:', formData); } ##
<form>
<input
type="text"
placeholder="Name"
data-co-element-ref="##:nameInput##"
/>
<input
type="email"
placeholder="Email"
data-co-element-ref="##:emailInput##"
/>
<button type="button" data-co-event="##:{click: submitForm}##">
Submit
</button>
</form>
</template>
data-co-load="##:handler::data##"
- Element LoadingExecutes a function when an element is loaded into the DOM:
<div
class="chart-container"
data-co-load="##:initializeChart::data.chartData##"
></div>
<!--
Where initializeChart is defined as:
function initializeChart(element, {data, customData, element, component, compomint}) {
// Initialize a chart in the element using the data
new Chart(element, {
type: 'line',
data: customData
});
}
-->
Load handlers receive these parameters:
element
- The element itselfloadData
- An object containing:
data
- The component’s data objectcustomData
- The optional custom data passed after ::
element
- The element itselfcomponent
- The template scope objectcompomint
- Reference to the global compomint objectCompomint includes built-in support for multiple languages:
// Add translations for a single key
compomint.addI18n("greeting", {
en: "Hello",
fr: "Bonjour",
es: "Hola",
de: "Hallo",
});
// Add nested translations
compomint.addI18n("messages.welcome", {
en: "Welcome to our site",
fr: "Bienvenue sur notre site",
es: "Bienvenido a nuestro sitio",
});
// Add multiple translations at once
compomint.addI18ns({
greeting: {
en: "Hello",
fr: "Bonjour",
},
farewell: {
en: "Goodbye",
fr: "Au revoir",
},
buttons: {
submit: {
en: "Submit",
fr: "Soumettre",
},
cancel: {
en: "Cancel",
fr: "Annuler",
},
},
});
Use in templates
<span>##=compomint.i18n.greeting('Hello!')##</span>
<!-- when language is 'ja' then output is 'Hello!' (default text) -->
<p>##=compomint.i18n.messages.welcome('Welcome to our site')##</p>
<button>##=compomint.i18n.buttons.submit('Submit')##</button>
The current language is determined by document.documentElement.lang
. The parameter passed to the i18n function serves as the default text if no translation is found.
Use in template i18n object
<template id="my-component">
##! // Define translations in template preloading compomint.addI18ns({
'my-component': { 'greeting': { 'en': 'Hello', 'fr': 'Bonjour' }, 'subtitle':
{ 'en': 'Welcome to our app', 'fr': 'Bienvenue dans notre application' } } });
##
<div>
<h1>##=i18n.greeting## ##=data.name##!</h1>
<p>##=i18n.subtitle##</p>
</div>
</template>
You can remap template IDs, which is useful for versioning or aliasing:
// Remap template IDs
compomint.remapTmpl({
"old-button": "ui-Button-v2",
"legacy-form": "ui-Form-v3",
});
// Now, this:
const button = compomint.tmpl("old-button")(data);
// Is equivalent to:
const button = compomint.tmpl("ui-Button-v2")(data);
You can customize how templates are processed:
// Define custom template settings
const customSettings = {
dataKeyName: "model", // Use 'model' instead of 'data' in templates
statusKeyName: "state", // Use 'state' instead of 'status' in templates
// Override other settings as needed
};
// Create template with custom settings
compomint.addTmpl("custom-template", "<div>##=model.label##</div>", {
keys: customSettings,
});
// Use the template
const component = compomint.tmpl("custom-template")({ label: "Hello" });
Custom Template Engine
You can define a custom template engine with your own expression syntax and processing rules:
// Define a custom template engine
const customTemplateEngine = {
rules: {
// Custom interpolation rule using double braces
customInterpolate: {
pattern: /\{\{(.+?)\}\}/g,
exec: function (match) {
return `<span class="custom-output">${match}</span>`;
},
},
// Custom code execution rule
customCode: {
pattern: /\{%(.+?)%\}/g,
exec: function (code) {
return `'; ${code}; __p+='`;
},
},
},
keys: {
dataKeyName: "model", // Use 'model' instead of 'data'
statusKeyName: "state", // Use 'state' instead of 'status'
componentKeyName: "scope", // Use 'scope' instead of 'component'
i18nKeyName: "translate", // Use 'translate' instead of 'i18n'
},
};
// Use custom template engine when creating templates
compomint.addTmpl("custom-engine-template",
"<div>{{model.userName}} - {%(.+?)%\}/g,
exec: function (code) {
return `'; ${code}; __p+='`;
},
},
},
keys: {
dataKeyName: "model", // Use 'model' instead of 'data'
statusKeyName: "state", // Use 'state' instead of 'status'
componentKeyName: "scope", // Use 'scope' instead of 'component'
i18nKeyName: "translate", // Use 'translate' instead of 'i18n'
},
};
// Use custom template engine when creating templates
compomint.addTmpl("custom-engine-template",
"<div>{{model.userName}} - {% state.clickCount = state.clickCount || 0; %}</div>",
{ templateEngine: customTemplateEngine }
);
// Use custom template engine when loading templates from URL
compomint.addTmplByUrl("templates.html", {
templateEngine: customTemplateEngine
}, function() {
console.log("Templates with custom engine loaded!");
});
// Promise-based loading with custom template engine
compomint.addTmplByUrl({
url: "templates.html",
option: {
templateEngine: customTemplateEngine,
loadScript: true,
loadStyle: true,
}
}).then(() => {
console.log("Custom engine templates loaded successfully!");
});
The custom template engine allows you to:
Custom Template Expression Syntax
// Use custom delimiters for template expressions
compomint.addTmpl("custom-syntax", "<div></div>", {
interpolate: {
pattern: //g,
exec: function (interpolate) {
return `';(() => {let interpolate=${interpolate};\n__p+=((__t=(typeof (interpolate)=='function' ? (interpolate)() : (interpolate)))==null?'':__t);})()\n__p+='`;
},
},
});
Templates in Compomint have a comprehensive lifecycle that you can manage:
// Render a template with data
const component = compomint.tmpl("my-template")({
name: "John Doe",
email: "john@example.com",
});
// Append by direct reference
document.body.appendChild(component.element);
// Append using component method
component.appendTo(document.getElementById("container"));
// Render directly into a container
compomint.tmpl("my-template")(data, document.getElementById("container"));
// With a callback after insertion
compomint.tmpl("my-template")(
data,
document.getElementById("container"),
function (scope) {
console.log("Template rendered and inserted!");
// Initialize additional components
}
);
// Complete re-render with new data
component.render({
name: "Jane Doe",
email: "jane@example.com",
});
// Partial update (only specified properties)
component.refresh({
name: "Jane Doe",
// email remains unchanged
});
// Remove completely from DOM
component.remove();
// Remove but leave a placeholder element
// Useful for later re-insertion
component.remove(true);
// Replace with another template
const newScope = compomint.tmpl("other-template")(otherData);
component.replace(newScope);
// Or replace directly
component.replace(compomint.tmpl("other-template")(otherData));
You can define hooks for different lifecycle events:
// Define lifecycle hooks
component.beforeAppendTo = function () {
console.log("About to insert into DOM");
// Pre-insertion setup
};
component.afterAppendTo = function () {
console.log("Inserted into DOM");
// Post-insertion initialization
};
component.beforeRemove = function () {
console.log("About to remove from DOM");
// Cleanup resources
};
component.afterRemove = function () {
console.log("Removed from DOM");
// Final cleanup
};
component.beforeRefresh = function () {
console.log("About to update data");
// Prepare for update
};
component.afterRefresh = function () {
console.log("Updated with new data");
// Post-update processing
};
// Release template resources
component.release();
compomint.addTmpl(id, content, settings)
- Creates a new template
id
- Template identifiercontent
- Template string or DOM elementsettings
- Optional template settingscompomint.addTmpls(source, removeInnerTemplate, settings)
- Loads multiple templates from source
source
- HTML string or DOM element containing templatesremoveInnerTemplate
- Whether to remove templates after loadingsettings
- Optional template settingscompomint.addTmplByUrl(url, options, callback)
- Loads templates from URL
url
- URL string, array of URLs, or object with URL and optionsoptions
- Loading options (loadScript, loadStyle, loadLink) or callback functioncallback
- Optional function to call after loadingcompomint.tmpl(id)(data, container, callback, component)
- Renders a template with data
id
- Template identifierdata
- Data object to render withcontainer
- Optional element to append tocallback
- Optional function to call after renderingcomponent
- Optional existing scope to reusecomponent.appendTo(element)
- Appends template to elementcomponent.render(data)
- Re-renders with new datacomponent.refresh(data)
- Updates with partial datacomponent.remove(spacer)
- Removes from DOMcomponent.replace(newScope)
- Replaces with another templatecomponent.release()
- Releases template resourcescomponent.element
- Reference to the rendered DOM elementcomponent.data
- The data used to render the templatecomponent.status
- Status object for template statecomponent._id
- Unique ID for the template instancecomponent.tmplId
- The template IDcomponent.wrapperElement
- The wrapper element (if provided during rendering)compomint.addI18n(key, translations)
- Adds translations for a key
key
- Translation key (can be nested with dots)translations
- Object mapping language codes to translationscompomint.addI18ns(translationsObject)
- Adds multiple translations
translationsObject
- Object mapping keys to translation objectscompomint.i18n[key](defaultText)
- Gets translation for current language
key
- Translation keydefaultText
- Default text if translation not foundcompomint.tools.genElement(tagName, attrs = {}, ...children)
- Creates a DOM element
tagName
- Type of element to createattrs
- Object of attributes to set...children
- Child elements to appendcompomint.tools.props(...propsObjects)
- Creates HTML attribute string
propsObjects
- Objects of properties to convert to attributescompomint.tools.genId(prefix)
- Generates a unique ID
prefix
- ID prefix (usually template ID)compomint.tools.escapeHtml.escape(string)
- Escapes HTML characters
string
- String to escapecompomint.tools.escapeHtml.unescape(string)
- Unescapes HTML characters
string
- String to unescapecompomint.configs.printExecTime
- Enable template rendering time loggingcompomint.configs.debug
- Enable debug modecompomint.configs.throwError
- Throw errors instead of silently failingCompomint comes with several built-in utility templates to help you get started:
Directly uses compomint.tools.genElement
to create a DOM element.
Expects an array [tagName, attributes]
as data.
Note: Use className
for the class attribute within the attributes
object.
// Create a div with id and class
const div = compomint.tmpl('co-Ele')(['div', {
id: 'myDiv',
class: 'container', // className also works here
style: 'background-color: #f0f0f0; padding: 10px;'
}]);
document.body.appendChild(div.element);
// Create an input element
const input = compomint.tmpl('co-Ele')(['input', {
type: 'text',
name: 'username',
placeholder: 'Enter username',
required: true,
class: 'form-control' // className also works here
className: 'form-control'
}]);
document.body.appendChild(input.element);
A convenient template for creating generic HTML elements with common attributes and flexible content.
Expects an object data
with the following optional properties:
tag
: The HTML tag name (e.g., ‘div’, ‘span’, ‘p’). Defaults to 'div'
if not provided.id
: The element’s ID attribute. If set to true
, it automatically uses the component’s unique instance ID (component._id
).props
: An object containing attributes to be set on the element (e.g., { class: 'my-class', style: 'color: red;', 'data-value': '123' }
). Uses the data-co-props
rule internally.event
: An object defining event handlers, similar to the data-co-event
attribute syntax.content
: The content for the element. If it’s a string, it will be directly inserted. If it’s a DOM Node, DocumentFragment, or another Compomint component scope, it will be appended.// Create a paragraph element with various attributes and string content
const paragraph = compomint.tmpl("co-Element")({
tag: "p",
id: "myParagraph",
props: {
class: "info-text important",
style: "color: blue; font-weight: bold;",
"data-custom": "value",
},
content: "This is an important paragraph.",
event: { click: () => alert("Paragraph clicked!") },
});
document.body.appendChild(paragraph.element);
// Create a div containing another element (using co-Ele for the inner part)
const innerSpan = compomint.tmpl("co-Ele")([
"span",
{ className: "highlight" },
"Highlighted Text",
]);
const wrapperDiv = compomint.tmpl("co-Element")({
// tag defaults to 'div'
props: { class: "wrapper" },
content: innerSpan.element, // Insert the span element
});
document.body.appendChild(wrapperDiv.element);
This creates a comprehensive component showcase where you can see the template source code and rendered components side by side.
Understanding how Compomint works internally can help you use it more effectively:
Bridge.templateSettings
┌───────────────┐ ┌─────────────────┐ ┌───────────────────┐
│ Template Text │ -> │ Parse & Extract │ -> │ Generate Function │
└───────────────┘ └─────────────────┘ └───────────────────┘
│
┌────────────────┐ ┌───────────────┐ ┌────▼─────────────┐
│ Template Scope │ <- │ DOM Creation │ <- │ Execute Function │
└────────────────┘ └───────────────┘ └──────────────────┘
│ ▲
│ │
└────────────────────┘
Post-processing
Compomint uses a “lazy evaluation” system to handle dynamic elements and events:
This approach ensures proper timing for DOM manipulation and allows for deferred execution of expensive operations.
The template scope (tmplScope
) is an object that:
┌─────────────────────────────────────────┐
│ Template Scope │
├─────────────────┬───────────────────────┤
│ Properties │ Methods │
├─────────────────┼───────────────────────┤
│ .element │ .render(data) │
│ .data │ .refresh(data) │
│ .status │ .remove(spacer) │
│ ._id │ .replace(newScope) │
│ .tmplId │ .appendTo(element) │
│ [named elements]│ .release() │
└─────────────────┴───────────────────────┘
Minimize Re-renders: Use refresh()
for minor updates instead of render()
// Good: Update only what changed
tmplScope.refresh({ counter: newCount });
// Avoid: Complete re-render for small changes
tmplScope.render({ ...data, counter: newCount });
Batch DOM Operations: Use document fragments when inserting multiple elements
// Create a fragment first
const fragment = document.createDocumentFragment();
items.forEach((item) => {
const element = tmpl.ui.ListItem({ text: item }).element;
fragment.appendChild(element);
});
// Then add to DOM once
container.appendChild(fragment);
Cache Element References: Store references to elements you’ll need to access frequently
<input type="text" data-co-named-element="##:'nameInput'##">
// Later: Use the cached reference
tmplScope.nameInput.focus();
Use Lazy Loading: Load and initialize heavy components asynchronously
<div id="chart-container">##%heavyChartComponent::true##</div>
Template Granularity: Create smaller, reusable templates instead of large monolithic ones
// Better: Compose from smaller components
compomint.tmpl("page-layout")({
header: tmpl.ui.Header({ title: "Dashboard" }),
sidebar: tmpl.ui.Sidebar({ items: menuItems }),
content: tmpl.ui.ContentPanel({
title: "Welcome",
body: tmpl.ui.WelcomeMessage({ userName: "John" }),
}),
});
Template Namespacing: Use consistent ID patterns
// Domain-specific grouping
"ui-Button"; // UI components
"form-TextField"; // Form components
"chart-BarChart"; // Chart components
"page-Dashboard"; // Page templates
Data Preparation: Format data before passing to templates
// Prepare data before rendering
const userData = {
fullName: user.firstName + " " + user.lastName,
formattedDate: new Date(user.joinDate).toLocaleDateString(),
isAdmin: user.role === "admin",
};
const userCard = compomint.tmpl("user-card")(userData);
Event Delegation: Use event delegation for dynamic content
<ul
class="item-list"
data-co-event="##:{
click: function(event) {
// Check if a list item was clicked
if (event.target.closest('li')) {
const listItem = event.target.closest('li');
console.log('Item clicked:', listItem.dataset.id);
}
}
}##"
>
## data.items.forEach(item => { ##
<li data-id="##=item.id##">##=item.name##</li>
## }); ##
</ul>
Separation of Concerns: Keep templates focused on presentation
// Prepare data and logic outside the template
function createUserCard(userData) {
// Business logic
const isVerified = checkUserVerification(userData);
const permissions = getUserPermissions(userData);
// Pass prepared data to template
return tmpl.user.Card({
user: userData,
isVerified: isVerified,
permissions: permissions,
onEdit: function () {
openUserEditForm(userData.id);
},
});
}
Documentation: Comment templates with their expected data structure ```javascript /*
...
);
```Validate Input Data: Check data before rendering
function renderUserProfile(userData) {
// Validate required fields
if (!userData || !userData.id || !userData.name) {
console.error("Invalid user data:", userData);
return tmpl.error.InvalidData({
message: "Unable to display user profile due to missing data",
});
}
return tmpl.user.Profile(userData);
}
Provide Fallbacks: Use default values for missing data
<div class="user-card">
<img src="##=data.avatar || 'images/default-avatar.png'##" alt="User" />
<h3>##=data.name || 'Unknown User'##</h3>
<p>##=data.bio || 'No bio available'##</p>
</div>
Error Recovery: Use try/catch for data processing
<div class="data-chart">
## try { const chartData = processChartData(data.rawData); const
chartOptions = generateChartOptions(data.chartType); } catch (error) {
console.error('Error processing chart data:', error); chartData = null;
chartOptions = null; } ## ##if (chartData && chartOptions) {##
<canvas
data-co-load="##:initChart::({data: chartData, options: chartOptions})##"
></canvas>
##} else {##
<div class="error-message">
Unable to display chart due to data processing error
</div>
##}##
</div>
Debug Mode: Enable debug mode during development
// Enable in development
compomint.configs.debug = true;
compomint.configs.printExecTime = true;
compomint.configs.throwError = true;
// Disable in production
compomint.configs.debug = false;
compomint.configs.printExecTime = false;
compomint.configs.throwError = false;
// First, define the templates
compomint.addTmpls(`
<template id="todo-App">
<style id="style-todo-App">
.todo-App {
font-family: 'Arial', sans-serif;
max-width: 500px;
margin: 0 auto;
padding: 20px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
border-radius: 5px;
}
.todo-App .todo-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.todo-App .todo-form {
display: flex;
margin-bottom: 20px;
}
.todo-App .todo-form input {
flex-grow: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
}
.todo-App .todo-form button {
padding: 8px 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.todo-App .todo-list {
list-style-type: none;
padding: 0;
}
.todo-App .todo-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-App .todo-item.completed .todo-text {
text-decoration: line-through;
color: #888;
}
.todo-App .todo-text {
flex-grow: 1;
margin-left: 10px;
}
.todo-App .todo-actions button {
margin-left: 5px;
background: none;
border: none;
color: #777;
cursor: pointer;
}
.todo-App .todo-actions button:hover {
color: #333;
}
.todo-App .todo-stats {
display: flex;
justify-content: space-between;
margin-top: 20px;
color: #777;
font-size: 14px;
}
.todo-App .todo-empty {
text-align: center;
color: #777;
padding: 20px;
}
</style>
##
// Initialize state if not already present
status.todos = status.todos || data.initialTodos || [];
// Add a new todo item
function addTodo() {
const text = newTodoInput.value.trim();
if (text) {
status.todos.push({
id: Date.now(),
text: text,
completed: false
});
newTodoInput.value = '';
component.refresh();
}
}
// Toggle todo completion status
function toggleTodo(todoId) {
const todo = status.todos.find(t => t.id === todoId);
if (todo) {
todo.completed = !todo.completed;
component.refresh();
}
}
// Delete a todo item
function deleteTodo(todoId) {
status.todos = status.todos.filter(t => t.id !== todoId);
component.refresh();
}
// Clear completed todos
function clearCompleted() {
status.todos = status.todos.filter(t => !t.completed);
component.refresh();
}
// Filter todos based on current filter
status.filter = status.filter || 'all';
function setFilter(filter) {
status.filter = filter;
component.refresh();
}
function getFilteredTodos() {
switch(status.filter) {
case 'active':
return status.todos.filter(t => !t.completed);
case 'completed':
return status.todos.filter(t => t.completed);
default:
return status.todos;
}
}
const filteredTodos = getFilteredTodos();
const activeCount = status.todos.filter(t => !t.completed).length;
const completedCount = status.todos.length - activeCount;
##
<div class="todo-App">
<div class="todo-header">
<h1>##=data.title || 'Todo List'##</h1>
</div>
<form class="todo-form">
<input
type="text"
placeholder="What needs to be done?"
data-co-element-ref="##:newTodoInput##"
>
<button type="submit" data-co-event="##:(event) => {
event.preventDefault();
addTodo();
}##">Add</button>
</form>
<div class="todo-filters">
<button
data-co-event="##:{click: () => setFilter('all')}##"
class="##=status.filter === 'all' ? 'active' : ''##">
All
</button>
<button
data-co-event="##:{click: () => setFilter('active')}##"
class="##=status.filter === 'active' ? 'active' : ''##">
Active
</button>
<button
data-co-event="##:{click: () => setFilter('completed')}##"
class="##=status.filter === 'completed' ? 'active' : ''##">
Completed
</button>
</div>
##if (filteredTodos.length === 0) {##
<div class="todo-empty">
##if (status.todos.length === 0) {##
No todos yet. Add one above!
##} else {##
No todos match the current filter.
##}##
</div>
##} else {##
<ul class="todo-list">
##filteredTodos.forEach(todo => {##
<li class="todo-item ##=todo.completed ? 'completed' : ''##">
<input
type="checkbox"
##=todo.completed ? 'checked' : ''##
data-co-event="##:{
change: () => toggleTodo(todo.id)
}##"
>
<span class="todo-text">##=todo.text##</span>
<div class="todo-actions">
<button data-co-event="##:{
click: () => deleteTodo(todo.id)
}##">
×
</button>
</div>
</li>
##})##
</ul>
##}##
##if (status.todos.length > 0) {##
<div class="todo-stats">
<span>##=activeCount## item##=activeCount !== 1 ? 's' : ''## left</span>
##if (completedCount > 0) {##
<button data-co-event="##:{
click: clearCompleted
}##">
Clear completed (##=completedCount##)
</button>
##}##
</div>
##}##
</div>
</template>
`);
// Initialize the todo app
const todoApp = tmpl.todo.App({
title: "My Todo List",
initialTodos: [
{ id: 1, text: "Learn Bridge.js", completed: true },
{ id: 2, text: "Build a todo app", completed: false },
{ id: 3, text: "Share with the community", completed: false },
],
});
// Add to the DOM
document.getElementById("app-container").appendChild(todoApp.element);
compomint.addTmpls(`
<template id="theme-Switcher">
<style id="style-theme-Switcher">
.theme-switcher {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.theme-switcher .theme-menu {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 10px;
margin-bottom: 10px;
display: none;
}
.theme-switcher .theme-menu.active {
display: block;
}
.theme-switcher .theme-list {
list-style: none;
padding: 0;
margin: 0;
}
.theme-switcher .theme-item {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
}
.theme-switcher .theme-item:hover {
background: #f5f5f5;
}
.theme-switcher .theme-item.active {
background: #e3f2fd;
font-weight: bold;
}
.theme-switcher .color-preview {
width: 16px;
height: 16px;
border-radius: 50%;
margin-right: 8px;
border: 1px solid #ddd;
}
.theme-switcher .theme-toggle {
background: #2196F3;
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.theme-switcher .theme-toggle:hover {
background: #1976D2;
}
</style>
##
// Available themes
const themes = data.themes || [
{ id: 'light', name: 'Light', primaryColor: '#2196F3', bgColor: '#ffffff', textColor: '#333333' },
{ id: 'dark', name: 'Dark', primaryColor: '#90CAF9', bgColor: '#282c34', textColor: '#f5f5f5' },
{ id: 'sepia', name: 'Sepia', primaryColor: '#FF9800', bgColor: '#f8f0e3', textColor: '#5f4b32' }
];
// Initialize the current theme
status.isMenuOpen = status.isMenuOpen || false;
status.currentTheme = status.currentTheme || data.defaultTheme || themes[0].id;
// Function to toggle menu open/closed
function toggleMenu() {
status.isMenuOpen = !status.isMenuOpen;
component.refresh({});
}
// Function to apply theme
function applyTheme(themeId) {
status.currentTheme = themeId;
const theme = themes.find(t => t.id === themeId);
// Apply theme to document
document.documentElement.style.setProperty('--primary-color', theme.primaryColor);
document.documentElement.style.setProperty('--background-color', theme.bgColor);
document.documentElement.style.setProperty('--text-color', theme.textColor);
// Add theme class to body
document.body.className = '';
document.body.classList.add('theme-' + themeId);
// Close the menu
status.isMenuOpen = false;
// Call the onThemeChange callback if provided
if (data.onThemeChange) {
data.onThemeChange(themeId, theme);
}
component.refresh({});
}
// Initialize theme on first load
if (data.autoApply !== false && !status.initialized) {
status.initialized = true;
// Add CSS variables to document if not already present
const style = document.createElement('style');
style.textContent = \`
:root {
--primary-color: #2196F3;
--background-color: #ffffff;
--text-color: #333333;
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
a {
color: var(--primary-color);
}
button.primary {
background-color: var(--primary-color);
color: white;
}
\`;
document.head.appendChild(style);
// Apply initial theme
const theme = themes.find(t => t.id === status.currentTheme);
if (theme) {
setTimeout(() => applyTheme(status.currentTheme), 100);
}
}
##
<div class="theme-Switcher">
<div class="theme-menu ##=status.isMenuOpen ? 'active' : ''##">
<ul class="theme-list">
##themes.forEach(theme => {##
<li
class="theme-item ##=status.currentTheme === theme.id ? 'active' : ''##"
data-co-event="##:{
click: () => applyTheme(theme.id)
}##"
>
<span class="color-preview" style="background-color: ##=theme.primaryColor##"></span>
##=theme.name##
</li>
##})##
</ul>
</div>
<button class="theme-toggle" data-co-event="##:{
click: toggleMenu
}##">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"></circle>
<path d="M12 1v2"></path>
<path d="M12 21v2"></path>
<path d="M4.22 4.22l1.42 1.42"></path>
<path d="M18.36 18.36l1.42 1.42"></path>
<path d="M1 12h2"></path>
<path d="M21 12h2"></path>
<path d="M4.22 19.78l1.42-1.42"></path>
<path d="M18.36 5.64l1.42-1.42"></path>
</svg>
</button>
</div>
</template>
`);
// Initialize theme switcher
const themeSwitcher = tmpl.theme.Switcher({
defaultTheme: "light",
autoApply: true,
onThemeChange: function (themeId, theme) {
console.log("Theme changed to:", themeId);
console.log("Theme properties:", theme);
},
});
document.body.appendChild(themeSwitcher.element);
For more detailed information and up-to-date examples, check out the official documentation:
Compomint Website Github Repository
Compomint is released under the MIT License.
MIT License
Copyright (c) 2016-present, Choi Sungho
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.