A lightweight, secure alternative to building DOM elements without innerHTML security risks.
This framework provides a safe way to create DOM elements programmatically, avoiding XSS vulnerabilities common with string-based HTML generation.
function createElement(tag, props = {}, ...children) {
const element = document.createElement(tag);
// Handle props safely
setProps(element, props);
// Handle children safely
appendChildren(element, children);
return element;
}
const SAFE_ATTRIBUTES = new Set([
'class', 'className', 'id', 'href', 'src', 'alt', 'title',
'type', 'value', 'placeholder', 'disabled', 'readonly',
'target', 'rel', 'download', 'role', 'aria-label'
]);
function setProps(element, props) {
Object.entries(props).forEach(([key, value]) => {
if (key === 'className') {
element.className = String(value);
} else if (key.startsWith('on') && typeof value === 'function') {
// Event handlers - safe because they're actual functions
const eventName = key.slice(2).toLowerCase();
element.addEventListener(eventName, value);
} else if (SAFE_ATTRIBUTES.has(key)) {
// Only allow safe attributes
element.setAttribute(key, String(value));
} else if (key === 'style' && typeof value === 'object') {
// Handle style objects
Object.assign(element.style, value);
}
// Unsafe attributes are silently ignored
});
}
function appendChildren(element, children) {
children.flat(Infinity).forEach(child => {
if (child == null || child === false) return;
if (typeof child === 'string' || typeof child === 'number') {
// Safe text content - no HTML parsing
element.appendChild(document.createTextNode(String(child)));
} else if (child instanceof Node) {
// DOM nodes are safe
element.appendChild(child);
} else if (Array.isArray(child)) {
// Handle nested arrays
appendChildren(element, child);
}
// Other types are ignored for safety
});
}
function render(element, container) {
if (typeof container === 'string') {
container = document.querySelector(container);
}
if (!container) {
throw new Error('Container not found');
}
container.replaceChildren(element);
}
// Alternative: append instead of replace
function appendTo(element, container) {
if (typeof container === 'string') {
container = document.querySelector(container);
}
if (!container) {
throw new Error('Container not found');
}
container.appendChild(element);
}
// Simple text element
const heading = createElement('h1', { className: 'title' }, 'Welcome');
// Element with attributes
const link = createElement('a', {
href: '/notes/123',
className: 'btn btn-primary',
target: '_blank'
}, 'View Note');
// Element with styles
const box = createElement('div', {
className: 'box',
style: { backgroundColor: 'blue', padding: '10px' }
}, 'Styled content');
const button = createElement('button', {
className: 'btn btn-danger',
onclick: (e) => {
console.log('Button clicked!', e);
},
onmouseover: () => {
console.log('Mouse over button');
}
}, 'Click me');
// Icon + text
const icon = createElement('i', { className: 'fas fa-play' });
const playButton = createElement('button', {
className: 'btn btn-success',
onclick: startRecording
}, icon, ' Start Recording');
// Complex nesting
const card = createElement('div', { className: 'card' },
createElement('div', { className: 'card-header' },
createElement('h3', {}, 'Recording Complete')
),
createElement('div', { className: 'card-body' },
createElement('p', {}, 'Your audio has been saved successfully.'),
createElement('div', { className: 'actions' },
createElement('a', {
href: '/notes/123',
className: 'btn btn-primary'
}, 'View Note'),
createElement('button', {
className: 'btn btn-secondary',
onclick: startNewRecording
}, 'Record Again')
)
)
);
// Dynamic list creation
const menuItems = ['Home', 'About', 'Contact'];
const navItems = menuItems.map(item =>
createElement('li', {},
createElement('a', {
href: `/${item.toLowerCase()}`,
className: 'nav-link'
}, item)
)
);
const nav = createElement('ul', { className: 'nav' }, ...navItems);
function createUserProfile(user) {
return createElement('div', { className: 'user-profile' },
createElement('h2', {}, user.name),
user.avatar && createElement('img', {
src: user.avatar,
alt: `${user.name}'s avatar`,
className: 'avatar'
}),
user.isAdmin && createElement('span', {
className: 'badge admin-badge'
}, 'Admin'),
createElement('p', {}, user.bio || 'No bio available')
);
}
const content = createElement('div', {},
createElement('h1', {}, 'New Content'),
createElement('p', {}, 'This replaces everything in the container')
);
render(content, '#main-container');
// or
render(content, document.getElementById('main-container'));
const newItem = createElement('li', {}, 'New list item');
appendTo(newItem, '#todo-list');
const elements = [
createElement('h2', {}, 'Section 1'),
createElement('p', {}, 'Content for section 1'),
createElement('h2', {}, 'Section 2'),
createElement('p', {}, 'Content for section 2')
];
const container = document.getElementById('content');
container.replaceChildren(...elements);
linkContainer.innerHTML = `<a href="${noteUrl}" class="btn btn-success">Go to recording page</a>`;
Note: This is actually safe since noteUrl
is server-controlled and content is static
const link = createElement('a', {
href: noteUrl,
className: 'btn btn-success'
}, 'Go to recording page');
linkContainer.replaceChildren(link);
Benefits: Better for complex components, easier testing, reusable elements
- No XSS vulnerabilities - No HTML string parsing
- Attribute validation - Only safe attributes are allowed
- Automatic escaping - Text content is automatically escaped
- Type safety - Only valid DOM nodes and primitives accepted
- Event handler safety - Real function references, not string eval
- Direct DOM manipulation - No HTML parsing overhead
- Reusable elements - Elements can be moved between containers
- Memory efficient - No temporary HTML strings
- Better debugging - Real DOM nodes in dev tools
function createRecordingLink(noteId, baseUrl) {
const noteUrl = baseUrl.replace('0', noteId);
const link = createElement('a', {
href: noteUrl,
className: 'btn btn-success',
onclick: (e) => {
// Optional: Add analytics or other tracking
console.log('User navigating to note:', noteId);
}
}, 'Go to recording page');
const container = createElement('div', {
className: 'mt-3'
}, link);
return container;
}
// Usage in your fetch handler
.then(data => {
if (data.id) {
const linkContainer = createRecordingLink(data.id, noteDetailUrlBase);
document.querySelector('#new_note_link').replaceChildren(linkContainer);
}
})
This framework provides all the benefits of a component system while maintaining security and performance.