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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@studiohyperdrive/ngx-forms

v18.0.3

Published

An Angular package to help with complex forms and their validation.

Downloads

227

Readme

# Angular Tools: NgxForms (@studiohyperdrive/ngx-forms)

This library provides multiple utilities for complex form use-cases.

Installation

Install the package first:

npm install @studiohyperdrive/ngx-forms

Versioning and build information

This package will follow a semver-like format, major.minor.patch, in which:

  • major: Follows the Angular major version
  • minor: Introduces new features and (potential) breaking changes
  • patch: Introduces bugfixes and minor non-breaking changes

For more information about the build process, authors, contributions and issues, we refer to the ngx-tools repository.

Concept

The ngx-forms package provides several validators, guards, directives, utils and abstracts to facilitate creating complex form flows in your application. These individual items can be use separately to your liking.

The validators of the ngx-forms package provide easy and robust way to tackle more complex validation requirements, such as various dependency based required flows, as smaller frequently used use-cases such as several dates related validators.

Simplifying the process of creating (complex) custom ControlValueAccessors, ngx-forms introduces the FormAccessor and FormContainer approach to provide a quick and easy way to build custom controls and re-usable form components.

To handle errors in a uniform way, ngx-forms provides the NgxErrors directive approach that will unify all error handling with either the default or a custom defined error component.

Last but not least, the SaveOnExit flow of ngx-forms allows for a quick and simple solution to the often re-occuring "save-before-you-exit" flow that is common amongst complex form implementation.

Implementation

Validators

A set of extra custom validators compatible with the default Angular validators and reactive forms.

extendedEmail

Extends the default e-mail validator with a required period in the tld part of te email.

allOrNothingRequired

A FormGroup validator that checks whether either all controls in a FormGroup are filled in, or no controls are. This validator is particularly useful when dealing with optional FormGroups within a form.

atLeastOneRequired

A FormGroup validator that checks whether at least one of the provided controls was filled in. A separate function to determine the filled in state can be provided.

compareValidator

A FormGroup validator that will compare child control values with a provided comparator.

dependedRequired

A FormGroup validator that checks whether a series of controls are filled in when another control was filled in. A separate function to determine the filled in state can be provided.

decimalsAfterComma

A validator that checks whether a provided number matches with a maximum amount of decimals after the comma.

chronologicalDates

A validator that checks whether two dates are in chronological order.

dateRangeValidator

A validator that checks whether a date falls between a provided range. The start and end date of the range are exclusive.

hasNoFutureDateValidator

A validator which validates if a date is not in the future.

wordCountValidator

A validator that will check the amount of words provided in a control.

set/clearFormError

In custom validators, it is often useful to be able to quickly add or remove a specific error from the control. Using the setFormError we can easily set a specific error on a control, whilst clearFormError will remove said error.

FormAccessor

A FormAccessor can represent a control, group or array form, and has its own validation, disabled and touched state.

This approach allows us to easily compartmentalize larger forms into smaller components, which then hold their own logic and allow them to be re-used throughout the entire application with ease. An added benefit to this approach is the ability to specifically map the data to an interface that best matches the form UI, whilst preserving the initial interface of the data throughout the rest of the application.

FormAccessor is an abstract layer on top of the CustomControlValueAccessor, and provides a default implementation for the most commonly implemented methods that are needed for a ControlValueAccessor. These methods are: writeValue, onChange, validate, setDisabledState, markAsTouched and markAsPristine. Each of these methods has a default implementation that can be overwritten for specific use-cases.

Base implementation

In order to use this way of working, we create a component and extend the FormAccessor class. To make sure that the internal process is typed, we pass a data type for the form data by passing the FormDataType, and the kind of control we want to use internally by passing a FormControlType

export class TestComponent extends FormAccessor<FormDataType, FormControlType>

The FormAccessor requires a single method to be implemented by its extender, being initForm. This method should return an AbstractControl to which we'll write the data to.

initForm(): FormGroup {
	return new FormGroup({
		foo: new FormControl(),
		bar: new FormControl()
	})
}

Extended implementation

As mentioned earlier, this approach allows us to map the data to a specific interface that is beneficial for the form. For example, whilst the external API expects an object, internally in the form, using an array is easier. The FormAccessor has an extended implementation to make this possible.

We start by providing a third interface when extending, so we know which type of data we use internally.

export class TestComponent extends FormAccessor<FormDataType, FormControlType, InternalFormDataType>

When writing the data from the exterior form to this component, we can now map the data from the interface FormDataType to InternalFormDataType by using the optional onWriteValueMapper method.

onWriteValueMapper({value: {foo: true, bar: false}) {
	return [
		... value.foo ? ['foo'] : [],
		... value.bar ? ['bar'] : []
		]
}

In the example above, we changed an object to an string array, because our form UI input components expect us to provide this type. When the input then changes, we want to ensure that the external form now gets the correct data we initially patched to this accessor. We do this by implementing the optional onChangeMapper method.

onChangeMapper({value: string[]) {
	return {
		foo: value.includes('foo'),
		bar: value.includes('bar')
	}
}

Disabling and enabling control

If we wish to disable or enable all controls within a FormAccessor, we can simply disable/enable the parent control that is connected to this accessor.

If we wish to disable specific controls within the a FormAccessor, we can use the disableFields input to pass down the keys of these controls. By default, disabling these will cause a valueChanges emit. This behavior can be overwritten by implementing the emitValueWhenDisableFieldsUsingInput function.

setDisableState

By default, Angular always runs setDisableState on ControlValueAccessors. Due to the implementation of the FormAccessor, this might interfere with FormGroups defined in the initForm.

To prevent this interference, the first setDisableState is ignored by default. If you wish to opt out of this behavior, you can set the skipInitialSetDisable to false.

DataFormAccessor

A special case of the FormAccessor, the DataFormAccessor allows you to provide a set of data to the data input which will be used when creating the underlying form. Common examples for this use case include using an array of data to build up a set of controls in the form.

Given that the DataFormAccessor is a special case of the regular FormAccessor, all earlier mentioned methods and @Inputs are still applicable.

createAccessorProviders

In order to make a component accessible through an AbstractControl, the NG_VALUE_ACCESSOR needs to be provided to the component. In the same vain, in order to make the validation work, the NG_VALIDATORS needs to be provided.

To simplify this process, ngx-forms provides a helpful util function called createAccessorProviders. This will automatically generate all the necessary providers for the ngx-forms workflow.

In the examples section you will find out how this is implemented.

Examples

Simple example
interface UserName {
	name: string;
	firstName: string;
}

interface UserNameForm {
	name: FormControl<string>;
	firstName: FormControl<string>;
}

@Component({
	selector: 'user-name-form',
	templateUrl: './user-name.component.html',
	providers: [createAccessorProviders(UserNameFormComponent)],
})
export class UserNameFormComponent
	extends FormAccessor<UserName, FormGroup<UserNameForm>>
	implements OnChanges
{
	constructor(readonly cdRef: ChangeDetectorRef, private readonly formBuilder: FormBuilder) {
		super(cdRef);
	}

	initForm(): FormGroup<UserNameForm> {
		return this.formBuilder.group({
			name: [null, Validators.required],
			firstName: [null, Validators.required],
		});
	}
}
Mapper example
interface UserName {
	name: string;
	firstName: string;
}

interface UserNameForm {
	name: FormControl<string>;
	firstName: FormControl<string>;
}

@Component({
	selector: 'user-name-form',
	templateUrl: './user-name.component.html',
	providers: [createAccessorProviders(UserNameFormComponent)],
})
export class UserNameFormComponent
	extends FormAccessor<string, FormGroup<UserNameForm>, UserName>
	implements OnChanges
{
	constructor(readonly cdRef: ChangeDetectorRef, private readonly formBuilder: FormBuilder) {
		super(cdRef);
	}

	initForm(): FormGroup<UserNameForm> {
		return this.formBuilder.group({
			name: [null, Validators.required],
			firstName: [null, Validators.required],
		});
	}

	onWriteValueMapper(value: string) {
		const [firstName, name] = value.split('-');

		return { firstName, name };
	}

	onChangeMapper(value: UserName) {
		return `${value.firstName}-${value.name}`;
	}
}
Overwrite example
interface UserName {
	name: string;
	firstName: string;
}

interface UserNameForm {
	name: FormControl<string>;
	firstName: FormControl<string>;
}

@Component({
	selector: 'user-name-form',
	templateUrl: './user-name.component.html',
	providers: [createAccessorProviders(UserNameFormComponent)],
})
export class UserNameFormComponent
	extends FormAccessor<UserName, FormGroup<UserNameForm>>
	implements OnChanges
{
	constructor(readonly cdRef: ChangeDetectorRef, private readonly formBuilder: FormBuilder) {
		super(cdRef);
	}

	initForm(): FormGroup<UserNameForm> {
		return this.formBuilder.group({
			name: [null, Validators.required],
			firstName: [null, Validators.required],
		});
	}

	validate() {
		return form.valid ? null : { invalidUserName: true };
	}
}
DataFormAccessor
interface SurveyQuestion {
	name: string;
	id: string;
}

interface SurveyForm {
	name: FormControl<string>;
	[key: id]: FormControl<string>;
}

@Component({
	selector: 'survey-form',
	templateUrl: './survey.component.html',
	providers: [createAccessorProviders(SurveyFormComponent)],
})
export class SurveyFormComponent
	extends DataFormAccessor<SurveyQuestion[], Record<string, string>, FormGroup<SurveyForm>>
	implements OnChanges
{
	constructor(readonly cdRef: ChangeDetectorRef, private readonly formBuilder: FormBuilder) {
		super(cdRef);
	}

	initForm(questions: SurveyQuestion[]): FormGroup<SurveyForm> {
		const form = this.formBuilder.group({
			name: [null, Validators.required],
		});

		questions.forEach((question) => {
			form.addControl(question.id, this.formBuilder.control('', Validators.required));
		});

		return form;
	}
}

FormAccessorContainer

In order to update the value and validity of all controls of several (nested) FormAccessors, we use the FormAccessorContainer.

BaseFormAccessor

If you're using the createAccessorProviders util, this step is not needed. If you provide your accessors manually, this next step is important when you work with a FormAccessorContainer.

In order to reach all FormAccessors and their children, we need to provide the BaseFormAccessor value in the providers array of the accessors.

		{
			provide: BaseFormAccessor,
			useExisting: forwardRef(() => BasicRegistrationDataFormComponent)
		}

UpdateValueAndValidity

Calling this method on a FormAccessorContainer will recursively update the value and validity for each FormAccessor and their corresponding FormAccessor children in the template.

NGXErrors

Inspired by Tod Motto's NgxErrors approach, ngx-forms provides its own implementation of the ngxErrors directive which automizes the error message rendering.

Intended to be used in projects that require consistent error messages throughout the entire codebase, this implementation of ngxErrors allows for a record of corresponding messages to validator errors to be set on root level, which then can be rendered in either a custom component or a standard p element.

The error message is always rendered right below the element the ngxErrors directive is placed on.

Configuration

To implement the ngxErrors directive, we have to provide the necessary configuration on root level and import the NgxFormsErrorsDirective where used.

A simple example is shown below.

    // Root
    providers: [
		{
            provide: NgxFormsErrorsConfigurationToken,
            useValue: {
                errors: {
                    required: 'This is a required field.',
                    email: 'This field is not a valid email address.'
                },
                showWhen: 'touched',
            }
        },
    ]

    // Component
    @Component({
        selector: 'test',
        standalone: true,
        imports: [NgxFormsErrorsDirective]
    })

Basic implementation

By default, only two properties are required when setting up the NgxFormsErrorsDirective.

The provided errors record makes sure that the error key that is found in the ValidationErrors of a control will be matched with the message we wish to show to our users.

The showWhen property will determine when an error message becomes visible. You can either set it to touched or to dirty.

Once configured, all we need to do is attach the directive where we wish to render the error. We suggest attaching this directly to the input or your custom input component.

<ng-container [formGroup]="form">
	<p>Hello</p>
	<input *ngxFormsErrors="'hello'" formControlName="hello" type="text" />
</ng-container>

The ngxFormsErrors directive allows for a string value that matches with the provided control in a FormGroup. Alternatively, you can also pass the AbstractControl directly.

By using this approach, when the control is invalid and in our case touched, the directive will render a p element with the ngx-forms-error class underneath the input.

Custom component

Of course, in many projects we do not simply want to add a p element. Instead, we wish to use our own custom component where we can add an icon, custom styling and even transform our provided strings using a translation package.

We can do this by providing a custom component to the component property in the configuration.

// Root
    providers: [
		{
            provide: NgxFormsErrorsConfigurationToken,
            useValue: {
                errors: {
                    required: 'This is a required field.',
                    email: 'This field is not a valid email address.'
                },
                showWhen: 'touched',
            component: CustomErrorComponent,
            }
        },
    ]

This CustomErrorComponent has to extend the NgxFormsErrorAbstractComponent. This will provide the component with several inputs that can be used in our custom component.

The most important Input is the errors. This is an array of strings which will contain all error messages that we wish to show in the error component.

The second Input is the errorKeys input, which provides us with an array of keys that are found in the validation errors.

On top of that, the data input provides us with the actual ValidationErrors on the control.

Multiple errors

By default, the directive only renders a single error, the first one that gets provided in the validation errors object. If we wish to show more errors, we can provide the show property in the configuration.

We can either provide a specific number of errors we wish to see or provide the option all to see all errors.

Save on exit

When building forms, we often want to have the ability to notify the user that they're about to leave a filled in form without saving. The ngx-forms package offers a workflow that allows you to handle this customizable way.

NgxSaveOnExitComponent

Starting with the NgxSaveOnExitComponent, we provide an abstract component that has all the required methods to handle the flow.

Consisting of two methods, the isDirty method is the most important one. This method will let the service and the guard know whether or not the component and its form are dirty and need to trigger the save-on-exit flow.

The isValid method will allow you to handle different flows whenever the filled in form is valid or not. For instance, when showing a modal to the user, we can prompt that the form is not valid and therefor cannot be saved to the API.

The component also comes with a window:beforeunload handler. This means that whenever the user closes the tab or the browser, we can prompt the user that the form has stopped it from closing the window. By default, this behavior is not enabled, but it can be enabled by setting allowBeforeUnloadHandler to true.

NgxSaveOnExitAbstractService

Next up, the NgxSaveOnExitAbstractService allows us to handle custom behavior to the dirty state of the component and its form.

This service can be provided globally or can be provided in individual components/modules where you wish to handle the flow differently.

The handleDirtyState gets the instance of the component and allows you to decide however you wish to handle your save-on-exit flow. Do you simply want to show a toast message and let the user route? No problem! Do you wish to open a modal and let the user decide? It's all up to you.

If you provide the service on root level and wish to bypass certain routes under certain circumstances, you can do so by implementing the optional bypassSaveOnExit method.

NgxSaveOnExitGuard

Once your component has implemented the NgxSaveOnExitComponent abstract and a service was provided for the NgxSaveOnExitAbstractService, you can apply the NgxSaveOnExitGuard on your route.

The guard will detect whenever the user tries to route away from the component and from there on will handle the entire save-on-exit flow.