Skip to content

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.

Here is an example of a simple information block.

First create an interface for your block by extending NgxFwBlock.

info-block.type.ts
export interface InfoBlock extends NgxFwBlock {
type: 'info-block';
message: string;
}

Then implement the component.

info-block.component.ts
@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
}
info-block.component.html
<p>{{ message() }}</p>

Finally, register the block in app.config.ts

app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
// other providers
provideFormwork({
componentRegistrations: {
info: InfoBlockComponent,
},
}),
],
};

Checkout Configuration for how to configure a Block. A Block only has access to the base properties.

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):

  1. If content.hidden is an expression string, it’s evaluated against the current form values
  2. If no hidden expression is defined, the control inherits the hidden state from its parent group
  3. Both conditions can be combined - a control is hidden if either its own condition evaluates to true OR its parent group is hidden
info-block.component.ts
@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');
}
}
info-block.component.html
@if(!isHidden()){
<p>{{ message() }}</p>
}

To make testing easier, a base test id will always be generated for you.

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'.

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:

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}`;
},
},
});

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.

text-control.component.ts
@Component({
// ...
})
export class TextControlComponent {
private readonly control = inject(NgxfwControlDirective<TextControl>);
constructor() {
this.control.setTestIdBuilderFn(simpleTestIdBuilder);
}
}

The test ID generation follows this order of precedence:

  1. Control-Specific testIdBuilderFn: If a function is set directly on the control instance.
  2. Global testIdBuilderFn: If defined in the provideFormwork configuration.
  3. 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.

info-block.component.ts
@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);
}
info-block.component.html
<p [attr.data-testId]="testId()">{{ message() }}</p>