Dynamic Angular Forms

Published on August 15, 2019

angular.js

Multi-color fish swimming in blue water

I’ve been spending a lot of time on StackOverflow recently, answering Angular.js questions and earning some reputation and badges. I’ve been working through a problem with a developer that wants to create a dynamic form based on a service response containing the specifications for each input (original question can be found here).

Wait, what?

A dynamic form, in this case, means that your application is provided with a configuration of inputs, and the HTML for the form is generated based on that configuration. This allows your code to be reusable in two ways: in the creation of each input in a form, and between the forms of multiple clients or users.

Fish and Birds

Let’s create a scenario where you’re building an application to collect data about animals. The first scientist is in the Amazon studying birds. A second scientist is at the Great Barrier Reef studying fish. You want to be able to use the same application to collect data, but the questions each scientist might use will probably be completely different.

If we want to make this form using static HTML, you’d need to make two — one tailored to birds, and one for fish. In the example below, you see the redundancy:

<form action="doSomething">
  
  <label for="birdColor">Bird Color</label>
  <input type="text" id="birdColor" name="birdColor">
 
  <label for="birdSize">Bird Size</label>
  <input type="text" id="birdSize" name="birdSize">
  
  <label for="eggColor">Egg Color</label>
  <input type="text" id="eggColor" name="eggColor">

  <button type="submit">Submit</button>

</form>

Each input has a corresponding label, input type, name, and ID. For each input, you need to retype the HTML for each input in this form, and then make a whole other form to handle the fish questions. If you want to make a change to an input, you’ll need to make it everywhere this form is used. This is where dynamic form creation is awesome.

In the case of the StackOverflow question, the form was generated based on a button click, that in turn called a service with a param that said which configuration should be returned, and then responded with the appropriate JSON. The key here is that the configuration DEFINED the structure of each input, and then the HTML input templates were created based on that.

An example of a bird form configuration as JSON could be:

[{
  "id": 1,
  "type": "checkbox",
  "required": true,
  "label": "Bird Color"
}, {
  "id": 2,
  "type": "textarea",
  "required": true,
  "label": "Description"
}, {
  "id": 3,
  "type": "text",
  "required": true,
  "label": "Egg Color"
}]

To make the form dynamic, we’re going to loop through each of these inputs provided and build the template.

The Loop

Angular.js comes with a handy built-in directive called ngRepeat. It instantiates a template once per item in a collection. Our goal is to create the generic input template for each bird question.

The first step is to make sure that the configuration data is accessible to the scope of the template. In this basic example, we’ll do this by assigning it to a $scope.questions property in our controller.

var app = angular.module('app', []);

app.controller('FormController', ['$scope', function ($scope) {
  
  $scope.questions = [];

  //Again, can be fired on a button click, on page load, etc.
  $scope.getQuestions = function (type) {
    
    var type = type ? type : 'default'; //some kind of validation can go here
    
    //make service call based on category and return questions
    $http({
      method: 'GET',
      url: '/forms',
      params: { name: type }
    }).then(function (res) {
      
      //Parse response to get just questions collection
      $scope.questions = res && res.data && res.data.questions;
      
    })['finally'](function (err) {
      //Error handling
    });

  };
  
  $scope.submit = function () {
    //submit form data
  };
  
}]);

Now the questions collection has been defined in scope, we can create a matching template using ng-repeat.

 <div data-ng-controller="FormController">
    
    <!-- click a button to see a form -->
    <button data-ng-click="getQuestions('fish')">New Fish Record</button>
    <button data-ng-click="getQuestions('bird')">New Bird Record</button>

    <form novalidate>
    
      <p data-ng-repeat="item in questions">
        <label for="">{{ item.label }}</label>
        <input type={{ item.type }} data-ng-model="formData[item.id]">
      </p>
      
      <button type="submit" data-ng-click="submit()">Submit</button>
      
    </form>
    
  </div>

It loops through each question, and dynamically updates each input with the value from the service response. It is generic enough that it can be used for both fish AND birds, simply by changing the configuration.

Here’s an example of a possible flow for the scientist looking at birds:

  1. See a cool bird you want to record info about.
  2. Go to the form page.
  3. Click the “New Bird Record” button. The form will populate with bird questions.
  4. Fill out the info, click submit.

This process is the same for the scientist looking at fish, except they’d click the “New Fish Record” button. It uses the same HTML, the same JavaScript, calling the same service — just with a different parameter.

You don’t need to populate that form type via a button. You could pull it from the URL as a route or state parameter. You could have it defined in the scientist profile or settings. You could select it from a drop-down. It all depends on your application.

Form Submission

Generating the form dynamically is all well and good, but how do you actually get the data from each input into a submit function? With a normal Angular form, we use ngModel with a unique model name, like <input type="text" data-ng-model="formData['birdName']">. We can still do this dynamically. I usually create a property in my controller called $scope.formData = {} to hold all of my form values. Then, when looping through the form, I add a property based on the ID (or something equally unique) of the question.

<p data-ng-repeat="item in questions">
  <label for="">{{ item.label }}</label>
  <input type={{ item.type }} data-ng-model="formData[item.id]">
</p>

The item.id is coming directly from the JSON response. If the ID for this particular input is “eggColor” and you’ve typed in “blue”, then the $scope.formData object now looks like this: { "eggColor": "blue" }. Now, when you submit the form, you can make a service call to handle your submission, and pass along your $scope.formData.

$scope.submit = function () {

   //Validation/parsing of input could go here
  
  //Make service call
  $http({
    method: 'POST',
    url: '/forms',
    data: $scope.formData
  }).then(function (res) {
    //success handler
  })['function'](function (err) {
    //error handler
  }) ;
  
}

In Conclusion

This is a VERY simple example that attempts to make sense of a more complicated concept. If all you were dealing with were checkboxes and text inputs, this would work perfectly. However, this doesn’t account for textareas, radio buttons, or select buttons. This is still very possible, but involves a little more work. I have done some of this work as part of my Acoma data collection app, so if you’re working on a form with many inputs, check that out! Feel free to fork it, add an issue, or contact me with questions.