A lightweight JavaScript library to monitor and track changes in HTML forms. FormWatcher intelligently detects added, removed, and modified form fields, with support for dynamic form changes and real-time updates.
FormWatcher provides a simple yet powerful way to track form state changes in your web applications. It's perfect for:
- Detecting unsaved changes before users navigate away
- Implementing auto-save functionality with change detection
- Tracking form field modifications for audit trails
- Handling dynamic forms where fields are added/removed at runtime
- Building form validation workflows based on change states
- ✅ Real-time change detection - Tracks input, select, textarea, checkbox, and radio button changes
- ✅ Dynamic field support - Automatically detects fields added or removed from the DOM
- ✅ Flexible field exclusion - Exclude specific fields from tracking using CSS selectors
- ✅ Debounced updates - Configurable debounce delay to optimize performance
- ✅ Comprehensive change tracking - Identifies added, removed, modified, and re-added fields
- ✅ Callback support - React to form changes with custom callbacks
- ✅ Zero dependencies - Pure JavaScript with no external dependencies
- ✅ Lightweight - Minimal footprint for optimal performance
FormWatcher automatically tracks form fields by assigning unique identifiers to each field. If a field doesn't have an id attribute, FormWatcher will add a data-form-watcher-id attribute to the field's DOM element. This allows FormWatcher to:
- Track fields even if they don't have an
idattribute - Maintain field identity even when the field is dynamically removed and re-added to the DOM
- Provide consistent tracking across page lifecycle
Example of automatic ID assignment:
<!-- Before FormWatcher initialization -->
<input type="text" name="username" value="john">
<!-- After FormWatcher initialization -->
<input type="text" name="username" value="john" data-form-watcher-id="username-550e8400-e29b-41d4-a716-446655440000">You can also manually set data-form-watcher-id on fields if you want to control their identifiers.
Install via npm:
npm install @etoundi1er/form-watcherOr using yarn:
yarn add @etoundi1er/form-watcherIf you're not using a bundler, you can use the browser-compatible UMD build:
<!-- Load from local file -->
<script src="./form-watcher.browser.js"></script>
<!-- Or from CDN (when published) -->
<script src="https://unpkg.com/@etoundi1er/form-watcher/form-watcher.browser.js"></script>
<script>
// FormWatcher is now available as a global variable
const watcher = new FormWatcher('#myForm', {
onFormChanged: (hasChanges, changes, event) => {
console.log('Form changed:', hasChanges);
}
});
</script>import FormWatcher from '@etoundi1er/form-watcher';
// Initialize with a CSS selector
const watcher = new FormWatcher('#myForm');
// Check if form has changes
const hasChanges = watcher.checkForChanges();
console.log('Has changes:', hasChanges);
// Get detailed changes
const changes = watcher.getChanges();
console.log('Changes:', changes);const watcher = new FormWatcher('#myForm', {
debounceDelay: 500, // Wait 500ms after last change before checking
excludeSelectors: ['#password', '.ignore-field', '[data-no-track]'], // Exclude specific fields
onFormChanged: (hasChanges, changes, event) => {
// Callback invoked whenever form changes are detected
console.log('Form changed:', hasChanges);
console.log('Details:', changes);
console.log('Triggered by:', event?.target);
// Enable/disable save button based on changes
document.getElementById('saveBtn').disabled = !hasChanges;
}
});Note: You can use any CSS selector in excludeSelectors, including:
- ID selectors:
#password - Class selectors:
.ignore-field - Attribute selectors:
[data-no-track],[data-form-watcher-id="specific-id"] - Type selectors:
input[type="hidden"]
const formElement = document.querySelector('#myForm');
const watcher = new FormWatcher(formElement, {
onFormChanged: (hasChanges, changes, event) => {
if (hasChanges) {
console.log('Modified fields:', changes.modified.length);
console.log('Added fields:', changes.added.length);
console.log('Removed fields:', changes.removed.length);
console.log('Changed field:', event?.target?.name);
}
}
});const watcher = new FormWatcher('#myForm', {
onFormChanged: (hasChanges) => {
// Warn user before leaving page with unsaved changes
window.onbeforeunload = hasChanges ? () => {
return 'You have unsaved changes. Are you sure you want to leave?';
} : null;
}
});
// Reset state after saving
document.getElementById('saveBtn').addEventListener('click', async () => {
await saveFormData();
watcher.resetState(); // Mark current state as "original"
});The onFormChanged callback receives the event that triggered the change, allowing you to access detailed information about what caused the update:
const watcher = new FormWatcher('#myForm', {
onFormChanged: (hasChanges, changes, event) => {
if (event) {
// Access the field that triggered the change
const field = event.target;
console.log('Field name:', field.name);
console.log('Field type:', field.type);
console.log('New value:', field.value);
// Show a toast notification
if (hasChanges) {
showToast(`Field "${field.name}" was modified`);
}
} else {
// Event is null when changes are detected programmatically
// (e.g., via MutationObserver or resetState)
console.log('Changes detected programmatically');
}
}
});Note: The event parameter may be null when changes are detected through non-user interactions, such as:
- Fields dynamically added/removed via the MutationObserver
- Programmatic calls to
resetState() - Initial state setup
Always use optional chaining (event?.target) when accessing the event object.
const watcher = new FormWatcher('#dynamicForm');
// FormWatcher automatically detects when fields are added
document.getElementById('addField').addEventListener('click', () => {
const newField = document.createElement('input');
newField.type = 'text';
newField.name = 'dynamicField';
document.getElementById('dynamicForm').appendChild(newField);
// FormWatcher will automatically track this new field
});
// Or when fields are removed
document.getElementById('removeField').addEventListener('click', () => {
const field = document.querySelector('[name="dynamicField"]');
field?.remove();
// FormWatcher will detect the removal
});new FormWatcher(formElementOrSelector, options)Parameters:
formElementOrSelector(string | HTMLElement) - CSS selector string or DOM element of the form to trackoptions(Object) - Optional configuration objectdebounceDelay(number) - Debounce delay in milliseconds (default: 300)excludeSelectors(Array) - Array of CSS selectors for fields to exclude from tracking (default: [])onFormChanged(Function) - Callback function invoked when changes are detected (default: null)- Parameters:
(hasChanges: boolean, changes: Object, event: Event | null) hasChanges- Whether the form has changes compared to its original statechanges- Object containing detailed information about added, removed, modified, and re-added fieldsevent- The DOM event that triggered the change (may benullfor programmatic changes)
- Parameters:
Compares the current form state with the original state and returns whether changes exist.
Returns: boolean - True if changes are detected, false otherwise
const hasChanges = watcher.checkForChanges();Returns a detailed object containing all changes categorized by type.
Returns: Object with the following structure:
{
added: [], // Fields added after initialization
removed: [], // Fields removed from the form
modified: [], // Fields with changed values
reAdded: [] // Fields that were removed and then added back
}Example:
const changes = watcher.getChanges();
changes.modified.forEach(field => {
console.log(`Field ${field.id} changed from "${field.original.value}" to "${field.current.value}"`);
// Access the DOM element directly
if (field.element) {
field.element.classList.add('modified');
}
});
// Highlight all added fields
changes.added.forEach(field => {
if (field.element) {
field.element.style.border = '2px solid green';
}
});Resets the original state to match the current state. Useful after saving form data.
// After successfully saving
await saveForm();
watcher.resetState(); // Current state becomes the new baseline{
id: 'field-id',
element: HTMLElement, // The DOM node reference
original: { value: 'old value', checked: false },
current: { value: 'new value', checked: true }
}{
id: 'field-id',
element: HTMLElement, // The DOM node reference
value: 'current value',
checked: true // for checkboxes/radios
}{
id: 'field-id',
element: HTMLElement, // The DOM node reference (if still accessible)
value: 'last known value',
checked: false // for checkboxes/radios
}{
id: 'field-id',
element: HTMLElement, // The DOM node reference
value: 'current value',
checked: true,
previousValue: 'value before removal',
previousChecked: false
}FormWatcher uses modern JavaScript features and browser APIs. It is compatible with:
| Browser | Version |
|---|---|
| Chrome | ≥ 51 |
| Firefox | ≥ 54 |
| Safari | ≥ 10 |
| Edge | ≥ 15 |
| Opera | ≥ 38 |
Required Browser Features:
- ES6 Classes
- Map data structure
- MutationObserver API
- Dataset API
- querySelector/querySelectorAll
For older browser support, you may need to include polyfills for:
Map- core-jsMutationObserver- mutation-observer
The browser-compatible version (form-watcher.browser.js) is compiled from index.js:
# Install dependencies (if any)
npm install
# Build browser version
npm run build:browserThis generates a UMD build that works in browsers, AMD, and CommonJS environments.
Note: The browser build is automatically generated before publishing to npm via the prebuild script.
MIT License
Copyright (c) 2025 Frank Etoundi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Contributions are welcome! Please feel free to submit a Pull Request.
Found a bug or have a feature request? Please create an issue at: https://github.com/etoundi1er/form_watcher/issues
Frank Etoundi
- Website: https://www.etoundi.com/
- Email: frank.etoundi@gmail.com
- GitHub: @etoundi1er