Blocks
Sometimes you need additional information or functionality within a form. This could be the case for example if you need to add an information block, images or anything that does not contribute to the forms value.
In this case you can define a Block.
Minimal Setup
Section titled “Minimal Setup”Here is an example of a simple information block.
First create an interface for your block by extending NgxFwBlock
.
export interface InfoBlock extends NgxFwBlock { type: 'info-block'; message: string;}
Then implement the component.
@Component({ selector: 'app-info-block', imports: [], templateUrl: './info-block.component.html', viewProviders: [ { provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }), }, ], hostDirectives: [ { directive: NgxfwBlockDirective, inputs: ['content', 'name'], }, ],})export class InfoBlockComponent { // 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 blockDirective = inject(NgxfwBlockDirective<InfoBlock>);
// Explicitly setting a type definition is not required, but some IDEs work better if they are present readonly content: Signal<InfoBlock> = this.blockDirective.content;
// You also have access to the underlying form readonly rootForm = this.blockDirective.rootForm;
// We get proper type information when accessing this.content() readonly message = computed(() => this.content().message); // <- This is the custom property for your block}
<p>{{ message() }}</p>
Finally, register the block in app.config.ts
export const appConfig: ApplicationConfig = { providers: [ // other providers provideFormwork({ componentRegistrations: { info: InfoBlockComponent, }, }), ],};
Configuration
Section titled “Configuration”Checkout Configuration for how to configure a Block. A Block only has access to the base properties.
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 InfoBlockComponent { private readonly blockDirective = inject(NgxfwBlockDirective<InfoBlock>);
readonly message = computed(() => this.content().message);
// 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.blockDirective.isHidden;
constructor() { // Let formwork know, that you take care of handling visibility this.blockDirective.setVisibilityHandling('manual'); }}
@if(!isHidden()){<p>{{ message() }}</p>}
Test ID
Section titled “Test ID”To make testing easier, a base test id will always be generated for you.
Default Behavior
Section titled “Default Behavior”By default, the test id is just equal to the control’s id
, prepended with its parent’s test id if it’s part of a group. For example, a control with id: 'firstName'
inside a group with testId: 'user-details'
would have a final test id of 'user-details-firstName'
. If the control is at the root level, its test id would simply be 'firstName'
.
Customizing Test IDs
Section titled “Customizing Test IDs”You can customize how test IDs are generated by providing a testIdBuilderFn
. This function receives the control’s content and its parent’s test ID (if any) and should return the desired test ID string.
There are two ways to provide a custom testIdBuilderFn
:
Global Configuration
Section titled “Global Configuration”You can define a global testIdBuilderFn
when you set up Formwork using provideFormwork
. This function will be used for all controls unless overridden at the component level.
provideFormwork({ // ... globalConfig: { testIdBuilderFn: (content, parentTestId) => { return `${parentTestId ? parentTestId + '_' : ''}${content.type}-${content.id}`; }, },});
Control-Specific Configuration
Section titled “Control-Specific Configuration”Individual components can also define their own testIdBuilderFn. This is done within the component’s logic by calling setTestIdBuilderFn
on the injected NgxfwControlDirective
. This allows for fine-grained control over how test IDs are generated for specific components
This example demonstrate how to do this for a Control, but it works exactly the same for Groups and Blocks.
@Component({ // ...})export class TextControlComponent { private readonly control = inject(NgxfwControlDirective<TextControl>);
constructor() { this.control.setTestIdBuilderFn(simpleTestIdBuilder); }}
Precedence
Section titled “Precedence”The test ID generation follows this order of precedence:
- Control-Specific testIdBuilderFn: If a function is set directly on the control instance.
- Global testIdBuilderFn: If defined in the provideFormwork configuration.
- Default Behavior: If no custom builder function is provided.
This system provides flexibility, allowing you to set a general rule for test IDs across your application while still being able to override it for specific cases.
Here is an example of how to access the test ID.
@Component({ selector: 'app-info-block', imports: [], templateUrl: './info-block.component.html', viewProviders: [ { provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }), }, ], hostDirectives: [ { directive: NgxfwBlockDirective, inputs: ['content', 'name'], }, ],})export class InfoBlockComponent { private readonly blockDirective = inject(NgxfwBlockDirective<InfoBlock>); readonly testId: Signal<string> = this.blockDirective.testId; readonly message = computed(() => this.content().message);}
<p [attr.data-testId]="testId()">{{ message() }}</p>