Saturday, August 2, 2014

The flexible form builder [Part 1 viewer]

Any html form is a recursive structure of fields. A field type can be one of many primitive types like text, textarea, select, radio etc. or composite type that combines primitive types or other composite types or array of primitive or composite types. Form values are saved as a single row which is a stored composite object like SQL table row or a JSON object. Field values can be a reference to another stored object.

We want a form designer that has full support for all types of fields. So, we need support for primitive, composite and array types and any combination of those types.

As first step I have designed a form viewer that can render the form from a form definition provided as JSON object. To keep the blog post simple I use one primitive type named text, composite type, array type and to define a form a separate form type. Form , composite and array types has child fields array named fields.

Here is an example of possible form definition:

form =
    {'name':'form', 'type':'composite', 
        'fields': [    
            {'name': 'name','type': 'text'},
            {'name':'address', 'type':'composite',
               'fields':[
                   {'name': 'street','type': 'text'},
                   {'name': 'city','type': 'text'},
                   {'name': 'zip','type': 'text'},
                   {'name': 'state','type': 'text'},
                   {'name':'members', 'type':'array',
                       'fields':[
                            {'name': 'name','type': 'text'}
                        ]
                   }
               ]          
            },
            {'name':'assets', 'type':'array',
                'fields':[
                    {'name': 'type','type': 'text'},
                    {'name': 'price','type': 'text'},
                    {'name': 'brand','type': 'composite', 
                        'fields':[
                            {'name': 'name','type': 'text'},
                            {'name':'locations', 'type':'array',
                                'fields':[
                                    {'name': 'city','type': 'text'}
                                 ]
                            }
                        ]
                    }       
                ]          
            }       
        ]
    };  

This is a form that has primitive types, composite type, array of primitive type and array of composite type. Other than reference type which is just a primitive type with loaded data this covers what we need to create almost any type of form.

Here is the form viewer preview:
See the Pen atCcI by Maruf Maniruzzaman (@kuasha) on CodePen.
I have created an angular directive named field.
cosmosApp.directive('field', function($compile) {
    return {
      restrict: 'E',
      scope: {
        item:'='        
      },
      
      link: function(scope, element, attributes) {
        scope.item_data = [];
        scope.add_item = function(position){
          var newItem = {};
          angular.forEach(scope.item.fields, function(value, index){
            newItem[value.name] = "test";
          });
          scope.item_data.splice(position+1, 0, newItem);
                          
        };
        scope.removeItem = function(index){
          scope.item_data.splice(index, 1);
        };
        var template = '<span>{{item.name}}<input type="text" /></span> &nbsp;'; 
        
        if(scope.item.type === "composite"){
          template = '<div>{{item.name}}</div><ul><li ng-repeat="ph in item.fields">
                            <field item="ph"></field></li></ul>';
        }
        else if(scope.item.type === "array"){
          if(scope.item_data.length<1){
            scope.item_data = [];
            scope.add_item();
          }
          template = '<div>{{item.name}}<button ng-click="add_item(-1)">+</button></div>
                         <ul><li ng-repeat="d in item_data">
                           <field item="ph" ng-repeat="ph in item.fields"></field>
                           <button ng-click="removeItem($index)">-</button>
                           <button ng-click="add_item($index)">+</button></li>
                         </ul>';
        }
        
        var newElement = angular.element(template);
        $compile(newElement)(scope);
        element.replaceWith(newElement);        
      }
    };
  });
To use this directive we create a form in our controller and create a dom node like this: