Skip to content

Improvements & DRY Code

To reduce the amount of boilerplate needed with each component and to improve maintainability, you can set up a few helper objects. This way, should anything change, you only need to update one file.

  • If you create helper files yourself, you can place them in any folder in your project.
  • Run schematics with the --helper flag and use --helperPath <path> to point to your helper directory (default: src/app/shared/helper).
  • The schematic will look in that path for files named exactly:
    • control-container.view-provider.ts
    • control.host-directive.ts
    • group.host-directive.ts
    • block.host-directive.ts

When running schematics, pass the flags to use your files:

Terminal window
ng generate ngx-formwork:<schematic> --helper --helperPath src/app/shared/helper

Or set these defaults in angular.json under your project’s schematics section:

"ngx-formwork:control": { "helper": true, "helperPath": "src/app/shared/helper" },

ControlContainer is required for all controls and groups that will be used within ngx-formwork. Injection of the control container allows the components to use reactive forms functionality, without needing to pass the form group through inputs and wrapping the template into additional tags. See this YouTube Video for more detailed explanation: How to Make Forms in Angular REUSABLE (Advanced, 2023)

control-container.view-provider.ts
export const controlContainerViewProviders = [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true }),
},
];
text-control.component.ts || group.component.ts
@Component({
// Other component decorator options
viewProviders: controlContainerViewProviders,
})

This is a convenience helper to apply the NgxfwControlDirective.

control.host-directive.ts
export const ngxfwControlHostDirective = {
directive: NgxfwControlDirective,
inputs: ['content', 'name'],
};

Use it like this:

text-control.component.ts
@Component({
// Other component decorator options
hostDirectives: [
// Apply here
ngxfwControlHostDirective
],
})

This is a convenience helper to apply the NgxfwGroupDirective.

group.host-directive.ts
export const ngxfwGroupHostDirective = {
directive: NgxfwGroupDirective,
inputs: ['content', 'name'],
};

Use it like this:

group.component.ts
@Component({
// Other component decorator options
hostDirectives: [
// Apply here
ngxfwGroupHostDirective
],
})

For official documentation of Union Types checkout the official docs.

Setting up a union type for your own controls is highly recommended, as it gives you much better type safety, when writing your forms in TypeScript.

export type MyAppControls = TestTextControl | TestGroup | InfoBlock;

Registering all controls, validators, etc. directly in the app.config.ts is not ideal. ngx-formwork provides multiple approaches to organize your code better.

Create a file next to your app.config.ts with this content to get started. The defineFormworkConfig function is a helper that provides type support when defining the configuration in a separate file.

formwork.config.ts
import { defineFormworkConfig } from 'ngx-formwork';
export const formworkConfig = defineFormworkConfig({
componentRegistrations: {
// Component registrations go here
},
// validatorRegistrations are optional
validatorRegistrations: {
// Validator registrations go here
},
// asyncValidatorRegistrations are optional
asyncValidatorRegistrations: {
// Async Validator registrations go here
},
});

In app.config.ts use it like this:

app.config.ts
import { formworkConfig } from './formwork.config.ts';
export const appConfig: ApplicationConfig = {
providers: [
// other providers
provideFormwork(formworkConfig)
]
};

For more advanced code organization, you can leverage Angular’s dependency injection system by providing the tokens directly.

component-registrations.provider.ts
import { NGX_FW_COMPONENT_REGISTRATIONS } from 'ngx-formwork';
import { TextControlComponent } from './components/text-control.component';
import { GroupComponent } from './components/group.component';
import { InfoBlockComponent } from './components/info-block.component';
export const componentRegistrationsProvider = {
provide: NGX_FW_COMPONENT_REGISTRATIONS,
useValue: new Map([
['text-control', TextControlComponent],
['group', GroupComponent],
['info', InfoBlockComponent],
// more registrations...
])
};
validator-registrations.provider.ts
import { NGX_FW_VALIDATOR_REGISTRATIONS, NGX_FW_ASYNC_VALIDATOR_REGISTRATIONS } from 'ngx-formwork';
import { Validators } from '@angular/forms';
import { letterValidator, noDuplicateValuesValidator, forbiddenLetterAValidator } from './validators';
import { asyncValidator, asyncGroupValidator } from './async-validators';
// Synchronous validators
export const validatorRegistrationsProvider = {
provide: NGX_FW_VALIDATOR_REGISTRATIONS,
useValue: new Map([
['min-chars', [Validators.minLength(3)]],
['letter', [letterValidator]],
['combined', [Validators.minLength(3), Validators.required, letterValidator]],
['no-duplicates', [noDuplicateValuesValidator]],
['forbidden-letter-a', [forbiddenLetterAValidator]],
// more registrations...
])
};
// Asynchronous validators
export const asyncValidatorRegistrationsProvider = {
provide: NGX_FW_ASYNC_VALIDATOR_REGISTRATIONS,
useValue: new Map([
['async', [asyncValidator]],
['async-group', [asyncGroupValidator]],
// more registrations...
])
};

In app.config.ts use them like this:

app.config.ts
import { componentRegistrationsProvider } from './component-registrations.provider';
import { validatorRegistrationsProvider, asyncValidatorRegistrationsProvider } from './validator-registrations.provider';
export const appConfig: ApplicationConfig = {
providers: [
// other providers
provideFormwork(),
// Custom providers MUST come after provideFormwork()
componentRegistrationsProvider,
validatorRegistrationsProvider,
asyncValidatorRegistrationsProvider,
]
};

Multiple Configurations with Injection Tokens

Section titled “Multiple Configurations with Injection Tokens”

You can also provide multiple configuration objects that will be merged according to their resolution strategy:

split-configurations.provider.ts
import { NGX_FW_COMPONENT_REGISTRATIONS, NGX_FW_VALIDATOR_REGISTRATIONS, NGX_FW_CONFIG } from 'ngx-formwork';
// First set of components
export const baseComponentsProvider = {
provide: NGX_FW_COMPONENT_REGISTRATIONS,
useValue: new Map([
['text', TextComponent],
['number', NumberComponent],
])
};
// Additional components from a different module
export const extraComponentsProvider = {
provide: NGX_FW_COMPONENT_REGISTRATIONS,
useValue: new Map([
['date', DateComponent],
['select', SelectComponent],
])
};
// Multiple global configs will be deep merged
export const baseConfigProvider = {
provide: NGX_FW_CONFIG,
useValue: {
testIdBuilderFn: (baseName, controlName) => `${baseName}-${controlName}`,
}
};
export const moduleConfigProvider = {
provide: NGX_FW_CONFIG,
useValue: {
extraSettings: {
theme: 'dark',
}
}
};

For simpler scenarios, you can still split your registration files by type while using the provideFormwork() function.

Create a file with the following content, at whatever location makes sense.

controls.registerations.ts
export const componentRegistrations: ComponentRegistrationConfig = {
'text-control': TextControlComponent,
group: GroupComponent,
info: InfoBlockComponent,
// more regsitrations...
};

In app.config.ts use it like this

app.config.ts
import { componentRegistrations } from './controls.registerations.ts';
export const appConfig: ApplicationConfig = {
providers: [
// other providers
provideFormwork({
componentRegistrations
})
]
};

In formwork.config.ts use it like this

app.config.ts
import { componentRegistrations } from './controls.registerations.ts';
export const formworkConfig = defineFormworkConfig({
// other providers
componentRegistrations,
});

Create a file with the following content, at whatever location makes sense. You can also further split the files between sync and async validators

validators.registerations.ts
export const validatorRegistrations: ValidatorConfig<RegistrationRecord> = {
'min-chars': [Validators.minLength(3)],
letter: [letterValidator],
combined: ['min-chars', Validators.required, 'letter'],
'no-duplicates': [noDuplicateValuesValidator],
'forbidden-letter-a': [forbiddenLetterAValidator],
};
export const asyncValidatorRegistrations: AsyncValidatorConfig<RegistrationRecord> = {
async: [asyncValidator],
'async-group': [asyncGroupValidator],
};

In app.config.ts use it like this

app.config.ts
import { validatorRegistrations, asyncValidatorRegistrations } from './validators.registerations.ts';
export const appConfig: ApplicationConfig = {
providers: [
// other providers
provideFormwork({
validatorRegistrations,
asyncValidatorRegistrations,
})
],
};

In formwork.config.ts use it like this

formwork.config.ts
import { componentRegistrations } from './controls.registerations.ts';
export const formworkConfig = defineFormworkConfig({
// other providers
componentRegistrations: {
validatorRegistrations,
asyncValidatorRegistrations,
},
});
misspelled.validators.registerations.ts
export const validatorRegistrations: ValidatorConfig<RegistrationRecord> = {
letter: [letterValidator],
// ⚠️ letter only spelled with one T.
// This will give an TS error in the provideFormwork function, but not in this case
combined: [Validators.required, 'leter'],
};

Global Configuration with Injection Tokens

Section titled “Global Configuration with Injection Tokens”

For advanced scenarios, you can provide global configuration options using the NGX_FW_CONFIG injection token.

global-config.provider.ts
import { NGX_FW_CONFIG } from 'ngx-formwork';
import { TestIdBuilderFn } from 'ngx-formwork';
// Example test ID builder function
const testIdBuilder: TestIdBuilderFn = (baseName, controlName) => {
return `${baseName}-${controlName}`;
};
export const globalConfigProvider = {
provide: NGX_FW_CONFIG,
useValue: {
testIdBuilderFn: testIdBuilder
}
};

In app.config.ts use it like this:

app.config.ts
import { globalConfigProvider } from './global-config.provider';
export const appConfig: ApplicationConfig = {
providers: [
// other providers
provideFormwork(),
globalConfigProvider
]
};