Controls
A control can be whatever you need it to be. It can be as generic as a TextControl
. be more specific like an EMailControl
, just wrap existing controls like a DateRangeControl
or have custom logic like a SearchableDropdownControl
.
Minimal Setup
Section titled “Minimal Setup”Here is an example of a simple text control.
First create an interface for your control.
export interface TextControl extends NgxFwControl { // Unique Key of your control that is used for differentiating controls // This can be descriptive like "email-control" type: 'text-control';
// Overwrite defaultValue with correct value type // the default value type is "unknown" defaultValue?: string;
// Additional options only applicable to this control hint?: string;}
Then implement the component.
Warning Be sure to bind to
[formControlName]
on the actual input element
@Component({ selector: 'app-text-control', imports: [ReactiveFormsModule], templateUrl: './text-control.component.html', viewProviders: [ { provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }), } ], hostDirectives: [ { directive: NgxfwGroupDirective, inputs: ['content'], } ],})export class TextControlComponent { // Inject the Directive to gain access to all public properties // Make sure to pass the correct type parameter to get proper type information private readonly control = inject(NgxfwControlDirective<TextControl>);
// Explicitly setting a type definition is not required, but some IDEs work better if they are present readonly content: Signal<TextControl> = this.control.content; // The configuration object of the control instance
// We get proper type information when accessing this.content() readonly hint = computed(() => this.content().hint); readonly label = computed(() => this.content().label); readonly id = computed(() => this.content().id);}
<!-- Just an example --><label [htmlFor]="id()">{{ label() }}</label><input [id]="id()" [formControlName]="id()"/><span>{{hint()}}</span>
Finally, register the control in app.config.ts
componentRegistrations: { text: TextControlComponent}
Configuration
Section titled “Configuration”Checkout Configuration for how to configure a control.
Hidden
Section titled “Hidden”By default, the visibility is handled by formwork. It will set the hidden
attribute on your component, if a control or group should not be visible.
You have the option to handle this by yourself, but can only be configured on a component level.
It can be useful for when you want to work with @if
to actually create and destroy components used in the template, or you want to show some placeholder instead.
The hidden state is determined using the following priority (content
could be any of your controls or groups):
- If
content.hidden
is an expression string, it’s evaluated against the current form values - If no
hidden
expression is defined, the control inherits the hidden state from its parent group - Both conditions can be combined - a control is hidden if either its own condition evaluates to true OR its parent group is hidden
@Component({ // ...})export class TextControlComponent { private readonly control = inject(NgxfwControlDirective<TextControl>); // Really only should ever be a boolean return value, but an expression could also return a number, string or object readonly isHidden: Signal<unknown> = this.control.isHidden;
constructor() {// Let formwork know, that you take care of handling visibilitythis.control.setVisibilityHandling('manual')}}
@if(isHidden()){ <span>Some placeholder you want to use</span>}@if(!isHidden()){ <label [htmlFor]="id()">{{ label() }}</label> <input [id]="id()" [formControlName]="id()" /> <span>{{hint()}}</span>}
Hide Strategy
Section titled “Hide Strategy”Besides visually hiding a control or group, the hidden state can have different effects depending on how this is handled in code.
This is relevant for when you have a hidden control, but still want to access its value through this.form.value
or this.form.getRawValue()
.
The following strategies are available:
Strategy | Effect |
---|---|
keep (default) | The control remains in the form model even when hidden |
remove | The control is removed from the form model when hidden |
Value Strategy
Section titled “Value Strategy”There are different strategies for handling values during visibility changes. This allows you to control exactly what should happen with the values, if their control or group is being hidden.
The following strategies are available:
Strategy | Effect |
---|---|
default (default) | Sets the specified default value |
last | Preserves the last input value |
reset | Resets the value using the reactive forms api |
Disabled
Section titled “Disabled”formwork will use Angulars reactive forms API to disable and enable a control or group. You can get access to the disabled state and use it in your template for whatever makes sense.
The disabled state is determined using the following priority (content
could be any of your controls or groups):
- If
content.disabled
is a boolean, that value is used directly - If
content.disabled
is an expression string, it’s evaluated against the current form values - If no
disabled
property is defined, the control inherits the disabled state from its parent group
This hierarchical inheritance ensures that child controls are automatically disabled when their parent group is disabled, unless explicitly overridden.
@Component({ // ...})export class TextControlComponent { private readonly control = inject(NgxfwControlDirective<TextControl>); readonly disabled: Signal<boolean> = this.control.disabled;}
<label [htmlFor]="id()">{{ label() }}</label><input [id]="id()" [formControlName]="id()" /><!-- Only show hint when control is disabled -->@if(disabled()){ <span>{{hint()}}</span>}
Readonly
Section titled “Readonly”formwork does not mark a control as readonly
, because there is no useful API for reactive forms to achieve this. But you can provide an expression, which will be evaluated, and then use that value in your template.
The readonly state is determined using the following priority (content
could be any of your controls or groups):
- If
content.readonly
is a boolean, that value is used directly - If
content.readonly
is an expression string, it’s evaluated against the current form values - If no
readonly
property is defined, the control inherits the readonly state from its parent group
This hierarchical inheritance ensures that child controls are automatically set to readonly when their parent group is readonly, unless explicitly overridden.
@Component({ // ...})export class TextControlComponent { private readonly control = inject(NgxfwControlDirective<TextControl>); readonly readonly: Signal<boolean> = this.control.readonly;}
<label [htmlFor]="id()">{{ label() }}</label><input [id]="id()" [formControlName]="id()" [attr.readonly]="readonly() || null"/><span>{{hint()}}</span>
Test ID
Section titled “Test ID”To make testing easier, a base test id will always be generated for you. You can combine it with other information to easily identify parts of your component.
By default the test id is just equal to the controls id.
@Component({ // ...})export class TextControlComponent { private readonly control = inject(NgxfwControlDirective<TextControl>); readonly testId: Signal<string> = this.control.testId;}
<label [htmlFor]="id()" [attr.data-testId]="testId() + '-label'" >{{ label() }}</label><input [attr.data-testId]="testId() + '-input'" [id]="id()" [formControlName]="id()"/>
Showing Errors
Section titled “Showing Errors”Showing errors works pretty much the same as always. You get access to the form control and then access hasError
.
In TypeScript set up a getter
// inject the instance of the directiveprivate readonly textControl = inject(NgxfwControlDirective<Control>);
// Get access to the underlying form textControl}get formControl() { return this.textControl.formControl}
Then, in your template you can do something like this
@if(formControl?.hasError('required')) { <span>Required</span>}