Track Unsaved Form Changes in JavaScript – dirty-form

Category: Form , Javascript | October 15, 2025
Authorkirillplatonov
Last UpdateOctober 15, 2025
LicenseMIT
Tags
Views52 views
Track Unsaved Form Changes in JavaScript – dirty-form

dirty-form is a lightweight JavaScript plugin that tracks form changes to prevent users from losing unsaved edits.

It monitors input, textarea, and select elements to detect when a form has been modified, then warns users if they try to navigate away before saving their changes.

Features:

  • State tracking callbacks: Exposes onDirty callback to trigger custom logic when forms become dirty.
  • Selective field exclusion: Allows excluding specific fields from tracking via data attributes.
  • Turbo/Hotwire integration: Built-in support for Turbo navigation events with beforeLeave callback.
  • Flexible usage modes: Can operate in tracking-only mode without navigation prevention.
  • Smart field detection: Handles input, textarea, select elements plus custom editors like Trix.

Use Cases:

  • Complex Settings Pages: On a user profile or application settings page with many inputs, a user can easily lose their work by accidentally clicking a link. This library provides an essential safeguard.
  • Content Management Systems: When an editor is writing a long article, an accidental back-button press can be infuriating. dirty-form adds a layer of protection against losing content.
  • Dynamic UI State: You can use the onDirty callback to enable a “Save Changes” button only after the user has modified a field. This gives the user clear feedback about the form’s state.

How to use it:

1. Install DirtyForm and import it into your project.

# Yarn
$ yarn add dirty-form
# NPM
$ npm install dirty-form
import DirtyForm from 'dirty-form'

2. Initialize DirtyForm with a form element. This setup automatically tracks all input, textarea, and select elements within the form. When users modify any field and try to navigate away, they’ll see a browser confirmation dialog warning them about unsaved changes.

const form = document.querySelector('#my-form')
new DirtyForm(form)

3. A common pattern involves keeping submit buttons disabled until users actually modify the form. This prevents accidental submissions and gives clear visual feedback:

const form = document.getElementById("my-form")
const dirtyForm = new DirtyForm(form, {
  onDirty: () => {
    form.querySelector("input[type=submit]").removeAttribute("disabled")
  },
})
form.addEventListener("submit", () => {
  dirtyForm.disconnect()
})

The onDirty callback fires once when the form first becomes dirty. Calling disconnect() on form submission removes all event listeners and prevents the navigation warning from appearing after the form has been successfully submitted.

4. Sometimes a form contains fields that shouldn’t trigger the dirty state, like a search filter. You can exclude any input by adding data-dirty-form="false".

<input type="text" name="search" data-dirty-form="false">

5. All available options.

  • message: The custom message string shown in the confirmation dialog. Default: ‘You have unsaved changes. Are you sure you want to leave?’.
  • onDirty: A callback function that fires once when the form transitions from a clean to a dirty state.
  • beforeLeave: A callback that fires before Turbo navigation occurs (if you’re using Turbo).
  • skipLeavingTracking: A boolean that disables the beforeunload prompt entirely. This is useful if you only want to use the onDirty callback for UI changes.
new DirtyForm(form, {
  message: 'You have unsaved changes. Are you sure you want to leave?',
  onDirty: () => { },
  beforeLeave: () => { },
  skipLeavingTracking: true,
})

FAQs:

Q: Does this work with dynamically added form fields?
A: No, Dirty-Form captures field references during initialization. If you add fields after creating the instance, they won’t be tracked. You’ll need to call disconnect() on the old instance and create a new one after modifying the DOM.

Q: Can I customize the confirmation dialog appearance?
A: The browser’s beforeunload dialog cannot be styled or customized beyond the message text. Modern browsers actually ignore custom messages entirely for security reasons. If you need a custom dialog, set skipLeavingTracking: true and implement your own navigation interception using the dirty state.

Q: How do I check if a form is currently dirty?
A: Access the isDirty property on your DirtyForm instance: if (dirtyForm.isDirty) { }. This boolean flag indicates whether any tracked field has been modified from its initial value.

Q: What happens with file inputs?
A: File inputs are tracked like other input types, but the initial value will always be empty. Any file selection will mark the form as dirty. You cannot programmatically set file input values for security reasons, so Dirty-Form treats them as modified once the user selects a file.

Q: Does this interfere with form validation?
A: No, Dirty-Form only tracks change state and navigation events. It doesn’t prevent form submission or interfere with validation logic. You can use it alongside any validation library without conflicts.

Q: Can I reset the dirty state without disconnecting?
A: The library doesn’t expose a reset method. The intended workflow is to call disconnect() after successful submission. If you need to reset state mid-session, create a new instance with the current form values as the baseline.

Q: How does it handle select elements with multiple selection?
A: Multiple select elements are tracked by their value property, which returns a comma-separated string of selected options. Any change to the selection will mark the form dirty.

You Might Be Interested In:


Leave a Reply