Dynamically add and remove form input fields with Ionic

February 6, 2018, 9:00 am Categories:

Categories

Forms are rarely fun to work with - particularly when there's all manner of data capture and parsing requirements that your application might need to fulfill.

Thankfully this burden is lightened considerably with Ionic's range of UI components and Angular's FormBuilder API, both of which allow for the rapid development of forms for Ionic web/mobile applications.

Over the course of the following tutorial I'm going to guide you through using these tools to create a form that allows input fields to be dynamically added/removed and validated along with all entered data successfully captured upon form submission.

What we'll be developing

Our Ionic application will consist of a single page with a form that allows a user to enter a technology framework name (I.e. Ionic, React, Angular etc) and then add additional fields for their associated technologies (I.e. TypeScript, HTML5, Apache Cordova etc).

On initial launch the application will appear as follows:

Ionic application displaying a form with 2 input fields

Once data has been entered into these fields the user can, if they wish to, add further Technology fields as demonstrated below:

Ionic application with new input fields added to the displayed form

As you can see each input field now also displays an option allowing the user to remove that generated field (and any data that it might contain) should they change their mind.

When the form is submitted the data is simply logged to the browser console as a JavaScript object like so:

Screen capture displaying submitted form data as a JavaScript object in the web browser console

You can, of course, handle the submitted data how you want!

Getting started

Assuming you have the latest version of Ionic installed open your system CLI software, navigate to where your digital projects are located and create the following ionic project named ionic-input-generata using a blank project template structure:

ionic start ionic-input-generata blank

We won't be installing any Ionic Native/Apache Cordova plugins or third-party libraries so, once the project has been generated, we can proceed straight to the component logic and templating.

Coding the form management logic

Within the HomePage component class we're going to make use of the following Angular modules to handle all form management tasks from input field generation and validation to tracking the removal of dynamically generated form input fields:

  • FormBuilder - Helps to structure and coordinate FormGroup, FormArray and FormControl instances used within a template form
  • FormGroup - Tracks the value and state of a group of FormControl instances - aggregating all values into an object
  • FormArray - Tracks the value and state of FormControl instances within a form - aggregating all values into an array (useful for when we don't know how many FormControls we might need)
  • Validators - Provides a set of functions for validating FormControls

We begin by importing these modules into the component:

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { FormBuilder, FormArray, FormGroup, Validators } from '@angular/forms';

This is followed, within the HomePage class, by defining a publicly available FormGroup property which will act as a bridge between the component class and its template allowing communication between the two to take place:

/**
 * @name form
 * @type {FormGroup}
 * @public
 * @description     Defines a FormGroup object for managing the template form 
 */
public form 	: FormGroup;

Within the class constructor we then use the FormBuilder API to programmatically create an object which defines the validation rules for the template form (for the purposes of this tutorial these validation rules are VERY basic and simply require that NO empty form fields be submitted - you can modify these rules as you see fit).

You'll notice within this FormGroup object that we define an array for the technologies array which calls a method named initTechnologyFields():

constructor(public navCtrl 		: NavController, 
            public navParams 	: NavParams,
            private _FB          : FormBuilder) 
{ 

   // Define the FormGroup object for the form
   // (with sub-FormGroup objects for handling 
   // the dynamically generated form input fields)
   this.form = this._FB.group({
      name       	  : ['', Validators.required],
      technologies     : this._FB.array([
         this.initTechnologyFields()
      ])
   });
}

This initTechnologyFields()method returns its own FormGroup object with validation rules for the specified field name:

/**
 * Generates a FormGroup object with input field validation rules for 
 * the technologies form object
 *
 * @public
 * @method initTechnologyFields
 * @return {FormGroup}
 */
initTechnologyFields() : FormGroup
{
   return this._FB.group({
      name : ['', Validators.required]
   });
}

This FormGroup is returned within its own method as we want to provide the ability for the user to dynamically add new technology fields to the template.

How this works is relatively simple.

Every time a new input field is added to the form a new FormGroup object, responsible for managing only that generated field, is created. This allows for dynamically generated fields to have their own default FormGroup management automatically initialised and made available simply through calling this method.

By adopting this approach we offload all the form management logic to Angular which handles all of that in the background for us.

No complicated DOM scripting or recursive loops to manage/track input field generation - we simply leave Angular to manage our form state instead courtesy of the FormGroup and FormArray modules (we'll see how these plug into the component template's form a little later on).

With the FormGroup logic in place we next move onto defining the logic for generating new input fields for the form with the addNewInputField() method:

/**
 * Programmatically generates a new technology input field
 *
 * @public
 * @method addNewInputField
 * @return {none}
 */
addNewInputField() : void
{
   const control = <FormArray>this.form.controls.technologies;
   control.push(this.initTechnologyFields());
}

This simply retrieves the form's existing technologies fields as a FormArray object.

A new input field is then generated by executing the initTechnologyFields() method which pushes the value into the FormArray object and, on the form itself, a new input field will appear as a result.

Removing generated input fields is handled with the removeInputField() method:

/**
 * Programmatically removes a recently generated technology input field
 *
 * @public
 * @method removeInputField
 * @param i    {number}      The position of the object in the array that needs to removed
 * @return {none}
 */
removeInputField(i : number) : void
{
   const control = <FormArray>this.form.controls.technologies;
   control.removeAt(i);
}

Here we simply call the removeAt method of the FormArray object to remove the value and tracking of the selected input field from the FormArray array (as well as from the template's form).

Finally we call the manage() method which simply logs the submitted form data to the browser console:

/**
 * Receive the submitted form data
 *
 * @public
 * @method manage
 * @param val    {object}      The posted form data
 * @return {none}
 */
manage(val : any) : void
{
   console.dir(val);
}

We could, of course, parse the submitted form data and do with that as we wish (I.e. post to a remote script) but for the purposes of this tutorial logging to the console is sufficient :)

The HomePage component - ionic-input-generata/src/pages/home/home.ts - should appear in full like the following:

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { FormBuilder, FormArray, FormGroup, Validators } from '@angular/forms';


@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {


   
   /**
    * @name form
    * @type {FormGroup}
    * @public
    * @description     Defines a FormGroup object for managing the template form 
    */
   public form 			: FormGroup;



   constructor(public navCtrl 		: NavController, 
               public navParams 	: NavParams,
               private _FB          : FormBuilder) 
   { 

      // Define the FormGroup object for the form
      // (with sub-FormGroup objects for handling 
      // the dynamically generated form input fields)
      this.form = this._FB.group({
         name       	  : ['', Validators.required],
         technologies     : this._FB.array([
            this.initTechnologyFields()
         ])
      });
   }



   /**
    * Generates a FormGroup object with input field validation rules for 
    * the technologies form object
    *
    * @public
    * @method initTechnologyFields
    * @return {FormGroup}
    */
   initTechnologyFields() : FormGroup
   {
      return this._FB.group({
         name 		: ['', Validators.required]
      });
   }



   /**
    * Programmatically generates a new technology input field
    *
    * @public
    * @method addNewInputField
    * @return {none}
    */
   addNewInputField() : void
   {
      const control = <FormArray>this.form.controls.technologies;
      control.push(this.initTechnologyFields());
   }



   /**
    * Programmatically removes a recently generated technology input field
    *
    * @public
    * @method removeInputField
    * @param i    {number}      The position of the object in the array that needs to removed
    * @return {none}
    */
   removeInputField(i : number) : void
   {
      const control = <FormArray>this.form.controls.technologies;
      control.removeAt(i);
   }



   /**
    * Receive the submitted form data
    *
    * @public
    * @method manage
    * @param val    {object}      The posted form data
    * @return {none}
    */
   manage(val : any) : void
   {
      console.dir(val);
   }



}

With the form management logic in place it's time to turn our attention to the component template and see how the markup is structured to allow the HTML form to be managed by and communicate with the component class.

The component template

<ion-header>
  <ion-navbar>
    <ion-title>Manage</ion-title>
  </ion-navbar>
</ion-header>


<ion-content padding>


   <!-- Assign the FormGroup of form to the HTML form 
        via a property binding (allowing the component 
        class to communicate/interact with the template -->
   <form 
      [formGroup]="form" 
      (ngSubmit)="manage(form.value)">
   	  <ion-list 
   	     margin-bottom>
         <ion-item 
            margin-bottom 
            no-lines>
            <ion-label floating>Framework name:</ion-label>

            <!-- Assign our first FormControl of name to the input field -->
            <ion-input 
               type="text"
               maxlength="40"
               formControlName="name"></ion-input>
            <span>Please enter a framework name of no more than 40 characters</span>
         </ion-item>


         <!-- Assign the technologies FormArray to the form
              where we want to track/generate new input track fields -->
	     <div 
	        formArrayName="technologies" 
	        margin-bottom>

            
            <!-- Assign a FormGroupName property binding to track
                 and manage each separate generated input field.
                 Also iterate through the technologies FormArray to 
                 correctly track/render new input fields against 
                 existing fields -->
            <section 
               [formGroupName]="i" 
               *ngFor="let tech of form.controls.technologies.controls; let i = index">
               <ion-item-group>
               	  <ion-item-divider color="light">Name #{{ i + 1 }}</ion-item-divider>
                  <ion-item>
                     <ion-label floating>Technology name:</ion-label>   
                     <ion-input 
                        type="text"
                        maxlength="50"
                        formControlName="name"></ion-input>
                  </ion-item>  
                  
   
                  <!-- Allow generated input field to be removed -->
                  <span 
                     float-right 
                     ion-button 
                     icon-left 
                     clear  
                     *ngIf="form.controls.technologies.length > 1" 
                     (click)="removeInputField(i)">
                     <ion-icon name="close"></ion-icon>
                     Remove
                  </span>  
               </ion-item-group>    
            </section>
         </div>


         <!-- Allow new input field to be generated/added -->
         <span 
            ion-button 
            float-left 
            icon-left 
            clear 
            (click)="addNewInputField()">
               <ion-icon name="add"></ion-icon>
               Add a new technology
         </span>


   	 </ion-list>


   	 <!-- Only allow form to be submitted IF validation criteria for
              input fields has been successfully passed -->
         <button 
   	     ion-button 
   	     block  
   	     margin-top
   	     color="primary" 
   	     text-center 
   	     [disabled]="!form.valid">Submit</button>
   </form>

</ion-content>

The HTML markup should be self-explanatory (with the addition of comments to explain how property bindings are used to connect Angular's FormGroup and FormArray objects - that have been set up in the component class - with the template's form) so now let's move onto testing our application.

The proof is in the pudding

All our coding efforts amount to naught without testing so let's run the ionic-input-generata application in our desktop browser using the following command issued within our system CLI software:

ionic serve

Coding errors aside you should experience the application being launched and running like so:

Ionic application displaying a form with 2 input fields

Now play around with adding and removing input fields and then submitting the completed form data!

In summary

Thanks to Angular's FormBuilder, FormGroup and FormArray classes generating and removing input fields is vastly simplified as the heavy lifting is managed internally by the framework. This frees us to concentrate on other application development tasks without worrying about performance, optimisation or other headaches/challenges that might befall us were we to try accomplishing this without a framework.

If you enjoyed this tutorial then feel free to share your thoughts, reactions and suggestions in the comments section below.

If you liked what was written here then please consider signing up to my FREE mailing list to stay updated on further articles and e-books that I have in the pipeline.

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top