Angular dynamic form generator
Prerequisites: A basic understanding of angular, angular’s reactive forms and basic recursion.
Angular tries its best to make form creation an enjoyable experience. They even offer a FormBuilder service for building groups and arrays of controls. With the form builder you can create abstracted objects that represent a FormGroup, let’s call the group baseball_team
that can have a property on it representing a FormArray, we’ll call players
. Players could then hold several FormControls representing each player.
Thanks angular. This example presents and elegant OOP way of handling a form’s creation/handling but your app isn’t as simple as a baseball team with players. You can’t assign a specific form class title with well defined relationships because the form you need has to be entirely polymorphic.
Your form needs to be able to receive an object of unknown size and shape and convert it into a giant form group, containing as many nested form groups as needed to represent the nested relationships. The component we will be building will be capable of handling an object representation of a baseball team, app settings or a bimolecular structure (if you can find a way to represent it in a javascript object).
Before we get started let’s look through a quick example of what we’re looking to achieve.
We just got access to the MLB’s public API and are successfully querying baseball teams. We take a glance at json response that we receive back and its a bit more complicated then we had hoped for:
For the sake of this argument let’s say that you are working on the GUI needed to update these player and teams. Sure you could make 4 separate forms and it would look like:
- A list of teams with a view/form dedicated to the team form containing the team name, team leaders, transaction, ect.
- A list of team players, a player edit view/form containing basic player information to update.
- If you really have the chops you could make a dynamic dropdown of every stat’s year followed by a form for the values. That would mean managing another dynamic form abstraction just for this portion.
This is still a simple example using a basic baseball team structure. The real world of development demands the consumption of structures unruly and often without much reason.
Take a quick look at the final structure we’ll have once our dynamic form is created. Once our http client intercepts this response and converts it to a javascript object, we will feed it into our component and it will be converted to a form group structure representing this rough design:
This is just a layout with a few of the teams values but displays the most important aspects of our form group’s structure we will be scaffolding:
Just from this example you can see why making several forms and abstractions will not only take an enormous amount of effort but will litter your code base with endless abstractions. We want an all in one way for ideally an administrative user to update any of these fields at their lea-sure.
And with that, let’s dig into our implementation.
Step 1: Build a component for building our form group
Generate a basic angular component within a directory that will house all components needed for the dynamic form to work. It does not need to implement NgInit or NgChanges.
Lets peek at what our form-layout component will look like and break down its functionality. This piece contains the lower level logic for converting our javascript objects to angular Form groups and thus will appear a bit cryptic.
Heres the component’s breakdown:
Defining an enum to represent the different types a control can take on helps guide any further implementation on the component in the future. For now, these four native types are all we’re concerned with
Component must implement OnChanges so that the form model is properly alerted whenever a control field is updated.
formMappings will be the actual javascript object that is passed into the component in need of converting.
updateForm as stated will be called when the submit button is clicked. Defining this input as being an EventEmitter type allows for a custom method to be passed into our form and will be percolated back up to its parent whenever the form is updated.
submitBtnText allows for changing the submit buttons text, otherwise defaults to a common naming.
When generating our control groups, it’s vital to know what type of control group needs generated. This determines if a dropdown, toggle, text field or larger group will be rendered to the user.
buildForm is called every time our form is updated to ensure all values are re-rendered correctly. Defining our rootFormGroup is the primary purpose of this component as it’s used to define our angular form in the markup. We will first look at the addControlGroups method called and then see how that returned value is used within the splitGroups method.
This method will accept our javascript object passed into the component no matter what its size or shape. Its purpose is to define the structure required by Angular’s form builder in order to build a legitimate form group. If you need a refresher on the requirements of this format, see https://angular.io/guide/reactive-forms
Line 75–78: loop through the object, assigning each mapping’s value as our control value and control type as the value returned when passing our control value to the getControlType().
Line 80–87: if our control type is an object, that means we need to dig down a level deeper into our data structure to define the next set of controls. After making sure our object has actual values defined for it, define this “new group” by recursively calling this same addControlGroups() method. This recursive behavior allows us to handle objects with an infinite (I say that lightly) amount of nested structures. Once that new group (or group of groups of groups) is defined, we add it to our defined controls object defined on line 75.
Line 89–95: if the control type is an array, we add the array value as it is to our controls structure. Otherwise, we build our own object with its value and rather its disabled. Change disabled to true if you want your fields to be changed. These values are wrapped as arrays per the format required for the form control’s group method. This format again is defined here . Finally we provide the controls structure we scaffolded to the form builders group method which will create the appropriate FormGroup object with all nested groups and controls that are needed.
The addControlGroups method we just covered returns this proper FormGroup object needed to the buildForm method, which is then passed into the splitGroups method.
To assist with rendering our controls and groups separately, we use this method to differentiate groups from controls. This is entirely optional and if you wish to show all of your controls and groups in the order they exist within your javascript object, just omit this method call. In most cases though, you will want to show your groupings separated rather than jumbled with other nested structures so for the sake of this example, we will use it.
Line 109–120: if the type of control is a form group, it will have a property called controls which contains the groups controls. If so, add its value to the groups object. Otherwise, add its value to the controls object.
Line 117–120 After we have the two structures needed, convert them into two separate form groups that are grouped into one parent form group. Finally, this is our rootFormGroup that is needed to render our form in the UI.
Lastly our submissions method gathers the values of our form controls and groups and emits them using whichever method was provided.
Step 2: Build out the template for our form layout
The form template is able to be very simple sense our core logic is pushed off to the component. Lets look at the end result and step through it.
Once rootFormGroup is established, we render a basic form providing it our rootFormGroup as the formGroup input and submitForm method to be called when the forms submitted.
If you remember when we had split out controls between groups and controls in our splitGroups method, this is where it comes into play. We provide that separate controls and groups structures into two separate instances of our control-listing component. Both will be referred to as parentFormGroup.
Step 3: Build the control listing component
The last piece needed is a more reactive UI piece capable of rendering the complex structures provided, according to the data types provided.
Generate the control-listing component
Here is what our component will look like and there is not nearly as much that we will have to walk through as our form-layout component handled the heavier conversion logic.
Here is the breakdown line by line
parentFormGroup as stated is the parent form that is passed in. We will be creating sub layered forms within our parent form and to tie them all together into one form entry, we must reference our parent form group.
nested: this is an extra property Im using in this example to make known that this group needs to have the option of toggling open and closed. This comes in handy with large nested groups that together make more alot of page scrolling.
showNested: used in conjunction with nested, will tell the form group rather it should be shown or hidden.
Step 4: Build the control listing template
This piece will handle traversing through each control in our provided parentFormGroup. Based on the control type, it will display the appropriate form input. The tricky part is for further nested groups, the nested group is recursively passed back into another instance of this component. This component recursion is necessary in order to handle an infinite amount of nested objects in our provided javascript structure.
This can be straining to look over so I will break it down line by line
As mentioned earlier, we create a new sub form imbedded in our parent form for each nested for group we have. Providing each sub form with the parentFormGroup insures that they all act as one form when submitted rather than separate submissions.
If this form control provided to the component is a group then nested will be true and a toggle icon of your choice will be used to show/hide the embedded group. This will never be true for the first control passed from the root control but the nested groups that are recursively passed back into will contain both nested when it is needed and parentFormGroup to define what the groups name will be formatted as.
You don’t have to understand this piece fully to implement this dynamic form but to make changes, I would try to make sense of this as its the key logic gate in determining how a control is rendered.
Line 11: the if conditional assures that this control or group of controls is only shown if is not a nested control (not a group) or is nested (is a group) and has toggled the showNested to true.
Line 12-13: loop through the controls provided from the parent group, checking they have a controls property. As discussed earlier, form groups have a controls property on them containing all nested controls. If it has a controls property, thus signifying a control group, recursively use another control-listing to display our groups and any further nested control groups. This time we are passing in the nested input as true, signifying that it’s a group.
If the control does not have a controls property, we assume it is not a group and instead present our #showValue template
The show value template is simply used to display the appropriate field based on the control type. Here I am using the angular material design templating library but you are free to use what you wish. The primary rule that needs followed is that the form control key is provided for the formControlName when defining your input. This will tie its value back into your form control group and properly display changes made.
The toggle and dropdown will be treated slightly different for whatever templating you’r using but generally works the same.
Step 6: Use the dynamic form in your app
Finally you can use the form in your application. The assumption in this example is that an api call has been made response gathered into a variable called mlbTeamApiResposne that can now be provided for the formMappings input.
You will now see a rough rendering of your forms ordered by controls first, followed by your grouped controls. I’ll leave the styling up to your discretion.