Skip to content

Groups

A group is used to group controls together. It results in an Angular FormGroup instance.

Most of the time you will only need one or two different group types. Where they really are handy though, is if you need different behaviors. For example one that does not do anything special visually and one that is collapsible.

Here is an example of a simple group. Most likely you will only set up one or two group components, if at all.

First create an interface for your group.

group.type.ts
export interface Group extends NgxFwFormGroup {
// Unique Key of your group that is used for differentiating groups
type: 'group';
// Additional options only applicable to this group if you want
}

Then implement the component.

Warning Be sure to bind to [formGroupName] on an element (e.g. div, ng-container)

group.component.ts
@Component({
selector: 'ngxfw-group',
imports: [NgxfwAbstractControlDirective, ReactiveFormsModule],
templateUrl: './group.component.html',
viewProviders: [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true }),
},
],
hostDirectives: [
{
directive: NgxfwGroupDirective,
inputs: ['content'],
}
],
})
export class GroupComponent {
// 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(NgxfwGroupDirective<Group>);
// Explicitly setting a type definition is not required, but some IDEs work better if they are present
readonly content: Signal<Group> = this.control.content; // The configuration object of the group instance
readonly id = computed(() => this.content().id);
readonly controls: Signal<NgxFwContent[]> = this.control.controls;
}
group.component.html
<!-- Just an example -->
<div [formGroupName]="id">
@for (control of controls(); track control.id) {
<ng-template *ngxfwNgxfwAbstractControl="control" />
}
</div>

Finally, register the group in app.config.ts

app.config.ts
componentRegistrations: {
group: GroupComponent
}

Checkout Configuration for how to configure a group.

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
group.component.ts
@Component({
// ...
})
export class GroupComponent {
private readonly control = inject(NgxfwGroupDirective<Group>);
// 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 visibility
this.control.setVisibilityHandling('manual')
}
}
group.component.html
@if(isHidden()){
<span>Some placeholder you want to use</span>
}
@if(!isHidden()){
<div [formGroupName]="id">
@for (control of controls(); track control.id) {
<ng-template *ngxfwNgxfwAbstractControl="control" />
}
</div>
}

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:

StrategyEffect
keep (default)The control remains in the form model even when hidden
removeThe control is removed from the form model when hidden

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:

StrategyEffect
default (default)Sets the specified default value
lastPreserves the last input value
resetResets the value using the reactive forms api

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

  1. If content.disabled is a boolean, that value is used directly
  2. If content.disabled is an expression string, it’s evaluated against the current form values
  3. 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.

group.component.ts
@Component({
// ...
})
export class GroupComponent {
private readonly control = inject(NgxfwGroupDirective<Group>);
readonly disabled: Signal<boolean> = this.control.disabled;
}
group.component.html
<div [formGroupName]="id">
@for (control of controls(); track control.id) {
<ng-template *ngxfwNgxfwAbstractControl="control" />
}
</div>
<!-- Only show hint when group is disabled -->
@if(disabled()){
<span>This is no longer relevant</span>
}

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

  1. If content.readonly is a boolean, that value is used directly
  2. If content.readonly is an expression string, it’s evaluated against the current form values
  3. 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.

group.component.ts
@Component({
// ...
})
export class GroupComponent {
private readonly control = inject(NgxfwGroupDirective<Group>);
readonly readonly: Signal<boolean> = this.control.readonly;
}
group.component.html
<div [formGroupName]="id">
@for (control of controls(); track control.id) {
<ng-template *ngxfwNgxfwAbstractControl="control" />
}
</div>
@if(readonly()){
<span>This cannot be edited</span>
}

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.

group.component.ts
@Component({
// ...
})
export class GroupComponent {
private readonly control = inject(NgxfwGroupDirective<Group>);
readonly testId: Signal<string> = this.control.testId;
}
group.component.html
<div [formGroupName]="id" [attr.data-testId]="testId() + '-wrapper'">
@for (control of controls(); track control.id) {
<ng-template *ngxfwNgxfwAbstractControl="control" />
}
</div>

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 directive
private readonly group = inject(NgxfwGroupDirective<Group>);
// Get access to the underlying form group}
get group() {
return this.group.formControl
}

Then, in your template you can do something like this

@if(group?.hasError('required')) {
<span>Required</span>
}