npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fagon/ngx-intellitoolx

v16.0.6

Published

A comprehensive Angular utility library for reactive forms, validation, form state management, UX messaging, and reusable business-rule validators.

Readme

IntelliToolx

npm version npm downloads bundle size Angular TypeScript License Reactive Forms Standalone Components PRs Welcome Made with Love

Overview

IntelliToolx is a comprehensive Angular utility library designed to simplify reactive form development, validation, and user experience.

It provides powerful helpers, reusable validators, smart error handling, UI components, and developer-friendly utilities that eliminate repetitive boilerplate and enforce consistent form behavior across applications.

Built with scalability, accessibility, and maintainability in mind, IntelliToolx helps teams build robust, user-friendly form workflows faster and with fewer bugs.

Installation

npm intall @fagon/ngx-intellitoolx

IntelliToolxHelper

Utility helpers for working with Angular Reactive Forms, deep comparisons, change detection, and common data transformations. Designed to simplify form state management, prevent accidental navigation, and normalize user input.

Import

import { IntelliToolxHelper } from "intellitoolx";

Methods

captureInitialFormValue(form)

Captures and clones the initial value of a form and marks it as pristine.

static captureInitialFormValue(form: AbstractControl): any

Features

  • Uses getRawValue() when available
  • Marks form as pristine
  • Returns a deep clone of the initial value

Example

const initial = IntelliToolxHelper.captureInitialFormValue(this.form);

trimFormGroup(control)

Recursively trims whitespace from all string values in a form.

static trimFormGroup(control: AbstractControl): void

Supports

  • FormGroup
  • FormArray
  • FormControl

Example

IntelliToolxHelper.trimFormGroup(this.form);

deepEqual(obj1, obj2)

Performs a deep comparison between two values.

static deepEqual(obj1: any, obj2: any): boolean

Special behavior

  • Treats null, undefined, and "" as equal
  • Normalizes numeric comparisons
  • Compares File objects by metadata
  • Supports nested objects & arrays

Example

IntelliToolxHelper.deepEqual(a, b);

isEmpty(value)

Checks if a value is empty.

static isEmpty(value: any): boolean

Returns true for: null, undefined, empty string, whitespace-only string

clone(value)

Creates a deep clone.

static clone(value: any): any

Uses structuredClone (Angular 14+ compatible).

formHasChanges(initialValue, form)

Determines whether a form value has changed.

static formHasChanges(initialValue: any, form: AbstractControl): boolean

Example

if (IntelliToolxHelper.formHasChanges(initial, this.form)) {
  // prompt user
}

confirmIfChanged(hasChanges, confirmHandler?)

Shows confirmation if changes exist.

static confirmIfChanged(hasChanges: boolean, confirmHandler?: ConfirmHandler): Promise<boolean>

Behavior

  • Returns true if no changes
  • Uses provided handler if available
  • Falls back to browser confirm()

Example

const canLeave = await IntelliToolxHelper.confirmIfChanged(hasChanges, () => dialog.confirm());

registerBeforeUnload(shouldBlock, message?)

Prevents accidental page exit when changes exist.

static registerBeforeUnload(shouldBlock: () => boolean, message?: string): () => void

Returns Cleanup function to remove the listener.

Example

const cleanup = IntelliToolxHelper.registerBeforeUnload(() => this.form.dirty);
// later
cleanup();

getFormControl(form, path)

Retrieves a nested FormControl using dot notation.

static getFormControl<T>(form: AbstractControl, path: string): FormControl<T> | null

Supports

  • Nested FormGroups
  • FormArrays using numeric indexes

Example

const control = IntelliToolxHelper.getFormControl(this.form, "address.street");
const item = IntelliToolxHelper.getFormControl(this.form, "items.0.name");

convertImageToBase64(file)

Converts an image file to a Base64 string.

static convertImageToBase64(file: File): Promise<string>

Example

const base64 = await IntelliToolxHelper.convertImageToBase64(file);

replaceCharacter(text, replaceChar, replaceWithChar)

Replaces all occurrences of a character in a string.

static replaceCharacter(text: any, replaceChar: string, replaceWithChar: string): string

Example

IntelliToolxHelper.replaceCharacter("1-2-3", "-", ":");
// 1:2:3

convertJsonStringToJson(value)

Safely converts a JSON string into an object.

static convertJsonStringToJson<T>(value: T | string | null | undefined): T | null

Behavior

  • Returns parsed object if valid JSON string
  • Returns original object if already parsed
  • Returns null if parsing fails

Example

const data = IntelliToolxHelper.convertJsonStringToJson<MyType>(jsonString);

Usage Pattern (Recommended)

initialValue = IntelliToolxHelper.captureInitialFormValue(this.form);

save() {
	IntelliToolxHelper.trimFormGroup(this.form);

	if (!IntelliToolxHelper.formHasChanges(this.initialValue, this.form)) {
		return;
	}
	// proceed with save
}

IntellitoolxRegExps

A collection of commonly used regular expressions for validation in Angular and TypeScript applications.

  1. Designed for:
  2. Reactive Forms validation
  3. Input sanitization
  4. Reusable validation patterns
  5. Consistent form behavior across projects

Import

import { IntellitoolxRegExps } from "intellitoolx";

Available Regular Expressions

EMAIL_REGEX

EMAIL*REGEX: /^(?=.{1,50}$)[a-zA-Z0-9.*%+-]+@[a-zA-Z0-9.-]+\.[A-Za-z]{2,}$/

Validates an email address with:

  1. Maximum length of 50 characters
  2. Standard email format
  3. Requires valid domain suffix (min 2 characters)

Valid Examples

Invalid Examples

  • invalid-email
  • user@
  • @test.com

Usage (Angular Validator)

this.form = new FormGroup({
  email: new FormControl("", [Validators.pattern(IntellitoolxRegExps.EMAIL_REGEX)]),
});

NUMBER_REGEX

NUMBER_REGEX: /^\d+$/

Validates:

  • Whole numbers only
  • No decimals
  • No negative values
  • No spaces or characters

Valid Examples

  • 1
  • 25
  • 99999

Invalid Examples

  • 1.5
  • -10
  • abc

AMOUNT_REGEX

AMOUNT_REGEX: /^\d+(\.\d{1,2})?$/

Validates monetary amounts:

  1. Whole numbers
  2. Optional decimal
  3. Maximum 2 decimal places

Valid Examples

  • 10,
  • 10.5
  • 10.50
  • 9999.99

Invalid Examples

  • 10.999
  • abc

DOMAIN_REGEX

DOMAIN_REGEX: /^(?=.{1,255}$)(https?|ftp):\/\/([\w.-]+)\.([a-z\.]{2,6})(:[0-9]{1,5})?(\/\S*)?$/

Validates full URLs with: http, https, or ftp protocol

Valid domain

  1. Optional port
  2. Optional path
  3. Maximum length of 255 characters

Valid Examples

  • https://example.com
  • http://sub.domain.org
  • https://example.com:8080/path
  • ftp://files.server.net

Invalid Examples

  • example.com
  • http:/example.com
  • htp://domain.com

Usage in Angular Reactive Forms

this.form = new FormGroup({
  email: new FormControl("", [Validators.pattern(IntellitoolxRegExps.EMAIL_REGEX)]),
  amount: new FormControl("", [Validators.pattern(IntellitoolxRegExps.AMOUNT_REGEX)]),
  website: new FormControl("", [Validators.pattern(IntellitoolxRegExps.DOMAIN_REGEX)]),
});

IntellitoolxFormUpdateMessage

A lightweight Angular standalone component that displays a configurable warning message when a form has no changes to save.

Designed to improve user experience by clearly informing users why an action (such as saving) cannot proceed.

Import

import { IntellitoolxFormUpdateMessage } from "intellitoolx";

Because it is standalone, import it directly:

@Component({
	standalone: true,
	imports: [IntellitoolxFormUpdateMessage],
})

Basic Usage

<itx-form-update-message></itx-form-update-message>

Default message:

There are no changes to save. Please modify a field to continue.

With Configuration

Customize text and styling using itxFormUpdateMessageConfig.

<itx-form-update-message [itxFormUpdateMessageConfig]="updateMessageConfig"> </itx-form-update-message>
updateMessageConfig = {
  message: "Nothing changed yet.",
  textColor: "#0c5460",
  backgroundColor: "#d1ecf1",
  borderColor: "#bee5eb",
};

Configuration Interface

export interface IntellitoolxFormUpdateMessageConfig {
  message?: string;
  textColor?: string;
  backgroundColor?: string;
  borderColor?: string;
  padding?: string;
  iconAndMessageGap?: string;
  borderRadius?: string;
  fontWeight?: number | string;
  iconSize?: string;
}

All properties are optional.

Default Styling Behavior If no configuration is provided, the component uses accessible warning styles.

| Property | Default | | ------------- | ------- | | text color | #963C00 | | background | #fff3cd | | border | #f3cd5a | | padding | 1rem | | border radius | 0.5rem | | gap | 0.5rem | | font weight | 400 | | icon size | 1rem |

Accessibility

  • Uses role="alert" to notify assistive technologies
  • Icon is hidden from screen readers with aria-hidden="true"
  • Provides clear visual contrast for warning context

Example: Show When No Changes Exist formHasChanges = false;

<itx-form-update-message *ngIf="!formHasChanges"> </itx-form-update-message>

Example: Integrating With IntelliToolxHelper

hasChanges = IntelliToolxHelper.formHasChanges(this.initialValue, this.form);
<itx-form-update-message *ngIf="!hasChanges"> </itx-form-update-message>

Theming Examples Success Style

updateMessageConfig = {
  message: "No updates were made.",
  textColor: "#155724",
  backgroundColor: "#d4edda",
  borderColor: "#c3e6cb",
};

Minimal Style

updateMessageConfig = {
  backgroundColor: "transparent",
  borderColor: "#ddd",
  textColor: "#555",
};

When to Use Use this component when:

  1. A save/update action is triggered without changes
  2. Preventing redundant submissions
  3. Improving form UX clarity
  4. Displaying inline form state feedback

IntellitoolxFormErrors

A lightweight Angular standalone component for displaying reactive form validation errors with customizable and extensible error messages. Built to:

  1. standardize validation messages
  2. support component-level overrides
  3. allow global extension of error messages
  4. simplify template error handling

Import

import { IntellitoolxFormErrors } from "intellitoolx";

You can import it directly into any component.

Basic Usage

<input [formControl]="emailControl" /> <itx-form-errors [control]="emailControl"></itx-form-errors>

Errors will display automatically when:

  • control is touched
  • control is invalid

Default Supported Errors

The component includes built-in messages for common validators:

| Error Key | Message | | ----------------------------- | ------------------------------------------ | | required | This field is required | | email | The email entered is invalid | | minlength | You must enter at least X characters | | maxlength | You must not enter more than X characters | | pattern | Your entry must match the required pattern | | passwordMismatch | Password and confirm password do not match | | futureDate | Future date is not allowed | | duplicateEmail | Each email must be unique | | maxWords | Exceeded maximum number of words | | maxMonthYear | Date is later than allowed | | minMonthYear | Date is earlier than allowed | | exceededAllowedDateDifference | Date difference can only be one month | | startDateAfterEndDate | Start date cannot be greater than end date |

Adding Control Labels (User-Friendly Messages)

<itx-form-errors [control]="amountControl" controlLabel="Amount"> </itx-form-errors>

Example output: Amount cannot be greater than 100

Component-Level Custom Messages Override messages for a specific field.

componentValidation = {
  required: { message: "Email is mandatory" },
  minlength: { message: "Too short" },
};
<itx-form-errors [control]="emailControl" [componentValidation]="componentValidation"> </itx-form-errors>

Using Custom Validators with Messages

If your validator returns an object:

return { customMinValue: { message: "Value is too small" } };

The component will display: Value is too small

Extending Error Messages (Recommended)

Consumers can extend the component to add global or shared messages.

Option 1 — Extend the Component

Create your own reusable error component:

import { Component } from "@angular/core";
import { IntellitoolxFormErrors } from "intellitoolx";

@Component({
  selector: "app-form-errors",
  standalone: true,
  imports: [IntellitoolxFormErrors],
  template: ` <itx-form-errors [control]="control" [componentValidation]="componentValidation" [controlLabel]="controlLabel" /> `,
})
export class AppFormErrors extends IntellitoolxFormErrors {
  override errorMessages = {
    ...this.errorMessages,
    required: "This field cannot be empty",
    phone: "Phone number is invalid",
    usernameTaken: "This username is already taken",
  };
}

Now use:

<app-form-errors [control]="control"></app-form-errors>

Option 2 — Extend via Custom Validator Keys

Return new error keys from validators:

return { usernameTaken: true };

Then provide the message:

componentValidation = {
  usernameTaken: { message: "Username already exists" },
};

Option 3 — Global Shared Messages Service (Advanced)

Create a shared constant:

export const APP_ERROR_MESSAGES = {
  required: "Required field",
  email: "Invalid email address",
};

Then extend:

override errorMessages = {
...APP_ERROR_MESSAGES,
};

Supported Error Value Types

The component intelligently handles different validator outputs:

  • Boolean { required: true } → uses default or custom message
  • String { customError: 'Invalid value provided' } → displays string directly
  • Object { minlength: { requiredLength: 5 } } → dynamic message rendering

Best Practices

  • Always provide controlLabel for better UX
  • Use componentValidation for field-specific overrides
  • Extend the component for app-wide consistency
  • Return structured error objects from custom validators
  • Keep error keys consistent across the application

Example (Full Integration)

<input formControlName="email" />

<app-form-errors [control]="form.controls.email" controlLabel="Email Address"> </app-form-errors>

RequiredMarkerDirective

An Angular standalone directive that automatically adds a visual required marker to form labels when the associated form control has a required validator. This ensures consistent UX and eliminates manual asterisk management.

Import

import { RequiredMarkerDirective } from "intellitoolx";

Because it is standalone, import it directly:

@Component({
	standalone: true,
	imports: [RequiredMarkerDirective],
})

Basic Usage

Add the directive to a element.

<label for="email" itxRequired>Email</label>

<input id="email" formControlName="email" />

If the control has Validators.required, the output becomes: Email*

How It Works

  • Reads the label’s for attribute.
  • Finds the matching control inside the parent FormGroup.
  • Checks for Validators.required.
  • Adds or removes the required marker dynamically.
  • Updates when value or status changes.

Dynamic Updates If required validation is added or removed at runtime:

control.setValidators([Validators.required]);
control.updateValueAndValidity();

The label updates automatically.

Default Marker Behavior

| Property | Default | | -------------------- | ------- | | marker sign | * | | color | red | | spacing | 0.25rem | | position after label | text |

Global Configuration

You can configure marker appearance globally using the provided injection token. Step 1 — Provide Configuration

import { REQUIRED_MARKER_GLOBAL_CONFIG } from "intellitoolx";

providers: [
  {
    provide: REQUIRED_MARKER_GLOBAL_CONFIG,
    useValue: {
      sign: "*",
      color: "#d9534f",
      spacing: "4px",
      position: "after", // 'before' | 'after'
    },
  },
];

Configuration Options

export interface ResolvedRequiredMarkerConfig {
  sign: string;
  color: string;
  spacing: string;
  position: "before" | "after";
}

Positioning the Marker After (default) Email \* Before position: 'before' \* Email

Preserves Additional Label Text The directive preserves text in parentheses or suffix content.

<label for="dob" itxRequired>Date of Birth (optional)</label>

Output when required: Date of Birth \* (optional) Works With Nested Form Groups Ensure the for attribute matches the control name.

<label for="address.street" itxRequired>Street</label>

<input formControlName="street" />

Requirements

  • Must be used inside a form with FormGroupDirective
  • Label for attribute must match control name

Common Mistakes

  • Missing for attribute
  • Label not associated with control
  • Control name mismatch

Example (Complete)

<form [formGroup]="form">
  <label for="email" itxRequired>Email</label>
  <input id="email" formControlName="email" />
</form>
email: new FormControl("", Validators.required);

JsonParsePipe

An Angular standalone pipe that safely parses JSON strings into JavaScript objects. Built on top of IntelliToolxHelper.convertJsonStringToJson, this pipe prevents template errors when dealing with dynamic or stringified JSON data.

Import

import { JsonParsePipe } from "intellitoolx";

Because it is standalone, import it directly:

@Component({
	standalone: true,
	imports: [JsonParsePipe],
})

Usage

Basic Example

{{ jsonString | jsonParse | json }}

Input

jsonString = '{"name":"John","age":30}';

Output

{
  "name": "John",
  "age": 30
}

When to Use

Use jsonParse when:

  • API responses contain stringified JSON
  • Form values store serialized objects
  • LocalStorage data needs parsing
  • Preventing template crashes from invalid JSON

Behavior

The pipe safely handles multiple input types:

| Input | Result | | ------------------------ | --------- | | Valid JSON string Parsed | object | | Invalid JSON string | null | | Object Returned | unchanged | | null / undefined | null |

Examples Parse API Data

<div *ngIf="user.data | jsonParse as parsed">{{ parsed.name }}</div>

Parse Stored JSON

{{ localStorageValue | jsonParse | json }}

Safe Access with Optional Chaining

{{ (settings | jsonParse)?.theme }}

Equivalent TypeScript Logic

The pipe internally performs:

IntelliToolxHelper.convertJsonStringToJson(value);

Error Safety

Unlike JSON.parse() directly in templates, this pipe:

  • prevents runtime template errors
  • avoids breaking change detection
  • returns null on parsing failure

For large datasets or repeated bindings: Prefer parsing in the component

parsedData = IntelliToolxHelper.convertJsonStringToJson(data);

IntelliToolx Validators

A set of reusable Angular Reactive Form validators designed for common business rules such as amount limits, password matching, word limits, and duplicate detection. These validators integrate seamlessly with Angular forms and work perfectly with IntellitoolxFormErrors for displaying user-friendly messages.

Import

import { customMaxAmountValidator, customMinAmountValidator, maxWordsValidator, passwordMismatchValidator, uniqueEmailsValidator } from "intellitoolx";

Validators Overview

| Validator | Purpose | | ------------------------- | -------------------------------------- | | customMaxAmountValidator | Enforces maximum numeric value | | customMinAmountValidator | Enforces minimum numeric value | | maxWordsValidator | Limits number of words | | passwordMismatchValidator | Ensures password fields match | | uniqueEmailsValidator | Prevents duplicate emails in FormArray |

customMaxAmountValidator(maxValue)

Ensures a numeric value does not exceed a specified maximum.

customMaxAmountValidator(maxValue: number): ValidatorFn

Example

amount = new FormControl("", [customMaxAmountValidator(1000)]);

Error Output

{
  "customMaxValue": {
    "requiredMin": 1000,
    "actual": 1200,
    "message": "The value must not exceed 1000.00"
  }
}

Notes

  • Accepts numeric values only
  • Returns a formatted error message
  • Works with IntellitoolxFormErrors automatically

customMinAmountValidator(minValue)

Ensures a numeric value is at least the specified minimum.

customMinAmountValidator(minValue: number): ValidatorFn

Example

amount = new FormControl("", [customMinAmountValidator(10)]);

Error Output

{
  "customMinValue": {
    "requiredMin": 10,
    "actual": 5,
    "message": "The value must be at least 10.00"
  }
}

maxWordsValidator(maxWords)

Limits the number of words in a text input. maxWordsValidator(maxWords: number): ValidatorFn

Example

description = new FormControl("", [maxWordsValidator(20)]);

Behavior

  • Ignores extra whitespace
  • Counts words separated by spaces

Error Output

{
  "maxWords": {
    "actual": 25,
    "max": 20
  }
}

passwordMismatchValidator(controlName, matchingControlName)

Validates that two form fields match (commonly used for password confirmation).

passwordMismatchValidator(
	controlName: string,
	matchingControlName: string
)

Example

this.form = new FormGroup(
  {
    password: new FormControl(""),
    confirmPassword: new FormControl(""),
  },
  {
    validators: passwordMismatchValidator("password", "confirmPassword"),
  },
);

Error Output

{ passwordMismatch: true }

Notes

  • Error is applied to the matching control
  • Ideal for password confirmation fields

uniqueEmailsValidator()

Ensures all email addresses inside a FormArray are unique.

uniqueEmailsValidator(): ValidatorFn

Example

this.form = new FormGroup({
  contacts: new FormArray([
    new FormGroup({
      email: new FormControl(""),
    }),
  ]),
});

this.contacts.setValidators(uniqueEmailsValidator());

Behavior

  • Ignores case and whitespace
  • Flags duplicates automatically
  • Updates errors dynamically

Error Output (control level) { duplicateEmail: true } Error Output (form array level) { duplicateEmails: true }

Integration with IntellitoolxFormErrors

These validators return error keys compatible with the error component:

| Validator Error | Key | | ------------------------- | ---------------- | | customMaxAmountValidator | customMaxValue | | customMinAmountValidator | customMinValue | | maxWordsValidator | maxWords | | passwordMismatchValidator | passwordMismatch | | uniqueEmailsValidator | duplicateEmail |

** Best Practices **

  • Use with Reactive Forms only
  • Combine with required validators when necessary
  • Provide control labels for better error messages
  • Normalize numeric inputs before validation
  • Use FormArray validators for dynamic lists

Complete Example

this.form = new FormGroup({
  amount: new FormControl("", [customMinAmountValidator(10), customMaxAmountValidator(1000)]),
  description: new FormControl("", [maxWordsValidator(20)]),
});

Unsaved Changes Protection

Protect Angular routes and modals from accidental navigation or closing when there are unsaved changes. Supports:

  1. Angular route navigation
  2. Browser back button
  3. Browser refresh / tab close
  4. Modal close (NG Bootstrap / Material / custom)
  5. Parent + child component setups
  6. Nested forms

The system is built around three core pieces:

  1. FormChangesTrackerService
  2. UnsavedChangesGuard
  3. CanComponentDeactivate interface (for route protection)

Architecture:

Form → TrackerService → Guard → Confirmation → Continue / Block

The library does NOT depend on your UI layer. Your app controls the confirmation modal. Core Exports

import { FormChangesTrackerService, UnsavedChangesGuard, CanComponentDeactivate } from "ngx-intellitoolx";

FormChangesTrackerService API

The FormChangesTrackerService tracks changes in Angular forms and provides methods for managing form state across your application.

  1. Single Form Operations

register(form)

Registers a single form for change tracking.

register(form: AbstractControl): void

Behavior:

  • Captures the current form value as the initial state
  • Marks the form as pristine
  • Stores the form reference for change detection

Example:

ngOnInit() {
	this.form = this.fb.group({
		name: [''],
		email: ['']
	});

	this.tracker.register(this.form);
}

reset(form)

Resets the tracked initial value to the current form value.

reset(form: AbstractControl): void

Use Case: After patching form data from an API, reset the tracker to prevent false positives. Example:

loadData() {
	this.api.getData().subscribe(data => {
		this.form.patchValue(data);
		// Reset tracker after patching to track only future changes
		this.tracker.reset(this.form);
	});
}

unregister(form)

Removes a form from the tracker.

unregister(form: AbstractControl): void

Example:

ngOnDestroy() {
	this.tracker.unregister(this.form);
}

hasChanges()

Checks if any tracked form has unsaved changes.

hasChanges(): boolean

Returns: true if any tracked form has changes, false otherwise. Example:

if (this.tracker.hasChanges()) {
  // Show confirmation modal
}

clearAll()

Removes all forms from the tracker.

clearAll(): void

Example:

ngOnDestroy() {
	this.tracker.clearAll();
}
  1. Batch Operations For applications with multiple forms that need to be managed together, the service provides batch methods.

registerForms(forms)

Registers multiple forms at once for change tracking.

registerForms(forms: AbstractControl[]): void

Use Case:

  • Multi-step forms
  • Forms in tabs
  • Parent component managing multiple child forms

Example:

ngOnInit() {
	this.personalInfoForm = this.fb.group({
		firstName: [''],
		lastName: ['']
	});

	this.contactForm = this.fb.group({
		email: [''],
		phone: ['']
	});

	this.addressForm = this.fb.group({
		street: [''],
		city: ['']
	});

	// Register all forms at once
	this.tracker.registerForms([
		this.personalInfoForm,
		this.contactForm,
		this.addressForm
	]);
}

resetForms(forms)

Resets multiple forms to their current values, useful after batch updates or API calls.

resetForms(forms: AbstractControl[]): void

Use Case:

  • After loading data into multiple forms
  • After bulk form updates
  • Resetting multiple wizard steps

Example:

loadAllData() {
	this.api.getUserData().subscribe(data => {
		this.personalInfoForm.patchValue(data.personal);
		this.contactForm.patchValue(data.contact);
		this.addressForm.patchValue(data.address);

		// Reset all forms after patching
		this.tracker.resetForms([
			this.personalInfoForm,
			this.contactForm,
			this.addressForm
		]);
	});
}

unregisterForms(forms)

Removes multiple forms from the tracker at once.

unregisterForms(forms: AbstractControl[]): void

Use Case:

  • Component cleanup with multiple forms
  • Dynamically removing form sections
  • Batch cleanup in parent components

Example:

ngOnDestroy() {
	// Unregister all forms at once
	this.tracker.unregisterForms([
		this.personalInfoForm,
		this.contactForm,
		this.addressForm
	]);
}

Complete Multi-Form Example

import { Component, OnInit, OnDestroy } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { FormChangesTrackerService } from "ngx-intellitoolx";

@Component({
  selector: "app-user-profile",
  templateUrl: "./user-profile.component.html",
})
export class UserProfileComponent implements OnInit, OnDestroy {
  personalForm!: FormGroup;
  contactForm!: FormGroup;
  preferencesForm!: FormGroup;

  constructor(
    private fb: FormBuilder,
    private tracker: FormChangesTrackerService,
    private api: UserApiService,
  ) {}

  ngOnInit() {
    // Create multiple forms
    this.personalForm = this.fb.group({
      firstName: [""],
      lastName: [""],
      dateOfBirth: [""],
    });

    this.contactForm = this.fb.group({
      email: [""],
      phone: [""],
      address: [""],
    });

    this.preferencesForm = this.fb.group({
      newsletter: [false],
      notifications: [false],
      theme: ["light"],
    });

    // Register all forms at once
    this.tracker.registerForms([this.personalForm, this.contactForm, this.preferencesForm]);

    // Load data
    this.loadUserData();
  }

  loadUserData() {
    this.api.getUserProfile().subscribe((data) => {
      this.personalForm.patchValue(data.personal);
      this.contactForm.patchValue(data.contact);
      this.preferencesForm.patchValue(data.preferences);

      // Reset all trackers after loading
      this.tracker.resetForms([this.personalForm, this.contactForm, this.preferencesForm]);
    });
  }

  saveAll() {
    if (!this.tracker.hasChanges()) {
      console.log("No changes to save");
      return;
    }

    const allData = {
      personal: this.personalForm.value,
      contact: this.contactForm.value,
      preferences: this.preferencesForm.value,
    };

    this.api.updateProfile(allData).subscribe(() => {
      // Reset all forms after successful save
      this.tracker.resetForms([this.personalForm, this.contactForm, this.preferencesForm]);
    });
  }

  ngOnDestroy() {
    // Clean up all forms at once
    this.tracker.unregisterForms([this.personalForm, this.contactForm, this.preferencesForm]);
  }
}

Best Practices

  1. Always unregister forms in ngOnDestroy() to prevent memory leaks
  2. Use clearAll() when component manages all tracked forms
  3. Reset after API loads to establish the correct baseline for change detection
  4. Batch operations improve code readability when managing multiple forms
  5. Call markAsPristine() after successful saves (handled automatically by reset() and resetForms())

Protecting Angular Routes

Add Guard to Route

{
	path: 'add-item',
	component: AddItemPageComponent,
	canDeactivate: [UnsavedChangesGuard]
}

Implement Route Component

Your routed component must implement CanComponentDeactivate. This is not related to the library, as the modal trigger logic exists entirely outside the library's scope.

import { FormChangesTrackerService } from "ngx-intellitoolx";
import { UtilityService } from "../services/utility.service";

@Component({
  selector: "app-add-item-page",
  template: `<app-item-form></app-item-form>`,
})
export class AddItemPageComponent implements CanComponentDeactivate {
  constructor(private utils: UtilityService) {}

  confirmUnsavedChanges(): Promise<boolean> {
    return this.utils.confirmUnsavedChanges();
  }
}

Track Form Changes (Child Component)

@Component({
  selector: "app-item-form",
  templateUrl: "./item-form.component.html",
})
export class ItemFormComponent implements OnInit {
  form!: FormGroup;

  constructor(
    private fb: FormBuilder,
    private tracker: FormChangesTrackerService,
  ) {}

  ngOnInit(): void {
    this.form = this.fb.group({
      name: [""],
      description: [""],
    });

    this.tracker.register(this.form);
  }

  patchForm() {
    this.form.patchValue({
      name: this.data.name,
      description: this.data.description,
    });
    // reset form after patching to track changes afterwards.
    // Very useful during form update
    this.tracker.reset(this.form);
  }

  save(): void {
    // API call
    this.form.markAsPristine();
  }

  ngOnDestroy(): void {
    // This is very important to not keep track of forms from other component
    this.tracker.clearAll();
  }
}

Protecting Browser Refresh / Tab Close

Add this inside the routed component:

// Declare cleanup callback
private unregister?: () => void;

constructor(
private tracker: FormChangesTrackerService,
public utilservice: UtilityService
) {
super();
}

ngOnInit() {
// Register a beforeunload handler to prevent or warn about navigation when there are unsaved changes.
this.unregister = IntelliToolxHelper.registerBeforeUnload(() =>
this.tracker.hasChanges(),
);
}

confirmUnsavedChanges(): Promise<boolean> {
return this.utilservice.confirmUnsavedChanges();
}

ngOnDestroy() {
// Safely invokes the teardown/unsubscribe callback if it exists.
this.unregister?.();
}

Protecting Modals (NG Bootstrap Example)

Route guards do NOT protect modals. You must intercept modal close manually.

Modal Form Component

import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { FormChangesTrackerService } from "ngx-intellitoolx";
import { UtilityService } from "../utility.service";

@Component({
  selector: "app-item-modal",
  templateUrl: "./item-modal.component.html",
})
export class ItemModalComponent implements OnInit {
  form!: FormGroup;

  constructor(
    private fb: FormBuilder,
    private tracker: FormChangesTrackerService,
    private utils: UtilityService,
    public activeModal: NgbActiveModal,
  ) {}

  ngOnInit(): void {
    this.form = this.fb.group({
      name: [""],
      description: [""],
    });

    this.tracker.register(this.form);
  }

  async closeModal() {
    if (this.tracker.hasChanges()) {
      const allowClose = await IntelliToolxHelper.confirmIfChanged(this.tracker.hasChanges(), () => this.utilsService.confirmUnsavedChanges());
      if (!allowClose) return;
    }

    this.activeModal.dismiss();
  }

  save(): void {
    this.form.markAsPristine();
    this.activeModal.close("saved");
  }

  ngOnDestroy(): void {
    // This is very important to not keep track of forms from other component
    this.tracker.clearAll();
  }
}

Modal Template

<div class="modal-header">
  <h5 class="modal-title">Add Item</h5>
  <button type="button" class="btn-close" (click)="closeModal()"></button>
</div>

<div class="modal-body">
  <form [formGroup]="form">
    <input formControlName="name" />
    <textarea formControlName="description"></textarea>
  </form>
</div>

<div class="modal-footer">
  <button class="btn btn-secondary" (click)="closeModal()">Cancel</button>

  <button class="btn btn-primary" (click)="save()">Save</button>
</div>

How It Works

| Action | What Happens | | -------------------- | --------------------- | | User edits form | Tracker marks dirty | | User navigates route | Guard checks tracker | | User refreshes page | beforeunload triggers | | User closes modal | Manual interception | | User saves form | Tracker resets |

Parent + Child Structure

If your form is nested:

RouteComponent
└── Modal
└── Form

Child form → marks tracker dirty

Route component → implements guard interface

Modal → manually checks tracker before close

Confirmation Modal Example (App Layer)

@Injectable({ providedIn: "root" })
export class UtilityService {
  constructor(private modal: NgbModal) {}

  confirmUnsavedChanges(): Promise<boolean> {
    const modalRef = this.modal.open(UnsavedConfirmComponent, {
      backdrop: "static",
    });

    return modalRef.result.then(() => true).catch(() => false);
  }
}

📄 License

MIT © Fagon Technologies