diff --git a/UNSAVED_CHANGES_POC.md b/UNSAVED_CHANGES_POC.md
new file mode 100644
index 0000000..f88af8d
--- /dev/null
+++ b/UNSAVED_CHANGES_POC.md
@@ -0,0 +1,338 @@
+# Unsaved Form Changes Warning - Proof of Concept
+
+**Related Issue:** [#2 - JavaScript - show alert before leaving page](https://github.com/contributte/forms/issues/2)
+
+This PoC demonstrates a feature that warns users when they attempt to navigate away from a page with unsaved form changes. The implementation is based on the browser's `beforeunload` event and works with both vanilla HTML forms and Nette Forms.
+
+## Features
+
+- ✅ **Automatic change detection** - Tracks all form field changes in real-time
+- ✅ **Browser confirmation dialog** - Shows native browser warning before leaving
+- ✅ **Multiple initialization methods** - Declarative (HTML attributes) or programmatic (JavaScript)
+- ✅ **Nette Forms integration** - Easy-to-use PHP helper class
+- ✅ **Customizable messages** - Set custom warning text
+- ✅ **Smart form submission** - Automatically clears warning on form submit
+- ✅ **Debug mode** - Console logging for development
+- ✅ **Zero dependencies** - Pure vanilla JavaScript, no jQuery required
+- ✅ **Framework agnostic** - Works with any form, not just Nette
+
+## Files Added
+
+```
+assets/
+ └── unsaved-changes.js # Core JavaScript tracker
+
+src/Controls/
+ └── UnsavedChangesControl.php # PHP helper for Nette Forms
+
+examples/
+ ├── unsaved-changes-demo.html # HTML demo with multiple examples
+ └── unsaved-changes-nette.php # Nette Forms integration demo
+```
+
+## Usage
+
+### 1. HTML Forms (Declarative)
+
+The simplest way to enable the warning is using a data attribute:
+
+```html
+
+
+
+```
+
+**With debug mode:**
+
+```html
+
+```
+
+### 2. JavaScript (Programmatic)
+
+For more control, initialize the tracker manually:
+
+```javascript
+var tracker = new UnsavedChangesTracker('#myForm', {
+ message: 'You have unsaved changes. Are you sure you want to leave?',
+ debug: true,
+ trackInputs: true,
+ trackTextareas: true,
+ trackSelects: true,
+ trackCheckboxes: true,
+ trackRadios: true,
+ resetOnSubmit: true
+});
+
+// Check if form is dirty
+if (tracker.isDirty) {
+ console.log('Form has unsaved changes');
+}
+
+// Manually reset the tracker
+tracker.reset();
+
+// Manually mark as dirty
+tracker.setDirty(true);
+
+// Destroy the tracker
+tracker.destroy();
+```
+
+### 3. Nette Forms (PHP)
+
+Use the `UnsavedChangesControl` helper class:
+
+```php
+use Nette\Forms\Form;
+use Contributte\Forms\Controls\UnsavedChangesControl;
+use Contributte\Forms\Rendering\Bootstrap5VerticalRenderer;
+
+$form = new Form();
+$form->addText('name', 'Name:');
+$form->addEmail('email', 'Email:');
+$form->addSubmit('submit', 'Submit');
+
+// Enable unsaved changes warning (default message)
+UnsavedChangesControl::enable($form);
+
+// Or with custom message
+UnsavedChangesControl::enable($form, 'You have unsaved changes!');
+
+// With debug mode
+UnsavedChangesControl::enable($form, 'Custom message', debug: true);
+
+// With advanced options
+UnsavedChangesControl::enableWithOptions($form, [
+ 'message' => 'Unsaved changes detected!',
+ 'debug' => true,
+]);
+
+// Disable the warning
+UnsavedChangesControl::disable($form);
+
+// Render form (works with any renderer)
+$form->setRenderer(new Bootstrap5VerticalRenderer());
+echo $form;
+```
+
+## Configuration Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `message` | string | "You have unsaved changes..." | Warning message shown to user |
+| `trackInputs` | boolean | `true` | Track text input fields |
+| `trackTextareas` | boolean | `true` | Track textarea fields |
+| `trackSelects` | boolean | `true` | Track select dropdowns |
+| `trackCheckboxes` | boolean | `true` | Track checkboxes |
+| `trackRadios` | boolean | `true` | Track radio buttons |
+| `excludeSelectors` | array | `[]` | Array of CSS selectors to exclude |
+| `resetOnSubmit` | boolean | `true` | Clear warning on form submit |
+| `debug` | boolean | `false` | Enable console logging |
+
+## How It Works
+
+1. **Initial State** - When initialized, the tracker stores the current value of all tracked form fields
+2. **Change Detection** - Listens to `input` and `change` events on form fields
+3. **Dirty Flag** - Sets an internal `isDirty` flag when any field value differs from its initial value
+4. **beforeunload Handler** - Attaches a `beforeunload` event listener that shows a confirmation dialog if the form is dirty
+5. **Form Submit** - On form submission, the dirty flag is cleared to allow navigation without warning
+
+## Browser Compatibility
+
+The `beforeunload` event is supported in all modern browsers:
+
+- ✅ Chrome/Edge 1+
+- ✅ Firefox 1+
+- ✅ Safari 3+
+- ✅ Opera 12+
+- ✅ Internet Explorer 4+
+
+**Note:** Modern browsers (Chrome 51+, Firefox 44+) ignore custom messages in the confirmation dialog for security reasons. They display a generic browser-provided message instead. However, the feature still works correctly.
+
+## Testing the PoC
+
+### Option 1: HTML Demo
+
+1. Open `examples/unsaved-changes-demo.html` in your browser
+2. Fill out any form on the page
+3. Try to close the tab or navigate away
+4. You'll see a browser confirmation dialog
+
+### Option 2: Nette Forms Demo
+
+1. Make sure composer dependencies are installed:
+ ```bash
+ composer install
+ ```
+
+2. Start a PHP development server:
+ ```bash
+ php -S localhost:8000 -t examples/
+ ```
+
+3. Open `http://localhost:8000/unsaved-changes-nette.php` in your browser
+
+4. Fill out the form and try to navigate away
+
+### Option 3: Integration Test
+
+Create your own test file:
+
+```html
+
+
+
+ Test
+
+
+
+
+
+
+```
+
+## API Reference
+
+### Constructor
+
+```javascript
+new UnsavedChangesTracker(form, options)
+```
+
+- `form` - HTMLFormElement or CSS selector string
+- `options` - Configuration object (see Configuration Options above)
+
+### Methods
+
+#### `reset()`
+Resets the tracker and marks the form as clean.
+
+```javascript
+tracker.reset();
+```
+
+#### `setDirty(dirty)`
+Manually set the dirty flag.
+
+```javascript
+tracker.setDirty(true); // Mark as dirty
+tracker.setDirty(false); // Mark as clean
+```
+
+#### `isDirtyForm()`
+Returns whether the form has unsaved changes.
+
+```javascript
+if (tracker.isDirtyForm()) {
+ console.log('Form has changes');
+}
+```
+
+#### `checkChanges()`
+Manually check if form has changes and update the dirty flag.
+
+```javascript
+var hasChanges = tracker.checkChanges();
+```
+
+#### `destroy()`
+Destroys the tracker and cleans up.
+
+```javascript
+tracker.destroy();
+```
+
+### Properties
+
+#### `isDirty`
+Boolean flag indicating whether the form has unsaved changes.
+
+```javascript
+console.log(tracker.isDirty); // true or false
+```
+
+## Integration with Existing Projects
+
+### Adding to Your Project
+
+1. **Copy the JavaScript file:**
+ ```bash
+ cp assets/unsaved-changes.js public/js/
+ ```
+
+2. **Include in your layout/template:**
+ ```html
+
+ ```
+
+3. **For Nette projects, copy the PHP helper:**
+ ```bash
+ cp src/Controls/UnsavedChangesControl.php your-project/src/Forms/
+ ```
+
+### With Asset Management
+
+If using Webpack, Vite, or similar:
+
+```javascript
+import UnsavedChangesTracker from './unsaved-changes.js';
+
+const tracker = new UnsavedChangesTracker('#myForm', {
+ message: 'You have unsaved changes!'
+});
+```
+
+## Potential Improvements
+
+This is a PoC. For production use, consider:
+
+- [ ] **NPM package** - Publish as a standalone package
+- [ ] **TypeScript version** - Add type definitions
+- [ ] **Framework integrations** - React, Vue, Angular adapters
+- [ ] **Advanced options** - Exclude specific fields, custom validators
+- [ ] **Callbacks** - `onDirty`, `onChange`, `onBeforeUnload` hooks
+- [ ] **AJAX forms** - Integration with AJAX submissions
+- [ ] **Multiple forms** - Support tracking multiple forms independently
+- [ ] **localStorage** - Auto-save drafts to localStorage
+- [ ] **Visual indicator** - Show a visual "unsaved changes" badge
+- [ ] **Unit tests** - Automated testing suite
+- [ ] **Minified version** - Production-ready minified build
+
+## Known Limitations
+
+1. **Custom message ignored** - Modern browsers show a generic message for security
+2. **No mobile support** - Mobile browsers don't support `beforeunload` reliably
+3. **SPA routing** - Doesn't work with client-side routing (React Router, Vue Router, etc.) without additional setup
+4. **File inputs** - File input changes are tracked but the actual file isn't stored
+5. **Dynamic forms** - Fields added after initialization aren't automatically tracked
+
+## Security Considerations
+
+- ✅ **No data storage** - Doesn't store or transmit form data
+- ✅ **Client-side only** - All tracking happens in the browser
+- ✅ **No external dependencies** - No third-party scripts loaded
+- ✅ **XSS safe** - Custom messages are not rendered as HTML
+
+## License
+
+MIT (same as contributte/forms)
+
+## Contributing
+
+This is a proof of concept for issue #2. Feedback and suggestions are welcome!
+
+## Related Resources
+
+- [MDN: beforeunload event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event)
+- [Nette Forms Documentation](https://doc.nette.org/en/forms)
+- [Contributte Forms Documentation](https://contributte.org/packages/contributte/forms.html)
diff --git a/assets/unsaved-changes.js b/assets/unsaved-changes.js
new file mode 100644
index 0000000..65d2b63
--- /dev/null
+++ b/assets/unsaved-changes.js
@@ -0,0 +1,302 @@
+/**
+ * Unsaved Form Changes Tracker
+ *
+ * Monitors form changes and displays a confirmation dialog when the user
+ * attempts to navigate away from the page with unsaved changes.
+ *
+ * @author Contributte Forms
+ * @license MIT
+ */
+(function() {
+ 'use strict';
+
+ /**
+ * UnsavedChangesTracker constructor
+ * @param {HTMLFormElement|string} form - Form element or selector
+ * @param {Object} options - Configuration options
+ */
+ function UnsavedChangesTracker(form, options) {
+ // Default options
+ this.options = Object.assign({
+ message: 'You have unsaved changes. Are you sure you want to leave?',
+ trackInputs: true,
+ trackTextareas: true,
+ trackSelects: true,
+ trackCheckboxes: true,
+ trackRadios: true,
+ excludeSelectors: [], // Array of selectors to exclude from tracking
+ resetOnSubmit: true,
+ debug: false
+ }, options || {});
+
+ // Get form element
+ if (typeof form === 'string') {
+ this.form = document.querySelector(form);
+ } else {
+ this.form = form;
+ }
+
+ if (!this.form) {
+ console.error('UnsavedChangesTracker: Form not found');
+ return;
+ }
+
+ this.isDirty = false;
+ this.initialValues = {};
+
+ this.init();
+ }
+
+ /**
+ * Initialize the tracker
+ */
+ UnsavedChangesTracker.prototype.init = function() {
+ this.log('Initializing tracker for form', this.form);
+
+ // Store initial values
+ this.storeInitialValues();
+
+ // Attach change listeners
+ this.attachListeners();
+
+ // Attach beforeunload handler
+ this.attachBeforeUnloadHandler();
+ };
+
+ /**
+ * Store initial form values
+ */
+ UnsavedChangesTracker.prototype.storeInitialValues = function() {
+ var self = this;
+ var elements = this.getTrackedElements();
+
+ elements.forEach(function(element) {
+ var key = self.getElementKey(element);
+ self.initialValues[key] = self.getElementValue(element);
+ });
+
+ this.log('Initial values stored:', this.initialValues);
+ };
+
+ /**
+ * Get all tracked form elements
+ */
+ UnsavedChangesTracker.prototype.getTrackedElements = function() {
+ var selectors = [];
+
+ if (this.options.trackInputs) {
+ selectors.push('input[type="text"]', 'input[type="email"]', 'input[type="password"]',
+ 'input[type="number"]', 'input[type="tel"]', 'input[type="url"]',
+ 'input[type="date"]', 'input[type="datetime-local"]', 'input[type="time"]',
+ 'input[type="search"]', 'input[type="color"]', 'input[type="hidden"]');
+ }
+
+ if (this.options.trackTextareas) {
+ selectors.push('textarea');
+ }
+
+ if (this.options.trackSelects) {
+ selectors.push('select');
+ }
+
+ if (this.options.trackCheckboxes) {
+ selectors.push('input[type="checkbox"]');
+ }
+
+ if (this.options.trackRadios) {
+ selectors.push('input[type="radio"]');
+ }
+
+ var elements = this.form.querySelectorAll(selectors.join(','));
+ var filtered = [];
+
+ // Filter out excluded elements
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ var excluded = false;
+
+ for (var j = 0; j < this.options.excludeSelectors.length; j++) {
+ if (element.matches(this.options.excludeSelectors[j])) {
+ excluded = true;
+ break;
+ }
+ }
+
+ if (!excluded) {
+ filtered.push(element);
+ }
+ }
+
+ return filtered;
+ };
+
+ /**
+ * Get unique key for an element
+ */
+ UnsavedChangesTracker.prototype.getElementKey = function(element) {
+ return element.name || element.id || 'element_' + Array.prototype.indexOf.call(this.form.elements, element);
+ };
+
+ /**
+ * Get element value
+ */
+ UnsavedChangesTracker.prototype.getElementValue = function(element) {
+ if (element.type === 'checkbox') {
+ return element.checked;
+ } else if (element.type === 'radio') {
+ var radios = this.form.querySelectorAll('input[name="' + element.name + '"]');
+ for (var i = 0; i < radios.length; i++) {
+ if (radios[i].checked) {
+ return radios[i].value;
+ }
+ }
+ return null;
+ } else {
+ return element.value;
+ }
+ };
+
+ /**
+ * Attach change listeners to form elements
+ */
+ UnsavedChangesTracker.prototype.attachListeners = function() {
+ var self = this;
+ var elements = this.getTrackedElements();
+
+ elements.forEach(function(element) {
+ // Use 'input' event for real-time tracking on text inputs
+ if (element.tagName === 'INPUT' &&
+ ['text', 'email', 'password', 'number', 'tel', 'url', 'search'].indexOf(element.type) !== -1) {
+ element.addEventListener('input', function() {
+ self.checkChanges();
+ });
+ }
+
+ // Use 'change' event for other elements
+ element.addEventListener('change', function() {
+ self.checkChanges();
+ });
+ });
+
+ // Handle form submit
+ if (this.options.resetOnSubmit) {
+ this.form.addEventListener('submit', function() {
+ self.log('Form submitted, resetting dirty flag');
+ self.reset();
+ });
+ }
+
+ this.log('Attached listeners to', elements.length, 'elements');
+ };
+
+ /**
+ * Check if form has changes
+ */
+ UnsavedChangesTracker.prototype.checkChanges = function() {
+ var self = this;
+ var elements = this.getTrackedElements();
+ var hasChanges = false;
+
+ elements.forEach(function(element) {
+ var key = self.getElementKey(element);
+ var currentValue = self.getElementValue(element);
+ var initialValue = self.initialValues[key];
+
+ if (currentValue !== initialValue) {
+ hasChanges = true;
+ }
+ });
+
+ this.isDirty = hasChanges;
+ this.log('Form dirty status:', this.isDirty);
+
+ return hasChanges;
+ };
+
+ /**
+ * Attach beforeunload handler
+ */
+ UnsavedChangesTracker.prototype.attachBeforeUnloadHandler = function() {
+ var self = this;
+
+ window.addEventListener('beforeunload', function(event) {
+ if (self.isDirty) {
+ self.log('Preventing navigation - unsaved changes detected');
+
+ // Modern browsers ignore custom messages, but we set it anyway
+ event.preventDefault();
+ event.returnValue = self.options.message;
+ return self.options.message;
+ }
+ });
+ };
+
+ /**
+ * Reset the tracker (mark as clean)
+ */
+ UnsavedChangesTracker.prototype.reset = function() {
+ this.isDirty = false;
+ this.storeInitialValues();
+ this.log('Tracker reset');
+ };
+
+ /**
+ * Mark form as dirty
+ */
+ UnsavedChangesTracker.prototype.setDirty = function(dirty) {
+ this.isDirty = dirty !== false;
+ this.log('Dirty flag manually set to:', this.isDirty);
+ };
+
+ /**
+ * Check if form is dirty
+ */
+ UnsavedChangesTracker.prototype.isDirtyForm = function() {
+ return this.isDirty;
+ };
+
+ /**
+ * Destroy the tracker
+ */
+ UnsavedChangesTracker.prototype.destroy = function() {
+ this.isDirty = false;
+ this.initialValues = {};
+ this.log('Tracker destroyed');
+ };
+
+ /**
+ * Debug logging
+ */
+ UnsavedChangesTracker.prototype.log = function() {
+ if (this.options.debug) {
+ console.log.apply(console, ['[UnsavedChangesTracker]'].concat(Array.prototype.slice.call(arguments)));
+ }
+ };
+
+ // Export to global scope
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = UnsavedChangesTracker;
+ } else {
+ window.UnsavedChangesTracker = UnsavedChangesTracker;
+ }
+
+ // Auto-initialize forms with data-unsaved-warning attribute
+ if (typeof document !== 'undefined') {
+ document.addEventListener('DOMContentLoaded', function() {
+ var forms = document.querySelectorAll('form[data-unsaved-warning]');
+
+ forms.forEach(function(form) {
+ var message = form.getAttribute('data-unsaved-warning');
+ var options = {
+ debug: form.hasAttribute('data-unsaved-debug')
+ };
+
+ if (message && message !== 'true') {
+ options.message = message;
+ }
+
+ new UnsavedChangesTracker(form, options);
+ });
+ });
+ }
+})();
diff --git a/examples/unsaved-changes-demo.html b/examples/unsaved-changes-demo.html
new file mode 100644
index 0000000..f1d915f
--- /dev/null
+++ b/examples/unsaved-changes-demo.html
@@ -0,0 +1,218 @@
+
+
+
+
+
+ Unsaved Changes Warning - Demo
+
+
+
+
+
+
Unsaved Form Changes Warning - PoC Demo
+
+
+
How to test:
+
+
Fill out any form below
+
Try to close the browser tab or navigate away
+
You'll see a browser confirmation dialog warning about unsaved changes
+
Submit the form to clear the warning
+
+
+
+
+
+
Example 1: Auto-initialization (Declarative)
+
This form uses data-unsaved-warning attribute for automatic initialization.
+
+
+
+
+
+
+
Example 2: Manual Initialization (Programmatic)
+
This form is initialized programmatically with custom options and status indicator.
+
+
+ Status: No unsaved changes
+
+
+
+
+
+
+
+
Example 3: Debug Mode
+
This form has debug mode enabled. Open your browser console to see tracking events.
You'll see a confirmation dialog about unsaved changes
+
Click "Submit" to save and clear the warning
+
+
+
+
+ Note: This is a demonstration. The form is rendered with Bootstrap 5
+ and the unsaved changes warning is enabled using
+ UnsavedChangesControl::enable($form).
+