@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.
Maintainers
Readme
IntelliToolx
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-intellitoolxIntelliToolxHelper
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): anyFeatures
- 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): voidSupports
- FormGroup
- FormArray
- FormControl
Example
IntelliToolxHelper.trimFormGroup(this.form);deepEqual(obj1, obj2)
Performs a deep comparison between two values.
static deepEqual(obj1: any, obj2: any): booleanSpecial 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): booleanReturns true for: null, undefined, empty string, whitespace-only string
clone(value)
Creates a deep clone.
static clone(value: any): anyUses structuredClone (Angular 14+ compatible).
formHasChanges(initialValue, form)
Determines whether a form value has changed.
static formHasChanges(initialValue: any, form: AbstractControl): booleanExample
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): () => voidReturns 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> | nullSupports
- 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): stringExample
IntelliToolxHelper.replaceCharacter("1-2-3", "-", ":");
// 1:2:3convertJsonStringToJson(value)
Safely converts a JSON string into an object.
static convertJsonStringToJson<T>(value: T | string | null | undefined): T | nullBehavior
- 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.
- Designed for:
- Reactive Forms validation
- Input sanitization
- Reusable validation patterns
- 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:
- Maximum length of 50 characters
- Standard email format
- 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:
- Whole numbers
- Optional decimal
- 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
- Optional port
- Optional path
- 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:
- A save/update action is triggered without changes
- Preventing redundant submissions
- Improving form UX clarity
- 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:
- standardize validation messages
- support component-level overrides
- allow global extension of error messages
- 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): ValidatorFnExample
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): ValidatorFnExample
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(): ValidatorFnExample
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:
- Angular route navigation
- Browser back button
- Browser refresh / tab close
- Modal close (NG Bootstrap / Material / custom)
- Parent + child component setups
- Nested forms
The system is built around three core pieces:
- FormChangesTrackerService
- UnsavedChangesGuard
- CanComponentDeactivate interface (for route protection)
Architecture:
Form → TrackerService → Guard → Confirmation → Continue / BlockThe 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.
- Single Form Operations
register(form)
Registers a single form for change tracking.
register(form: AbstractControl): voidBehavior:
- 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): voidUse 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): voidExample:
ngOnDestroy() {
this.tracker.unregister(this.form);
}hasChanges()
Checks if any tracked form has unsaved changes.
hasChanges(): booleanReturns: 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(): voidExample:
ngOnDestroy() {
this.tracker.clearAll();
}- 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[]): voidUse 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[]): voidUse 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[]): voidUse 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
- Always unregister forms in ngOnDestroy() to prevent memory leaks
- Use clearAll() when component manages all tracked forms
- Reset after API loads to establish the correct baseline for change detection
- Batch operations improve code readability when managing multiple forms
- Call markAsPristine() after successful saves (handled automatically by
reset()andresetForms())
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 closeConfirmation 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