@alphanull/domsmith
v2.2.0
Published
DomSmith is a lightweight and declarative DOM builder for JavaScript that enables you to create, update, and remove DOM trees with an intuitive API.
Maintainers
Readme
DomSmith
DomSmith is a lightweight and declarative DOM builder for JavaScript that enables you to create, update, and remove DOM trees with an intuitive API. It supports both HTML and SVG elements with automatic namespace handling (including <foreignObject> support) and centralized event management with proper cleanup. By also providing direct access to element references via the instance, DomSmith aims to simplify UI component creation for modern web applications.
Features
- Declarative DOM Creation: Create complex DOM structures with simple JSON-like configurations.
- HTML & SVG Support: Automatically uses the correct namespace (including
<foreignObject>support). - Centralized Event Handling: Easily attach events and ensure proper cleanup.
- Direct References: Expose element references directly on the builder instance.
- Node Replacement & Removal: Dynamically update and remove parts of your DOM tree.
- Plugin System: Extensible architecture with lifecycle hooks and priority-based execution.
- Flexible Mounting: Multiple insert modes (append, before, replace, top) for precise DOM placement.
- Memory Management: Proper cleanup with the
destroy()method.
Installation
Using NPM
npm install @alphanull/domsmithUsing CDN
Download latest version from jsDelivr Download latest version from unpkg
via GitHub
Download release from GitHub
Usage
1. Basic Usage
Create a simple DOM tree with nested children. The resulting DOM tree is immediately mounted to document.body.
import DomSmith from '@alphanull/domsmith';
const domConfig = {
_tag: 'div', // 'div' is default and can be omitted.
_nodes: [ // _nodes array contains child nodes
{
_tag: 'header', // The tag name
_nodes: [
{
_tag: 'h1',
// Use a string as a _nodes definition to create a single text node
_nodes: 'Welcome to DomSmith!'
}
]
},
{
_tag: 'section',
_nodes: [
{
_tag: 'p',
// You can also use a string for text node(s) in an _nodes array
_nodes: [
{
_tag: 'span',
_nodes: 'This is a basic usage example. '
},
'Second Text Node'
]
}
]
}
]
};
// New API with extended options (v2.1.0+)
const dom = new DomSmith(domConfig, { ele: document.body, insertMode: 'append' });
// Legacy API shortcut (insertMode defaults to 'append')
const dom2 = new DomSmith(domConfig, document.body);
// Alternatively, if you omit the second parameter, the DOM is not mounted immediately
const dom3 = new DomSmith(domConfig);
// ... later, you can mount manually:
dom3.mount({ ele: myEle, insertMode: 'replace' });2. Attributes and Properties
Assign attributes and properties directly in the configuration.
const domConfig = {
_tag: 'button',
id: 'myButton', // Will be set as an attribute or property.
className: 'btn-primary', // Will be set as an attribute or property.
'style.backgroundColor': 'skyblue', // Dot notation for nested properties.
_nodes: 'I am a Button'
};DomSmith automatically determines whether to use setAttribute() or set the value directly on the property. If the property exists, direct assignment is used. If the property is not found or assignment fails (for example, due to being readonly on svg elements) setAttribute() is used. Also, especially for style properties you can use dot notation.
Please note: It is strongly recommended to avoid properties beginning with an underscore to prevent conflicts with internal properties.
3. References
Expose direct references to DOM elements by specifying the _ref property for easy access and further manipulation:
const domConfig = {
_ref: 'container',
_nodes: [
{
_tag: 'p',
_nodes: 'Paragraph 1'
},
{
_tag: 'p',
_ref: 'secondParagraph',
_nodes: 'Paragraph 2'
}
]
};
const dom = new DomSmith(domConfig, document.body);
console.log(dom.container); // Direct access to the container div
console.log(dom.secondParagraph); // Direct access to the second paragraphIn case you want to directly reference a text node, you can use this format:
const domConfig = {
_ref: 'container',
_nodes: [
{
_ref: 'myText',
_text: 'A referenced TextNode'
}
]
};
const dom = new DomSmith(domConfig, document.body);
dom.myText.nodeValue = 'Changed Text'; // direct access to TextNodePlease note: You can use almost any string as a reference name, but since the refs are exposed on the root of the instance, certain names (especially those reserved for the API) cannot be used. Therefore, it is strongly recommended to avoid refs beginning with an underscore as well as a dollar sign to prevent conflicts with internal properties. Additionally, refs must be unique within each instance; duplicate refs will throw an error.
Legacy Support:
The old property names (ref, tag, text, nodes, events) are still supported but will show deprecation warnings. It's recommended to migrate to the new underscore-prefixed versions.
4. Events
Using the shortcut notation
Attach event listeners directly within the node definition. These event listeners will be automatically removed when teardown() or removeNode() is called, so manual cleanup is usually unecessary.
const domConfig = {
_tag: 'button',
_nodes: 'Click Me',
mouseover: () => console.log('Mouse over button'),
click: [ // You can bind multiple handlers to the same event by using an array of handlers
() => console.log('Button clicked!'),
() => console.log('Second Click handlers!')
]
};Please note:
Event names are derived dynamically at runtime and only cover events that have a corresponding “on” property (e.g., onclick). Some events, such as compositionupdate, do not have this equivalent, nor do custom events. In those cases, you can specify events explicitly:
Setting events explicity (mostly for edge cases and custom events)
const domConfig = {
_tag: 'input',
_events: {
compositionupdate: () => console.log('Composition updated')
}
};Manually adding and removing events
You can also add or remove events manually after creating the DomSmith Instance by using a reference:
const domConfig = {
_tag: 'button',
_nodes: 'Click Me',
_ref: 'button',
mouseover: () => console.log('Mouse over button')
};
const dom = new DomSmith(domConfig, document.body);
dom.addEvent('button', 'click', () => console.log('Button clicked!')); // add another listener
dom.removeEvent('button', 'mouseover'); // removes all 'mouseover' events
dom.removeEvent('button'); // removes _all_ events5. Node Replacement and Removal
Dynamically replace or remove nodes from the DOM tree. This process also cleans up any attached event listeners.
const domConfig = {
_nodes: [
{
_tag: 'p',
_ref: 'message',
_nodes: 'Old content'
}
]
};
const dom = new DomSmith(domConfig, { ele: document.body });
// Replace the paragraph with new content
dom.replaceNode('message', {
_tag: 'p',
_ref: 'message',
_nodes: 'New content'
});
// Remove the paragraph after 3 seconds
setTimeout(() => {
dom.removeNode(dom.message);
}, 3000);6. Multiple Root Nodes
This example demonstrates how to pass an array as the node definition so that multiple sibling nodes are created as the root. Each node is appended individually to the specified parent element, and the defined refs become direct properties on the instance.
const domConfig = [
{
_tag: 'header',
_nodes: 'Header Content',
},
{
_tag: 'main',
_ref: 'main',
_nodes: [
{
_tag: 'p',
_nodes: 'This is the main content.'
}
]
},
{
_tag: 'footer',
_nodes: 'Footer Content'
}
];7. SVG
This example shows how DomSmith supports SVG elements, including the use of . Child nodes within a are created in the HTML namespace, while the rest of the SVG uses the SVG namespace:
const domConfig = {
_tag: 'svg',
width: 300,
height: 200,
_nodes: [
{
_tag: 'rect',
x: 10,
y: 10,
width: 280,
height: 180,
fill: 'lightblue'
},
{
_tag: 'foreignObject',
width: 100,
height: 50,
// Within a foreignObject, child nodes are created in the HTML namespace.
_nodes: [
{
_nodes: 'HTML inside foreignObject',
style: 'color: red; font-size: 14px;'
}
]
}
]
});8. Plugin System
DomSmith includes a powerful plugin system that allows you to extend functionality. Plugins can hook into various lifecycle events and modify node definitions.
Built-in Plugins
DomSmith comes with two built-in plugins that are available as separate modules:
Input Range Plugin: Enhances <input type="range"> elements with touch-drag support for mobile devices.
// Automatically applied to all range inputs
const rangeConfig = {
_tag: 'input',
type: 'range',
min: 0,
max: 100,
value: 50
// Touch-drag support is automatically added
};
// Disable the plugin for specific elements
const disabledRangeConfig = {
_tag: 'input',
type: 'range',
$rangeFixDisable: true // Plugin will be skipped for this element
};Select Wrapper Plugin: Automatically wraps <select> elements for enhanced styling.
// Automatically wrapped in .select-wrapper container
const selectConfig = {
_tag: 'select',
_nodes: [
{ _tag: 'option', _nodes: 'Option 1' },
{ _tag: 'option', _nodes: 'Option 2' }
]
};Using Built-in Plugins
The built-in plugins are also available as separate modules for optional usage:
import DomSmith from '@alphanull/domsmith';
import inputRangePlugin from '@alphanull/domsmith/plugins/domSmithInputRange.min.js';
import selectPlugin from '@alphanull/domsmith/plugins/domSmithSelect.min.js';
// Register plugins manually
DomSmith.registerPlugin(inputRangePlugin);
DomSmith.registerPlugin(selectPlugin);
// Now use DomSmith with plugin support
const dom = new DomSmith(config, { ele: document.body });Custom Plugins
You can create custom plugins by implementing certain lifecycle hooks. Plugins are executed in priority order (higher priority runs first).
Available Lifecycle Hooks:
addNode(nodeDef)- Called during node creation, can modify and return the node definitionremoveNode(nodeDef)- Called during node removal, can modify and return the node definitionmount(dom, mountContext)- Called when DOM is mountedunmount(dom, mountContext)- Called when DOM is unmounteddestroy()- Called when the DomSmith instance is destroyeddestroyPlugin()- Called when globalDomSmith.destroyPlugins()is called
const myPlugin = {
addNode(nodeDef) {
// Modify node definition during creation
if (nodeDef._tag === 'button') {
nodeDef.className = 'custom-button';
}
return nodeDef;
},
mount(dom, mountContext) {
// Called when DOM is mounted
console.log('DOM mounted:', mountContext);
},
destroy() {
// Called when instance is destroyed
console.log('Plugin cleanup');
}
};
// Register plugin with priority (higher runs earlier)
DomSmith.registerPlugin(myPlugin, { priority: 10 });
// Destroy all plugins
DomSmith.destroyPlugins();Plugin Configuration with $ Variables
Plugins can be configured using $-prefixed properties in node definitions. These properties are not rendered to the DOM but are used by plugins for configuration.
const myCustomPlugin = {
addNode(nodeDef) {
// Check for plugin-specific configuration
if (nodeDef.$myPluginEnabled === false) return; // Skip processing
if (nodeDef._tag === 'div' && nodeDef.$myPluginClass) {
nodeDef.className = nodeDef.$myPluginClass;
}
return nodeDef;
}
};
// Usage in node definitions
const config = {
_tag: 'div',
$myPluginEnabled: true,
$myPluginClass: 'special-styling',
_nodes: 'Content'
};Plugin Chaining
Plugins are executed in sequence, allowing them to build upon each other's modifications:
const plugin1 = {
addNode(nodeDef) {
if (nodeDef._tag === 'button') {
nodeDef.className = 'base-button';
}
return nodeDef;
}
};
const plugin2 = {
addNode(nodeDef) {
if (nodeDef._tag === 'button' && nodeDef.className === 'base-button') {
nodeDef.className += ' enhanced-button';
}
return nodeDef;
}
};
// Register plugins with different priorities
DomSmith.registerPlugin(plugin1, { priority: 10 }); // Runs first
DomSmith.registerPlugin(plugin2, { priority: 5 }); // Runs second
// Result: button will have className 'base-button enhanced-button'Important Notes:
- Plugins are registered globally and affect all DomSmith instances
- Duplicate plugin instances are automatically ignored
$-prefixed properties are automatically filtered out and not rendered to the DOM- Plugin execution order is determined by priority (higher numbers run first)
- If a plugin hook returns
undefined, the original node definition is preserved
9. Mount Options
DomSmith supports multiple insert modes for precise DOM placement:
// Append (default) - adds to end of parent
const dom1 = new DomSmith(config, { ele: parent, insertMode: 'append' });
// Before - inserts before target element
const dom2 = new DomSmith(config, { ele: target, insertMode: 'before' });
// Replace - replaces target element
const dom3 = new DomSmith(config, { ele: target, insertMode: 'replace' });
// Top - inserts as first child of parent
const dom4 = new DomSmith(config, { ele: parent, insertMode: 'top' });10. Unmount & Cleanup
Use unmount() to remove the DomSmith elements from the DOM while preserving event bindings, allowing you to remount the instance later.
const dom = new DomSmith(domConfig, { ele: document.body });
dom.unmount();
// Later...
dom.mount(); // Re-mounts to original locationUse destroy() to completely remove the DomSmith instance, including all event listeners, mounted elements, and references.
const dom = new DomSmith(domConfig, { ele: document.body });
dom.destroy(); // Complete cleanupNote: The old teardown() method is deprecated and will show a warning. Use destroy() instead.
Docs
For more detailed docs, see JSDoc Documentation
License
Copyright © 2016-present Frank Kudermann @ alphanull.de
