Skip to content

Instantly share code, notes, and snippets.

@theptrk
Created August 10, 2025 22:52
Show Gist options
  • Save theptrk/25b7be5831d20def0b94b92eb3996aef to your computer and use it in GitHub Desktop.
Save theptrk/25b7be5831d20def0b94b92eb3996aef to your computer and use it in GitHub Desktop.

Secure Mini DOM Framework

A lightweight, secure alternative to building DOM elements without innerHTML security risks.

Overview

This framework provides a safe way to create DOM elements programmatically, avoiding XSS vulnerabilities common with string-based HTML generation.

Core Implementation

1. Main createElement Function

function createElement(tag, props = {}, ...children) {
  const element = document.createElement(tag);
  
  // Handle props safely
  setProps(element, props);
  
  // Handle children safely  
  appendChildren(element, children);
  
  return element;
}

2. Secure Property Handling

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
  });
}

3. Safe Children Handling

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
  });
}

4. Render Helper

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);
}

Usage Examples

Basic Elements

// 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');

Event Handling

const button = createElement('button', {
  className: 'btn btn-danger',
  onclick: (e) => {
    console.log('Button clicked!', e);
  },
  onmouseover: () => {
    console.log('Mouse over button');
  }
}, 'Click me');

Nested Elements

// 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')
    )
  )
);

Lists and Arrays

// 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);

Conditional Rendering

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')
  );
}

Rendering to DOM

Replace Content

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'));

Append Content

const newItem = createElement('li', {}, 'New list item');
appendTo(newItem, '#todo-list');

Multiple Elements

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);

Converting Your Current Code

Before (Template Literal - Actually Safe)

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

After (DOM-based - Alternative Approach)

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

Security Benefits

  1. No XSS vulnerabilities - No HTML string parsing
  2. Attribute validation - Only safe attributes are allowed
  3. Automatic escaping - Text content is automatically escaped
  4. Type safety - Only valid DOM nodes and primitives accepted
  5. Event handler safety - Real function references, not string eval

Performance Benefits

  1. Direct DOM manipulation - No HTML parsing overhead
  2. Reusable elements - Elements can be moved between containers
  3. Memory efficient - No temporary HTML strings
  4. Better debugging - Real DOM nodes in dev tools

Complete Example: Your Recording Page

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment