<h1>🔮 MutationObserver Magic Show</h1>
<p class="intro">Watch the DOM change before your eyes and see how MutationObserver catches every transformation!</p>
<div class="playground-container">
<div class="observer-status status-inactive" id="observerStatus">
Observer Status: INACTIVE
</div>
<div class="stats">
<div class="stat">
<div class="stat-value" id="elemCount">1</div>
<div class="stat-label">Elements</div>
</div>
<div class="stat">
<div class="stat-value" id="addCount">0</div>
<div class="stat-label">Additions</div>
</div>
<div class="stat">
<div class="stat-value" id="removeCount">0</div>
<div class="stat-label">Removals</div>
</div>
<div class="stat">
<div class="stat-value" id="attrCount">0</div>
<div class="stat-label">Attributes</div>
</div>
<div class="stat">
<div class="stat-value" id="textCount">0</div>
<div class="stat-label">Text Changes</div>
</div>
</div>
<div class="controls">
<div class="control-group">
<h3>Observer Controls</h3>
<div class="buttons">
<button id="startBtn" class="btn-start">Start Observing</button>
<button id="stopBtn" class="btn-stop">Stop Observing</button>
</div>
<div class="options">
<label class="checkbox-container">
<input type="checkbox" id="childListOpt" checked>
childList
</label>
<label class="checkbox-container">
<input type="checkbox" id="attributesOpt" checked>
attributes
</label>
<label class="checkbox-container">
<input type="checkbox" id="characterDataOpt" checked>
characterData
</label>
<label class="checkbox-container">
<input type="checkbox" id="subtreeOpt" checked>
subtree
</label>
</div>
</div>
<div class="control-group">
<h3>DOM Manipulation</h3>
<div class="buttons">
<button id="addBtn" class="btn-add">Add Element</button>
<button id="removeBtn" class="btn-remove">Remove Element</button>
<button id="colorBtn" class="btn-color">Change Color</button>
<button id="textBtn" class="btn-text">Change Text</button>
<button id="clearLogBtn" class="btn-clear">Clear Log</button>
</div>
</div>
</div>
<div class="playground">
<div class="dom-container" id="domContainer">
<div class="element" data-id="initial">Initial Element</div>
</div>
<div class="log-container">
<h3>Mutation Log</h3>
<div id="logContainer"></div>
</div>
</div>
<div class="challenge" id="challengeContainer">
<h3>Challenge: Observer Mastery</h3>
<p>Complete these tasks to master MutationObserver:</p>
<ol>
<li id="challenge1">Add at least 3 elements</li>
<li id="challenge2">Remove at least 1 element</li>
<li id="challenge3">Change the color of an element</li>
<li id="challenge4">Change text content of an element</li>
<li id="challenge5">Observe with only childList option enabled</li>
</ol>
</div>
</div>
:root {
--primary: #6200ee;
--secondary: #03dac6;
--danger: #cf6679;
--warning: #ffab00;
--success: #00c853;
--info: #2196f3;
--light: #f5f5f5;
--dark: #121212;
}
body {
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
max-width: 1000px;
margin: 0 auto;
background-color: var(--light);
color: var(--dark);
}
h1,
h2 {
text-align: center;
color: var(--primary);
}
.playground-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.observer-status {
text-align: center;
padding: 10px;
border-radius: 8px;
font-weight: bold;
color: white;
margin-bottom: 15px;
font-size: 18px;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s ease;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.status-active {
background-color: var(--success);
}
.status-inactive {
background-color: var(--danger);
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 15px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.control-group {
flex: 1;
min-width: 300px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
button {
padding: 10px 16px;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
min-width: 100px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
button:active {
transform: translateY(0);
}
.btn-start {
background-color: var(--success);
color: white;
}
.btn-stop {
background-color: var(--danger);
color: white;
}
.btn-add {
background-color: var(--primary);
color: white;
}
.btn-remove {
background-color: var(--danger);
color: white;
}
.btn-color {
background-color: var(--secondary);
color: var(--dark);
}
.btn-text {
background-color: var(--info);
color: white;
}
.btn-clear {
background-color: var(--warning);
color: var(--dark);
}
.options {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
}
.checkbox-container {
display: flex;
align-items: center;
margin-right: 15px;
}
.checkbox-container input {
margin-right: 5px;
}
.playground {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.dom-container {
border: 3px dashed var(--primary);
min-height: 300px;
padding: 15px;
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow-y: auto;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 10px;
transition: all 0.3s ease;
}
.dom-container-inactive {
border: 3px dashed var(--danger);
background-color: #ffebee;
}
.element {
padding: 12px;
border-radius: 6px;
margin: 5px;
display: inline-block;
background-color: var(--light);
border: 1px solid #ddd;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
user-select: none;
cursor: pointer;
}
.element:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.log-container {
border: 1px solid #ddd;
min-height: 300px;
padding: 15px;
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow-y: auto;
display: flex;
flex-direction: column;
}
.log-entry {
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 6px;
font-size: 14px;
animation: fadeIn 0.5s;
border-left: 4px solid transparent;
}
.log-time {
font-size: 12px;
color: #666;
margin-right: 8px;
}
.log-info {
background-color: #e3f2fd;
border-left-color: var(--info);
}
.log-add {
background-color: #e8f5e9;
border-left-color: var(--success);
}
.log-remove {
background-color: #ffebee;
border-left-color: var(--danger);
}
.log-attr {
background-color: #fff8e1;
border-left-color: var(--warning);
}
.log-text {
background-color: #e0f7fa;
border-left-color: var(--secondary);
}
.stats {
display: flex;
justify-content: space-between;
padding: 10px;
background-color: var(--dark);
color: white;
border-radius: 8px;
margin-bottom: 15px;
}
.stat {
text-align: center;
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: var(--secondary);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.pulse {
animation: pulse 0.5s;
}
.challenge {
background-color: white;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.challenge h3 {
color: var(--primary);
margin-top: 0;
}
.challenge-complete {
background-color: #e8f5e9;
border-left: 4px solid var(--success);
}
// DOM Elements & State Management
const elements = {
domContainer: document.getElementById("domContainer"),
logContainer: document.getElementById("logContainer"),
startBtn: document.getElementById("startBtn"),
stopBtn: document.getElementById("stopBtn"),
addBtn: document.getElementById("addBtn"),
removeBtn: document.getElementById("removeBtn"),
colorBtn: document.getElementById("colorBtn"),
textBtn: document.getElementById("textBtn"),
clearLogBtn: document.getElementById("clearLogBtn"),
childListOpt: document.getElementById("childListOpt"),
attributesOpt: document.getElementById("attributesOpt"),
characterDataOpt: document.getElementById("characterDataOpt"),
subtreeOpt: document.getElementById("subtreeOpt"),
observerStatus: document.getElementById("observerStatus"),
elemCount: document.getElementById("elemCount"),
addCount: document.getElementById("addCount"),
removeCount: document.getElementById("removeCount"),
attrCount: document.getElementById("attrCount"),
textCount: document.getElementById("textCount"),
challenge1: document.getElementById("challenge1"),
challenge2: document.getElementById("challenge2"),
challenge3: document.getElementById("challenge3"),
challenge4: document.getElementById("challenge4"),
challenge5: document.getElementById("challenge5")
};
// State
const state = {
observer: null,
observing: false,
stats: {
elements: 1,
additions: 0,
removals: 0,
attributes: 0,
texts: 0
},
challenges: {
addElements: false,
removeElement: false,
changeColor: false,
changeText: false,
childListOnly: false
}
};
// Color palette for elements
const colors = [
"#ffcdd2",
"#f8bbd0",
"#e1bee7",
"#d1c4e9",
"#c5cae9",
"#bbdefb",
"#b3e5fc",
"#b2ebf2",
"#b2dfdb",
"#c8e6c9",
"#dcedc8",
"#f0f4c3",
"#fff9c4",
"#ffecb3",
"#ffe0b2"
];
// Utility Functions
const getRandomColor = () => colors[Math.floor(Math.random() * colors.length)];
const getRandomElement = () => {
const elementsList = elements.domContainer.querySelectorAll(".element");
if (elementsList.length === 0) return null;
return elementsList[Math.floor(Math.random() * elementsList.length)];
};
// Logging & UI Updates
const addLog = (message, type) => {
const entry = document.createElement("div");
entry.className = `log-entry log-${type}`;
const time = document.createElement("span");
time.className = "log-time";
time.textContent = new Date().toLocaleTimeString();
entry.appendChild(time);
entry.appendChild(document.createTextNode(message));
elements.logContainer.insertBefore(entry, elements.logContainer.firstChild);
};
const updateStats = () => {
elements.elemCount.textContent = state.stats.elements;
elements.addCount.textContent = state.stats.additions;
elements.removeCount.textContent = state.stats.removals;
elements.attrCount.textContent = state.stats.attributes;
elements.textCount.textContent = state.stats.texts;
// Apply pulse animation
elements.elemCount.classList.add("pulse");
setTimeout(() => elements.elemCount.classList.remove("pulse"), 500);
};
const checkChallenges = () => {
// Challenge 1: Add elements
if (state.stats.additions >= 3 && !state.challenges.addElements) {
state.challenges.addElements = true;
elements.challenge1.style.textDecoration = "line-through";
elements.challenge1.style.color = "green";
}
// Challenge 2: Remove element
if (state.stats.removals >= 1 && !state.challenges.removeElement) {
state.challenges.removeElement = true;
elements.challenge2.style.textDecoration = "line-through";
elements.challenge2.style.color = "green";
}
// Challenge 3: Change color
if (state.stats.attributes >= 1 && !state.challenges.changeColor) {
state.challenges.changeColor = true;
elements.challenge3.style.textDecoration = "line-through";
elements.challenge3.style.color = "green";
}
// Challenge 4: Change text
if (state.stats.texts >= 1 && !state.challenges.changeText) {
state.challenges.changeText = true;
elements.challenge4.style.textDecoration = "line-through";
elements.challenge4.style.color = "green";
}
// Check if all challenges complete
if (Object.values(state.challenges).every(Boolean)) {
const challengeContainer = document.getElementById("challengeContainer");
challengeContainer.classList.add("challenge-complete");
challengeContainer.innerHTML =
"<h3>🎉 Congratulations! You've mastered MutationObserver!</h3><p>You now understand how to track DOM changes like a pro. This powerful API lets you respond to dynamic content in real-time.</p>";
}
};
// Observer Functions
const startObserving = () => {
if (state.observing) return;
// Get current element count for accuracy
state.stats.elements = elements.domContainer.querySelectorAll(
".element"
).length;
updateStats();
// Create options object based on checkboxes
const options = {
childList: elements.childListOpt.checked,
attributes: elements.attributesOpt.checked,
characterData: elements.characterDataOpt.checked,
subtree: elements.subtreeOpt.checked,
attributeOldValue: true
};
// Check for childList only challenge
if (
options.childList &&
!options.attributes &&
!options.characterData &&
!state.challenges.childListOnly
) {
state.challenges.childListOnly = true;
elements.challenge5.style.textDecoration = "line-through";
elements.challenge5.style.color = "green";
checkChallenges();
}
// Create new MutationObserver
state.observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
// Handle different mutation types
if (mutation.type === "childList") {
handleChildListMutation(mutation);
} else if (mutation.type === "attributes") {
handleAttributeMutation(mutation);
} else if (mutation.type === "characterData") {
handleCharacterDataMutation(mutation);
}
}
});
// Start observing with options
state.observer.observe(elements.domContainer, options);
state.observing = true;
// Update UI
elements.startBtn.disabled = true;
elements.stopBtn.disabled = false;
elements.observerStatus.textContent = "Observer Status: ACTIVE";
elements.observerStatus.classList.remove("status-inactive");
elements.observerStatus.classList.add("status-active");
elements.domContainer.classList.remove("dom-container-inactive");
// Add log entry
addLog(`Started observing with options: ${JSON.stringify(options)}`, "info");
};
const stopObserving = () => {
if (!state.observing) return;
state.observer.disconnect();
state.observing = false;
// Update UI
elements.startBtn.disabled = false;
elements.stopBtn.disabled = true;
elements.observerStatus.textContent = "Observer Status: INACTIVE";
elements.observerStatus.classList.remove("status-active");
elements.observerStatus.classList.add("status-inactive");
elements.domContainer.classList.add("dom-container-inactive");
// Add log entry
addLog("Stopped observing", "info");
};
// Mutation Handlers
const handleChildListMutation = (mutation) => {
// Added nodes
if (mutation.addedNodes.length > 0) {
state.stats.additions += mutation.addedNodes.length;
state.stats.elements = elements.domContainer.querySelectorAll(
".element"
).length;
updateStats();
const nodeInfo = Array.from(mutation.addedNodes)
.filter((node) => node.classList?.contains("element"))
.map((node) => node.dataset.id)
.join(", ");
if (nodeInfo) {
addLog(`Added element(s): ${nodeInfo}`, "add");
checkChallenges();
}
}
// Removed nodes
if (mutation.removedNodes.length > 0) {
state.stats.removals += mutation.removedNodes.length;
state.stats.elements = elements.domContainer.querySelectorAll(
".element"
).length;
updateStats();
const nodeInfo = Array.from(mutation.removedNodes)
.filter((node) => node.classList?.contains("element"))
.map((node) => node.dataset.id)
.join(", ");
if (nodeInfo) {
addLog(`Removed element(s): ${nodeInfo}`, "remove");
checkChallenges();
}
}
};
const handleAttributeMutation = (mutation) => {
state.stats.attributes++;
updateStats();
const target = mutation.target.dataset.id;
addLog(`Changed ${mutation.attributeName} on element ${target}`, "attr");
checkChallenges();
};
const handleCharacterDataMutation = (mutation) => {
state.stats.texts++;
updateStats();
// Find closest element with data-id
let node = mutation.target;
while (node && !node.dataset?.id) {
node = node.parentNode;
}
const id = node?.dataset.id || "unknown";
addLog(`Changed text content of element ${id}`, "text");
checkChallenges();
};
// DOM Manipulation Functions
const addElement = () => {
const id = `elem-${Date.now()}`;
const element = document.createElement("div");
element.className = "element";
element.dataset.id = id;
element.textContent = `Element ${id.split("-")[1]}`;
// Add click handler to change color
element.addEventListener("click", () => changeElementColor(element));
elements.domContainer.appendChild(element);
// Only log when observer is inactive, don't update stats
if (!state.observing) {
addLog(
`Added element: ${id} (Observer inactive - changes not tracked!)`,
"info"
);
}
};
const removeElement = () => {
const elementsList = elements.domContainer.querySelectorAll(".element");
if (elementsList.length <= 1) return; // Keep at least one element
const randomIndex = Math.floor(Math.random() * elementsList.length);
const element = elementsList[randomIndex];
const id = element.dataset.id;
element.remove();
// Only log when observer is inactive, don't update stats
if (!state.observing) {
addLog(
`Removed element: ${id} (Observer inactive - changes not tracked!)`,
"info"
);
}
};
const changeColor = () => {
const element = getRandomElement();
if (element) {
changeElementColor(element);
}
};
const changeElementColor = (element) => {
element.style.backgroundColor = getRandomColor();
// Only log when observer is inactive, don't update stats
if (!state.observing) {
addLog(
`Changed background color of element ${element.dataset.id} (Observer inactive - changes not tracked!)`,
"info"
);
}
};
const changeText = () => {
const element = getRandomElement();
if (!element) return;
const id = element.dataset.id;
// This is the key fix: modify the text node's data property directly
// instead of using textContent, which doesn't always trigger characterData mutations
if (
element.childNodes.length > 0 &&
element.firstChild.nodeType === Node.TEXT_NODE
) {
// If there's already a text node, modify it
element.firstChild.nodeValue = `Changed at ${new Date().toLocaleTimeString()}`;
} else {
// Otherwise create a new text node
while (element.firstChild) element.removeChild(element.firstChild);
element.appendChild(
document.createTextNode(`Changed at ${new Date().toLocaleTimeString()}`)
);
}
// Only log when observer is inactive, don't update stats
if (!state.observing) {
addLog(
`Changed text content of element ${id} (Observer inactive - changes not tracked!)`,
"info"
);
}
};
const clearLog = () => {
elements.logContainer.innerHTML = "";
addLog("Log cleared", "info");
};
// Event listeners
const setupEventListeners = () => {
elements.startBtn.addEventListener("click", startObserving);
elements.stopBtn.addEventListener("click", stopObserving);
elements.addBtn.addEventListener("click", addElement);
elements.removeBtn.addEventListener("click", removeElement);
elements.colorBtn.addEventListener("click", changeColor);
elements.textBtn.addEventListener("click", changeText);
elements.clearLogBtn.addEventListener("click", clearLog);
};
// Initialize
const init = () => {
elements.stopBtn.disabled = true;
elements.domContainer.classList.add("dom-container-inactive");
// Make sure characterData and subtree are checked by default
elements.characterDataOpt.checked = true;
elements.subtreeOpt.checked = true;
addLog(
'Welcome to the MutationObserver Magic Show! Click "Start Observing" to begin.',
"info"
);
addLog("Tip: Click directly on elements to change their color!", "info");
setupEventListeners();
};
// Start the app
init();
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.