[ PROMPT_NODE_25273 ]
Event Handlers
[ SKILL_DOCUMENTATION ]
# Event Handler TypeScript Patterns
Proper event typing ensures type-safe access to event properties and target elements.
## Mouse Events
```typescript
// Click events
function handleClick(event: React.MouseEvent) {
event.preventDefault();
event.stopPropagation();
// Target element typed correctly
event.currentTarget.disabled = true;
event.currentTarget.textContent = 'Clicked';
// Mouse position
console.log(event.clientX, event.clientY);
console.log(event.pageX, event.pageY);
// Mouse buttons
console.log(event.button); // 0 = left, 1 = middle, 2 = right
console.log(event.buttons); // Bitmask of pressed buttons
// Modifier keys
if (event.ctrlKey || event.metaKey) {
console.log('Ctrl/Cmd + Click');
}
if (event.shiftKey) {
console.log('Shift + Click');
}
if (event.altKey) {
console.log('Alt + Click');
}
}
// Mouse movement
function handleMouseMove(event: React.MouseEvent) {
const rect = event.currentTarget.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
console.log(`Position in element: ${x}, ${y}`);
}
// Hover events
function handleMouseEnter(event: React.MouseEvent) {
event.currentTarget.style.backgroundColor = 'lightblue';
}
function handleMouseLeave(event: React.MouseEvent) {
event.currentTarget.style.backgroundColor = '';
}
// Double click
function handleDoubleClick(event: React.MouseEvent) {
console.log('Double clicked');
}
```
## Form Events
```typescript
// Form submission
function handleSubmit(event: React.FormEvent) {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
const data = {
name: formData.get('name') as string,
email: formData.get('email') as string,
};
console.log(data);
}
// Input change
function handleChange(event: React.ChangeEvent) {
const target = event.target;
// For text inputs
if (target.type === 'text' || target.type === 'email') {
console.log(target.value); // string
}
// For checkboxes
if (target.type === 'checkbox') {
console.log(target.checked); // boolean
}
// For radio buttons
if (target.type === 'radio') {
console.log(target.value, target.checked);
}
// For number inputs
if (target.type === 'number') {
console.log(target.valueAsNumber); // number
}
// For file inputs
if (target.type === 'file') {
const files = target.files; // FileList | null
if (files && files.length > 0) {
console.log(files[0].name);
}
}
}
// Textarea change
function handleTextareaChange(event: React.ChangeEvent) {
console.log(event.target.value);
console.log(event.target.selectionStart); // Cursor position
}
// Select change
function handleSelectChange(event: React.ChangeEvent) {
const value = event.target.value;
const selectedIndex = event.target.selectedIndex;
const selectedOption = event.target.options[selectedIndex];
console.log(value, selectedOption.text);
}
// Input events (fires on every keystroke)
function handleInput(event: React.FormEvent) {
console.log(event.currentTarget.value);
}
// Reset event
function handleReset(event: React.FormEvent) {
event.preventDefault();
console.log('Form reset');
}
```
## Keyboard Events
```typescript
function handleKeyDown(event: React.KeyboardEvent) {
// Key identification
console.log(event.key); // 'Enter', 'Escape', 'a', etc.
console.log(event.code); // 'Enter', 'Escape', 'KeyA', etc.
// Common patterns
if (event.key === 'Enter') {
event.preventDefault();
console.log('Enter pressed');
}
if (event.key === 'Escape') {
event.currentTarget.blur();
}
// Arrow keys
if (event.key === 'ArrowUp') {
event.preventDefault();
// Navigate up
}
// Modifier keys
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
console.log('Ctrl+S - Save');
}
if (event.metaKey && event.key === 'k') {
event.preventDefault();
console.log('Cmd+K - Search');
}
// Check multiple modifiers
if (event.ctrlKey && event.shiftKey && event.key === 'P') {
event.preventDefault();
console.log('Ctrl+Shift+P - Command palette');
}
// Key combinations
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
console.log('Submit with Ctrl/Cmd+Enter');
}
}
function handleKeyUp(event: React.KeyboardEvent) {
console.log('Key released:', event.key);
}
function handleKeyPress(event: React.KeyboardEvent) {
// Deprecated - use keyDown instead
console.log('Key pressed:', event.key);
}
```
## Focus Events
```typescript
function handleFocus(event: React.FocusEvent) {
// Select all text on focus
event.target.select();
// Add visual indicator
event.currentTarget.classList.add('focused');
}
function handleBlur(event: React.FocusEvent) {
// Validate on blur
const value = event.target.value;
if (value === '') {
event.currentTarget.classList.add('error');
}
// Remove visual indicator
event.currentTarget.classList.remove('focused');
}
// Focus within
function handleFocusWithin(event: React.FocusEvent) {
// Related target - element receiving focus
const relatedTarget = event.relatedTarget as HTMLElement | null;
console.log('Focus moved from:', relatedTarget);
}
```
## Drag and Drop Events
```typescript
function handleDragStart(event: React.DragEvent) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', event.currentTarget.id);
// Custom drag image
const dragImage = document.createElement('div');
dragImage.textContent = 'Dragging...';
event.dataTransfer.setDragImage(dragImage, 0, 0);
}
function handleDragOver(event: React.DragEvent) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
event.currentTarget.classList.add('drag-over');
}
function handleDragLeave(event: React.DragEvent) {
event.currentTarget.classList.remove('drag-over');
}
function handleDrop(event: React.DragEvent) {
event.preventDefault();
event.currentTarget.classList.remove('drag-over');
const data = event.dataTransfer.getData('text/plain');
console.log('Dropped:', data);
// Handle files
const files = event.dataTransfer.files;
if (files.length > 0) {
Array.from(files).forEach((file) => {
console.log(file.name, file.type, file.size);
});
}
}
function handleDragEnd(event: React.DragEvent) {
console.log('Drag ended');
}
```
## Clipboard Events
```typescript
function handleCopy(event: React.ClipboardEvent) {
event.preventDefault();
// Custom copy behavior
const selection = window.getSelection();
if (selection) {
const text = `Copied from app: ${selection.toString()}`;
event.clipboardData.setData('text/plain', text);
}
}
function handleCut(event: React.ClipboardEvent) {
console.log('Cut:', event.currentTarget.value);
}
function handlePaste(event: React.ClipboardEvent) {
event.preventDefault();
const pastedText = event.clipboardData.getData('text/plain');
console.log('Pasted:', pastedText);
// Validate pasted content
const sanitized = pastedText.replace(/[^a-zA-Z0-9]/g, '');
event.currentTarget.value = sanitized;
}
```
## Composition Events (IME)
```typescript
// For international input methods (Chinese, Japanese, etc.)
function handleCompositionStart(event: React.CompositionEvent) {
console.log('Composition started');
}
function handleCompositionUpdate(event: React.CompositionEvent) {
console.log('Composing:', event.data);
}
function handleCompositionEnd(event: React.CompositionEvent) {
console.log('Composition ended:', event.data);
}
```
## Touch Events
```typescript
function handleTouchStart(event: React.TouchEvent) {
const touch = event.touches[0];
console.log('Touch start:', touch.clientX, touch.clientY);
}
function handleTouchMove(event: React.TouchEvent) {
event.preventDefault(); // Prevent scrolling
const touch = event.touches[0];
console.log('Touch move:', touch.clientX, touch.clientY);
}
function handleTouchEnd(event: React.TouchEvent) {
console.log('Touch ended');
}
// Multi-touch
function handleMultiTouch(event: React.TouchEvent) {
if (event.touches.length === 2) {
const [touch1, touch2] = event.touches;
const distance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
console.log('Pinch distance:', distance);
}
}
```
## Wheel Events
```typescript
function handleWheel(event: React.WheelEvent) {
// Prevent default scroll
event.preventDefault();
// Scroll delta
console.log('Delta X:', event.deltaX);
console.log('Delta Y:', event.deltaY);
console.log('Delta Z:', event.deltaZ);
// Delta mode (0 = pixels, 1 = lines, 2 = pages)
console.log('Delta mode:', event.deltaMode);
// Zoom with Ctrl+Wheel
if (event.ctrlKey) {
const zoomDelta = event.deltaY > 0 ? -0.1 : 0.1;
console.log('Zoom:', zoomDelta);
}
}
```
## Generic Event Handlers
Reusable handlers with proper typing.
```typescript
// Generic change handler
function createChangeHandler(
callback: (value: string) => void
) {
return (event: React.ChangeEvent) => {
if ('value' in event.target) {
callback(event.target.value);
}
};
}
// Usage
const handleNameChange = createChangeHandler((value) => {
setName(value);
});
const handleBioChange = createChangeHandler((value) => {
setBio(value);
});
// Generic click handler with target validation
function createClickHandler(
selector: string,
callback: (element: T) => void
) {
return (event: React.MouseEvent) => {
const target = event.target as HTMLElement;
const match = target.closest(selector);
if (match) {
callback(match as T);
}
};
}
// Usage
const handleItemClick = createClickHandler('li[data-id]', (item) => {
const id = item.dataset.id;
console.log('Clicked item:', id);
});
```
## Event Handler Type Aliases
```typescript
// Create reusable type aliases
type InputChangeHandler = React.ChangeEventHandler;
type ButtonClickHandler = React.MouseEventHandler;
type FormSubmitHandler = React.FormEventHandler;
// Usage
const handleChange: InputChangeHandler = (event) => {
console.log(event.target.value);
};
const handleClick: ButtonClickHandler = (event) => {
event.currentTarget.disabled = true;
};
const handleSubmit: FormSubmitHandler = (event) => {
event.preventDefault();
};
// Generic handler type
type EventHandler = (
event: Evt & { currentTarget: E }
) => void;
// Usage
const handleInput: EventHandler<HTMLInputElement, React.ChangeEvent> = (
event
) => {
console.log(event.currentTarget.value);
};
```
## Delegated Event Handlers
Type-safe event delegation.
```typescript
function ListContainer() {
const handleListClick = (event: React.MouseEvent) => {
const target = event.target as HTMLElement;
// Find clicked list item
const listItem = target.closest('li');
if (!listItem) return;
// Type guard for data attributes
const itemId = listItem.getAttribute('data-id');
if (itemId) {
console.log('Clicked item:', itemId);
}
// Handle button within list item
if (target.matches('button.delete')) {
event.stopPropagation();
console.log('Delete clicked');
}
};
return (
- Item 1
- Item 2
Click me
;
}
```
## Custom Events
```typescript
// Define custom event type
type CustomEventMap = {
'user:login': CustomEvent;
'user:logout': CustomEvent;
};
// Typed custom event dispatcher
function dispatchCustomEvent(
element: HTMLElement,
type: K,
detail?: CustomEventMap[K]['detail']
) {
const event = new CustomEvent(type, { detail, bubbles: true });
element.dispatchEvent(event);
}
// Component using custom events
function UserButton() {
const ref = useRef(null);
const handleLogin = () => {
if (ref.current) {
dispatchCustomEvent(ref.current, 'user:login', { userId: '123' });
}
};
useEffect(() => {
const element = ref.current;
if (!element) return;
const handleCustomLogin = (event: Event) => {
const customEvent = event as CustomEvent;
console.log('User logged in:', customEvent.detail.userId);
};
element.addEventListener('user:login', handleCustomLogin);
return () => {
element.removeEventListener('user:login', handleCustomLogin);
};
}, []);
return ;
}
```
Source: claude-code-templates (MIT). See About Us for full credits.