ใช้ Event Delegation บน document ตรวจสอบว่า Click target ไม่ใช่ Element ที่ต้องการ แล้วซ่อนมัน
ตัวอย่างจากบทความ
<!DOCTYPE html>
<html>
<body>
<script>
window.onload = function(){
var hideMe = document.getElementById('hideMe');
document.onclick = function(e){
if(e.target.id !== 'hideMe'){
hideMe.style.display = 'none';
}
};
};
</script>
<div id="hideMe">คลิกนอกพื้นที่นี้เพื่อซ่อน</div>
</body>
</html>
ปัญหาของตัวอย่างต้นฉบับ
- ใช้
document.onclickทับ handler อื่นได้ - ไม่ตรวจสอบ child elements (คลิกลูกก็ซ่อน)
- ไม่มีการ cleanup
วิธีที่ดีกว่า (Modern Approach)
1. ใช้ addEventListener + contains()
const dropdown = document.getElementById('dropdown');
document.addEventListener('click', function(event) {
// ถ้าคลิกนอก dropdown และไม่ใช่ dropdown เอง
if (!dropdown.contains(event.target)) {
dropdown.style.display = 'none';
}
});
2. รองรับหลาย Element (Dropdown หลายตัว)
function setupClickOutside(selector, callback) {
document.addEventListener('click', function(event) {
const elements = document.querySelectorAll(selector);
let clickedInside = false;
elements.forEach(el => {
if (el.contains(event.target)) {
clickedInside = true;
}
});
if (!clickedInside) {
callback(event);
}
});
}
// ใช้งาน
setupClickOutside('.dropdown-menu', () => {
document.querySelectorAll('.dropdown-menu').forEach(el => {
el.style.display = 'none';
});
});
3. ใช้ Custom Hook (React)
// useClickOutside.js
import { useEffect } from 'react';
import { useRef } from 'react';
export function useClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
// ใช้งาน
function Dropdown() {
const ref = useRef(null);
const [open, setOpen] = useState(false);
useClickOutside(ref, () => setOpen(false));
return (
{open && Content}
);
}
4. ใช้ Focus Management (Accessibility)
// สำหรับ Modal/Dialog - ใช้ focus trap
function trapFocus(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusableElements[0];
const last = focusableElements[focusableElements.length - 1];
element.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
}
Event Target vs CurrentTarget
document.addEventListener('click', (e) => {
e.target; // Element ที่ถูกคลิกจริง (ลึกที่สุด)
e.currentTarget; // Element ที่ attach listener (document)
this; // เหมือน currentTarget (ใน regular function)
});
สรุป Best Practices
- ใช้
addEventListenerแทนonclickproperty - ใช้
element.contains(event.target)ตรวจสอบการคลิกภายใน - Cleanup listener เมื่อไม่ต้องการ (return function ใน useEffect/componentWillUnmount)
- พิจารณา Accessibility (Focus trap, ARIA attributes)
- ใช้
mousedownหรือtouchstartนอกจากclickสำหรับ mobile