DOM Manipulation: How I Ditched jQuery and Learned to Love Vanilla JS
Two years ago, I was that developer who reached for jQuery for everything. Need to hide an element? $('.element').hide()
. Change some text? $('#text').text('new value')
. My JavaScript looked like a Christmas tree of dollar signs, and I thought that was normal.
Then I joined a team at Microsoft where jQuery was banned, and I had to learn vanilla JavaScript DOM manipulation. It was like learning to walk again, but once I got it, I realized how much cleaner and faster my code could be.
The jQuery Withdrawal Period
Here's what my old jQuery code looked like:
// My jQuery-heavy past
$(document).ready(function() {
$('.menu-button').click(function() {
$('.navigation').slideToggle();
$(this).toggleClass('active');
});
$('.form-input').on('blur', function() {
if ($(this).val() === '') {
$(this).addClass('error');
$(this).next('.error-message').show();
} else {
$(this).removeClass('error');
$(this).next('.error-message').hide();
}
});
$.ajax({
url: '/api/users',
type: 'GET',
success: function(data) {
$.each(data, function(index, user) {
$('.user-list').append(`
<div class="user-card">
<h3>${user.name}</h3>
<p>${user.email}</p>
</div>
`);
});
}
});
});
And here's the same functionality in modern vanilla JavaScript:
// Modern vanilla JavaScript
document.addEventListener('DOMContentLoaded', () => {
// Menu toggle
const menuButton = document.querySelector('.menu-button');
const navigation = document.querySelector('.navigation');
menuButton.addEventListener('click', () => {
navigation.classList.toggle('hidden');
menuButton.classList.toggle('active');
});
// Form validation
const formInputs = document.querySelectorAll('.form-input');
formInputs.forEach(input => {
input.addEventListener('blur', () => {
const errorMessage = input.nextElementSibling;
if (input.value === '') {
input.classList.add('error');
errorMessage.style.display = 'block';
} else {
input.classList.remove('error');
errorMessage.style.display = 'none';
}
});
});
// Fetch users
fetch('/api/users')
.then(response => response.json())
.then(users => {
const userList = document.querySelector('.user-list');
users.forEach(user => {
userList.appendChild(createUserCard(user));
});
})
.catch(error => console.error('Error:', error));
});
function createUserCard(user) {
const card = document.createElement('div');
card.className = 'user-card';
card.innerHTML = `
<h3>${user.name}</h3>
<p>${user.email}</p>
`;
return card;
}
DOM Selection: Finding Your Elements
The DOM is like a Seattle coffee shop - you need to know how to find what you're looking for:
Basic Selectors
// Get element by ID (fastest)
const header = document.getElementById('main-header');
const loginForm = document.getElementById('login-form');
// Get elements by class name (returns collection)
const buttons = document.getElementsByClassName('btn');
const errorMessages = document.getElementsByClassName('error');
// Get elements by tag name
const allDivs = document.getElementsByTagName('div');
const allImages = document.getElementsByTagName('img');
// Modern query selectors (most flexible)
const firstButton = document.querySelector('.btn'); // First match
const allButtons = document.querySelectorAll('.btn'); // All matches
// Complex selectors (CSS-style)
const activeMenuItems = document.querySelectorAll('.menu .item.active');
const firstInput = document.querySelector('form input[type="text"]');
const lastChild = document.querySelector('.container > div:last-child');
Practical Example: Coffee Shop Menu
// Building an interactive coffee shop menu
class CoffeeMenu {
constructor() {
this.menuContainer = document.querySelector('.menu-container');
this.cartContainer = document.querySelector('.cart-container');
this.totalElement = document.querySelector('.cart-total');
this.cart = [];
this.init();
}
init() {
this.renderMenu();
this.setupEventListeners();
}
renderMenu() {
const drinks = [
{ id: 1, name: 'Flat White', price: 4.50, description: 'Perfect Seattle classic' },
{ id: 2, name: 'Taro Bubble Tea', price: 5.50, description: 'Maya\'s favorite' },
{ id: 3, name: 'Cortado', price: 4.00, description: 'Smooth and balanced' }
];
drinks.forEach(drink => {
const drinkElement = this.createDrinkElement(drink);
this.menuContainer.appendChild(drinkElement);
});
}
createDrinkElement(drink) {
const drinkDiv = document.createElement('div');
drinkDiv.className = 'menu-item';
drinkDiv.dataset.drinkId = drink.id; // Store data
drinkDiv.innerHTML = `
<h3 class="drink-name">${drink.name}</h3>
<p class="drink-description">${drink.description}</p>
<span class="drink-price">$${drink.price.toFixed(2)}</span>
<button class="add-to-cart-btn" data-drink-id="${drink.id}">
Add to Cart
</button>
`;
return drinkDiv;
}
setupEventListeners() {
// Event delegation - listen on parent
this.menuContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('add-to-cart-btn')) {
const drinkId = parseInt(e.target.dataset.drinkId);
this.addToCart(drinkId);
}
});
// Cart interactions
this.cartContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-item')) {
const itemId = parseInt(e.target.dataset.itemId);
this.removeFromCart(itemId);
}
});
}
addToCart(drinkId) {
const drinkElement = document.querySelector(`[data-drink-id="${drinkId}"]`);
const name = drinkElement.querySelector('.drink-name').textContent;
const price = parseFloat(drinkElement.querySelector('.drink-price').textContent.slice(1));
this.cart.push({ id: drinkId, name, price });
this.updateCartDisplay();
// Visual feedback
const button = drinkElement.querySelector('.add-to-cart-btn');
button.textContent = 'Added!';
button.classList.add('added');
setTimeout(() => {
button.textContent = 'Add to Cart';
button.classList.remove('added');
}, 1000);
}
updateCartDisplay() {
this.cartContainer.innerHTML = '';
this.cart.forEach((item, index) => {
const cartItem = document.createElement('div');
cartItem.className = 'cart-item';
cartItem.innerHTML = `
<span>${item.name}</span>
<span>$${item.price.toFixed(2)}</span>
<button class="remove-item" data-item-id="${index}">×</button>
`;
this.cartContainer.appendChild(cartItem);
});
const total = this.cart.reduce((sum, item) => sum + item.price, 0);
this.totalElement.textContent = `Total: $${total.toFixed(2)}`;
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new CoffeeMenu();
});
Element Manipulation: Changing What You Found
Content Manipulation
// Text content (safe from XSS)
const title = document.querySelector('h1');
title.textContent = 'Welcome to Maya\'s Coffee Shop';
// HTML content (use carefully!)
const container = document.querySelector('.content');
container.innerHTML = '<p>This <strong>supports</strong> HTML tags</p>';
// Safe HTML insertion using template literals
function createSafeHTML(user) {
const div = document.createElement('div');
div.className = 'user-profile';
const name = document.createElement('h3');
name.textContent = user.name; // Safe text insertion
const email = document.createElement('p');
email.textContent = user.email;
div.appendChild(name);
div.appendChild(email);
return div;
}
Attribute Manipulation
// Getting and setting attributes
const image = document.querySelector('img');
const currentSrc = image.getAttribute('src');
image.setAttribute('src', '/images/new-photo.jpg');
image.setAttribute('alt', 'Updated coffee shop photo');
// Data attributes (HTML5)
const menuItem = document.querySelector('.menu-item');
menuItem.dataset.price = '4.50';
menuItem.dataset.category = 'coffee';
console.log(menuItem.dataset.price); // "4.50"
// Boolean attributes
const checkbox = document.querySelector('#agree-terms');
checkbox.checked = true;
checkbox.disabled = false;
const button = document.querySelector('#submit-btn');
button.disabled = true; // Disable button
Style Manipulation
// Direct style changes
const element = document.querySelector('.highlight');
element.style.backgroundColor = '#ffd700';
element.style.color = '#333';
element.style.padding = '20px';
// Better approach: use classes
element.classList.add('highlighted');
element.classList.remove('hidden');
element.classList.toggle('active');
// Check if class exists
if (element.classList.contains('error')) {
console.log('Element has error class');
}
// CSS custom properties (CSS variables)
document.documentElement.style.setProperty('--main-color', '#6B73FF');
Event Handling: Making Things Interactive
Modern Event Listeners
// Basic event listener
const button = document.querySelector('#order-button');
button.addEventListener('click', handleOrderClick);
function handleOrderClick(event) {
event.preventDefault(); // Prevent default behavior
console.log('Order button clicked!');
// Access the clicked element
const clickedButton = event.target;
clickedButton.textContent = 'Processing...';
}
// Multiple event types
const input = document.querySelector('#search-input');
input.addEventListener('focus', () => {
input.style.borderColor = '#6B73FF';
});
input.addEventListener('blur', () => {
input.style.borderColor = '#ccc';
});
input.addEventListener('input', (e) => {
console.log('Current value:', e.target.value);
performSearch(e.target.value);
});
Event Delegation (Performance Superpower)
// Bad: Adding listeners to many elements
const allButtons = document.querySelectorAll('.menu-item button');
allButtons.forEach(button => {
button.addEventListener('click', handleClick); // Lots of listeners!
});
// Good: One listener on parent (event delegation)
const menuContainer = document.querySelector('.menu-container');
menuContainer.addEventListener('click', (e) => {
// Check if clicked element is a button
if (e.target.matches('button.add-to-cart')) {
handleAddToCart(e);
}
// Or check by class
if (e.target.classList.contains('remove-item')) {
handleRemoveItem(e);
}
// Or check by data attribute
if (e.target.dataset.action === 'like') {
handleLike(e);
}
});
Dynamic Content Creation
Creating Elements Programmatically
// Real-world example: Dynamic user dashboard
class UserDashboard {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.users = [];
}
async loadUsers() {
try {
const response = await fetch('/api/users');
this.users = await response.json();
this.renderUsers();
} catch (error) {
this.showError('Failed to load users');
}
}
renderUsers() {
// Clear existing content
this.container.innerHTML = '';
// Create header
const header = this.createHeader();
this.container.appendChild(header);
// Create user grid
const userGrid = document.createElement('div');
userGrid.className = 'user-grid';
this.users.forEach(user => {
const userCard = this.createUserCard(user);
userGrid.appendChild(userCard);
});
this.container.appendChild(userGrid);
}
createHeader() {
const header = document.createElement('div');
header.className = 'dashboard-header';
const title = document.createElement('h2');
title.textContent = `Users (${this.users.length})`;
const addButton = document.createElement('button');
addButton.className = 'btn btn-primary';
addButton.textContent = '+ Add User';
addButton.addEventListener('click', () => this.showAddUserModal());
header.appendChild(title);
header.appendChild(addButton);
return header;
}
createUserCard(user) {
const card = document.createElement('div');
card.className = 'user-card';
card.dataset.userId = user.id;
// Avatar
const avatar = document.createElement('img');
avatar.src = user.avatar || '/images/default-avatar.png';
avatar.alt = `${user.name}'s avatar`;
avatar.className = 'user-avatar';
// Info container
const info = document.createElement('div');
info.className = 'user-info';
const name = document.createElement('h3');
name.textContent = user.name;
const email = document.createElement('p');
email.textContent = user.email;
email.className = 'user-email';
const role = document.createElement('span');
role.textContent = user.role;
role.className = `user-role role-${user.role.toLowerCase()}`;
// Actions
const actions = document.createElement('div');
actions.className = 'user-actions';
const editBtn = this.createActionButton('Edit', 'edit', () => this.editUser(user.id));
const deleteBtn = this.createActionButton('Delete', 'delete', () => this.deleteUser(user.id));
actions.appendChild(editBtn);
actions.appendChild(deleteBtn);
// Assemble card
info.appendChild(name);
info.appendChild(email);
info.appendChild(role);
card.appendChild(avatar);
card.appendChild(info);
card.appendChild(actions);
return card;
}
createActionButton(text, action, handler) {
const button = document.createElement('button');
button.textContent = text;
button.className = `btn btn-${action}`;
button.addEventListener('click', handler);
return button;
}
editUser(userId) {
const user = this.users.find(u => u.id === userId);
if (user) {
this.showEditModal(user);
}
}
async deleteUser(userId) {
if (confirm('Are you sure you want to delete this user?')) {
try {
await fetch(`/api/users/${userId}`, { method: 'DELETE' });
this.users = this.users.filter(u => u.id !== userId);
this.renderUsers();
this.showSuccess('User deleted successfully');
} catch (error) {
this.showError('Failed to delete user');
}
}
}
showError(message) {
// Create temporary error notification
const errorDiv = document.createElement('div');
errorDiv.className = 'notification error';
errorDiv.textContent = message;
document.body.appendChild(errorDiv);
setTimeout(() => {
errorDiv.remove();
}, 3000);
}
showSuccess(message) {
const successDiv = document.createElement('div');
successDiv.className = 'notification success';
successDiv.textContent = message;
document.body.appendChild(successDiv);
setTimeout(() => {
successDiv.remove();
}, 3000);
}
}
// Initialize dashboard
document.addEventListener('DOMContentLoaded', () => {
const dashboard = new UserDashboard('dashboard-container');
dashboard.loadUsers();
});
Form Handling and Validation
// Modern form handling
class ContactForm {
constructor(formSelector) {
this.form = document.querySelector(formSelector);
this.setupValidation();
this.setupSubmission();
}
setupValidation() {
const inputs = this.form.querySelectorAll('input, textarea');
inputs.forEach(input => {
input.addEventListener('blur', () => this.validateField(input));
input.addEventListener('input', () => this.clearErrors(input));
});
}
validateField(field) {
const value = field.value.trim();
const fieldName = field.getAttribute('name');
let isValid = true;
let errorMessage = '';
// Clear previous errors
this.clearErrors(field);
// Required field validation
if (field.hasAttribute('required') && !value) {
isValid = false;
errorMessage = `${this.getFieldLabel(field)} is required`;
}
// Email validation
if (field.type === 'email' && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
isValid = false;
errorMessage = 'Please enter a valid email address';
}
}
// Phone validation
if (field.type === 'tel' && value) {
const phoneRegex = /^\+?[\d\s\-\(\)]+$/;
if (!phoneRegex.test(value)) {
isValid = false;
errorMessage = 'Please enter a valid phone number';
}
}
// Show error if validation failed
if (!isValid) {
this.showFieldError(field, errorMessage);
}
return isValid;
}
showFieldError(field, message) {
field.classList.add('error');
let errorElement = field.parentNode.querySelector('.error-message');
if (!errorElement) {
errorElement = document.createElement('span');
errorElement.className = 'error-message';
field.parentNode.appendChild(errorElement);
}
errorElement.textContent = message;
}
clearErrors(field) {
field.classList.remove('error');
const errorElement = field.parentNode.querySelector('.error-message');
if (errorElement) {
errorElement.remove();
}
}
getFieldLabel(field) {
const label = this.form.querySelector(`label[for="${field.id}"]`);
return label ? label.textContent : field.getAttribute('name');
}
setupSubmission() {
this.form.addEventListener('submit', async (e) => {
e.preventDefault();
// Validate all fields
const inputs = this.form.querySelectorAll('input, textarea');
let allValid = true;
inputs.forEach(input => {
if (!this.validateField(input)) {
allValid = false;
}
});
if (allValid) {
await this.submitForm();
} else {
this.showGeneralError('Please fix the errors above');
}
});
}
async submitForm() {
const formData = new FormData(this.form);
const submitButton = this.form.querySelector('button[type="submit"]');
// Show loading state
const originalText = submitButton.textContent;
submitButton.textContent = 'Sending...';
submitButton.disabled = true;
try {
const response = await fetch(this.form.action, {
method: 'POST',
body: formData
});
if (response.ok) {
this.showSuccess('Message sent successfully!');
this.form.reset();
} else {
throw new Error('Network response was not ok');
}
} catch (error) {
this.showGeneralError('Failed to send message. Please try again.');
} finally {
// Restore button state
submitButton.textContent = originalText;
submitButton.disabled = false;
}
}
showGeneralError(message) {
// Remove existing general error
const existingError = this.form.querySelector('.general-error');
if (existingError) {
existingError.remove();
}
const errorDiv = document.createElement('div');
errorDiv.className = 'general-error';
errorDiv.textContent = message;
this.form.insertBefore(errorDiv, this.form.firstChild);
}
showSuccess(message) {
const successDiv = document.createElement('div');
successDiv.className = 'success-message';
successDiv.textContent = message;
this.form.insertBefore(successDiv, this.form.firstChild);
setTimeout(() => {
successDiv.remove();
}, 5000);
}
}
// Initialize form when DOM loads
document.addEventListener('DOMContentLoaded', () => {
new ContactForm('#contact-form');
});
Performance Optimization Tips
Efficient DOM Queries
// Bad: Repeated queries
function updateUserList() {
document.querySelector('.user-count').textContent = users.length;
document.querySelector('.user-list').innerHTML = '';
users.forEach(user => {
document.querySelector('.user-list').appendChild(createUserElement(user));
});
}
// Good: Cache DOM references
class UserListManager {
constructor() {
this.userCountElement = document.querySelector('.user-count');
this.userListElement = document.querySelector('.user-list');
this.users = [];
}
updateList() {
this.userCountElement.textContent = this.users.length;
this.userListElement.innerHTML = '';
// Use document fragment for multiple insertions
const fragment = document.createDocumentFragment();
this.users.forEach(user => {
fragment.appendChild(this.createUserElement(user));
});
this.userListElement.appendChild(fragment);
}
}
Debouncing User Input
// Debounce search input to avoid excessive API calls
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const searchInput = document.querySelector('#search');
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
// Perform actual search
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
Modern DOM APIs
Intersection Observer (Lazy Loading)
// Lazy load images when they come into view
class LazyImageLoader {
constructor() {
this.imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
observer.unobserve(entry.target);
}
});
});
this.observeImages();
}
observeImages() {
const lazyImages = document.querySelectorAll('img[data-src]');
lazyImages.forEach(img => this.imageObserver.observe(img));
}
loadImage(img) {
img.src = img.dataset.src;
img.classList.add('loaded');
img.removeAttribute('data-src');
}
}
// Initialize lazy loading
document.addEventListener('DOMContentLoaded', () => {
new LazyImageLoader();
});
Common DOM Manipulation Mistakes
Memory Leaks with Event Listeners
// Bad: Creating memory leaks
function createComponent() {
const element = document.createElement('div');
element.addEventListener('click', handleClick);
document.body.appendChild(element);
// Later removing element but not the listener!
element.remove(); // Listener still exists in memory
}
// Good: Clean up listeners
class Component {
constructor() {
this.element = document.createElement('div');
this.handleClick = this.handleClick.bind(this);
this.element.addEventListener('click', this.handleClick);
}
destroy() {
this.element.removeEventListener('click', this.handleClick);
this.element.remove();
}
handleClick(e) {
console.log('Clicked!');
}
}
Final Thoughts: DOM Mastery = Web Development Superpowers
That transition from jQuery to vanilla JavaScript was painful but transformative. I went from writing code that felt magical but opaque to code that I truly understood. My applications became faster, my bundle sizes smaller, and my debugging skills significantly improved.
DOM manipulation is the foundation of interactive web development. Whether you're building a simple contact form or a complex single-page application, understanding how to efficiently find, modify, and respond to elements is crucial.
The modern DOM API is incredibly powerful. With features like event delegation, intersection observers, and efficient querying methods, you can build performant, interactive experiences without heavy frameworks.
Start with the basics - selecting elements, changing content, handling events. Then gradually add complexity. Before you know it, you'll be building dynamic interfaces that feel native and responsive.
Remember: the DOM is just JavaScript objects representing HTML. Once you internalize that concept, everything else becomes logical manipulation of those objects.
Currently writing this from Analog Coffee in Capitol Hill, where I'm debugging a DOM issue while enjoying the perfect cortado. Share your DOM manipulation victories (or disasters) @maya_codes_pnw - we've all been there! 🌐☕
Add Comment
No comments yet. Be the first to comment!