Basics
Events
Layout
Advanced Modeling
Child Forms
A child form is a property of a model that:
- Has a type that is also a model AND;
- Explicitly declared as a
childForm
A property in a modal with a type that is also a model is a potential child form:
@FormModel()
class Hero {
nemesis: Hero;
}
nemesis is a property in the Hero
model. The type of nemesis is Hero
which is a model thus it is a potential child form.
Potential because it also requires explicit declaration:
@FormModel()
class Hero {
@FormProp({
childForm: true
})
nemesis: Hero;
}
Nested models
Model transform to FormGroup
, A child form is a model which means it also transform to FormGroup
.
The end result is a FormGroup
containing another FormGroup
.
In @angular/forms
, when a FormGroup
contains one or more FormGroup
instances, directly or indirectly, they are referred to as
nested models or nested forms.
Directly: The nested model is a value of a property on the parent.
Indirectly: The nested model is an item in a FormArray
instance that is a value of a property on the parent
A known model
Angular forms does not recognize types, only structures. A form is an instance of FormGroup
that is the root control.
The library is based on types and classes. In nForm the definition for a form is a bit more specific,
a form is a FormGroup
from an instance of a known model.
A
known model
is a class decorated with
@FormModel()
. The class is "known" to the library.
Explicitly declared
A child form is described as a property with a type that is a model, i.e. a child form is a known model which means that a child form is also a form by itself, being the root to of the child controls in it.
The other requirement is explicit declaration which means there is another state, a known model that is not a child form.
A child form is then a flag, this flag affect both logical and
visual aspects which is not covered by this chapter, the only thing we
need to know is that a known model always transform into a
FormGroup
weather it is a child form or not.
Known models that are not a child form are handled by other complex data structures, we will cover these scenarios in the relevant chapters.
Back to our model:
@FormModel()
class Hero {
@FormProp({
childForm: true
})
nemesis: Hero;
}
The nemesis
property in the Hero
class is a child form because it:
- Refer to a known model, the
Hero
model - Explicitly declared as a child form via
childForm: true
Adding address
To demonstrate child forms we need to extend our model, we need to
create a new model class and add a property to Hero
that has the
new model as type.
We create the HeroAddress
model, a flat structure that represents an address:
@FormModel()
export class HeroAddress {
@FormProp({ vType: 'text' }) street: string;
@FormProp({ vType: 'text' }) city: string;
@FormProp({ vType: 'text' }) zip: string;
}
Adding the property to Hero
:
@FormProp({
required: true,
render: {
vType: 'form',
label: 'Address'
},
childForm: true
})
address: HeroAddress;
Displaying a child form
We've mentioned that a child form is a form by itself, if we take an
instance of Hero
with the address property set with an instance
of HeroAddress
we can use the nform component to display the address:
<pbl-nform [model]="hero.address" ></pbl-nform>
But how do we display a child form as a child, what will display for address when we display the hero instance
<pbl-nform [model]="hero" ></pbl-nform>
This is where visual display becomes challenging, there are multiple ways to display nested forms, some are based on preferred style and some based on the context. The challenge is to allow freedom so the user can choose the one right for him.
Common visual styles are inline and external. External options are dialog window, tabs, pages etc...
The library provide tools that help achieve this freedom, either through inline control override or through a concrete renderer implementation. The renderer can choose a strict "my way" implementation, An implementation that pass the responsibility to the user or something in between a configuration driven (metadata) approach.
There is always a trade-off, strict is simple to use but not flexible and as we offer more flexibility we also add more complexity for the user.
Child from using inline control override
The magic happens in the template, we use the ExplodeChildFormPipe
pipe to create
a list of controls for our child form and iterate over them.
For each control we render a new row.
We apply a lot of re-use here which leads to minimal work.
Child from using the renderer
In this tutorial we use the material renderer. The material renderer does not render child forms, instead it renders an edit button which when clicked emits the (rendererEvent) event, a listener should handle the display of the child form.
We will not use the (rendererEvent), instead we will use a control outlet. Using event's is an excellent solution but we want to demonstrate the use of outlet, (rendererEvent) is used in the chapter about Arrays
Trying to navigate through complexity, the render implementations that ship with this package will take the less complex options, i.e. using the basic non configurable implementation which the user can override using tools from the library.
Control Outlet
We covered control outlet's in the basic section of the tutorial. Outlet's allow to replace the display of a control while showing the original control somewhere else. This is great for tabs, dialogs, etc...
In the following example we want show the HeroAddress
child form in
a different view as an external form.
To do that we define an outlet in the left drawer that will open once the user want's to edit the address.
To interact with the drawer we use a structural outlet that shows an edit button to open/close the left drawer when clicked. The original location of the address control is replaced with the template we define for the outlet.
Because the material renderer use a dynamic form to render child forms we know a complete new dynamic form will display in the left drawer.
Notice how the required error appears next to the edit button and disappear after clicking the edit button, why is that?
The address property has a required
validation. The model we are
showing is a new model with an empty address thus the error.
When the user click's on the edit we need to create a new address.
But how can we create the FormGroup
? We do that using the
control
panel discussed earlier.
editExternalForm(ctx: DynamicControlRenderContext): void {
this.rightDrawerOpened = true;
if (ctx.fControl.value === null) {
const heroAddressFormGroup = ctx.tdmForm.createControl(ctx.item.fullName, null, true);
ctx.fGroup.setControl(ctx.item.name, heroAddressFormGroup);
ctx.item.markAsChanged();
}
}
With ctx.fControl.value === null
we check if the address is null
which means empty, this will always be null when a child form is not
set.
When null
we need to create the FormGroup
which we do by calling
ctx.tdmForm.createControl(ctx.item.fullName, null, true)
which mean
create a control for the type in tx.item.fullName
, which I don't have
a value for (null
) but I want you to create a new one (true
).
Once we have the new FormGroup
instance we need to set it instead of
the current one, which is null
. We use the parent form group (which is
the form group for the hero model) and we set the address property
of it to our new form group (ctx.item.name
).
The last thing we need to do is to mark that this control has changed so it will be picked up by change detection.
A note on the renderer
Although custom renders are covered in specific section dedicated to the renderer this topic involves some aspects on the user side.
Custom renderer implementation can offer flexibility by extending the metadata a user can provide for a child form and using that to take decisions. These are sort of presets of layouts the user can choose from.
This way add's a lot of complexity on the renderer's implementation and also on the user's as more options might lead to confusion.
Remember that the user can always use outlets and creating a solution that covers all scenarios is not practical, this is why we chose the basic approach.
That said, it might be that the same visual style is applied repeatedly in many forms and the same behaviour using the same outlet configuration is done over and over. This is where adding some logic might help and creating a preset might simplify the process.