Skip to content

Dynamic form fields in Angular

A client of mine had a need for being able to collect dynamic data from within their forms. This was a later addition to an existing form that already used forms validation within angular.  The requirements were for 3 types of fields that could be added dynamically.

  • String
  • Numeric
  • Date

We were already using jquery’s datepicker for date inputs so I needed to integrate it in with the date type. With the numeric type, I had to add in custom validation so that they could input any numeric value and I could make it work with validation. With the string type I could use the native text input field. Another part of the requirements was the validation:

  • String  – Min length, Max length
  • Numeric – Min value, Max value
  • Date – Min date, Max date

To get started, I made the models to back my dynamic fields.


public class DynamicDataField
{
    [Key]
    public int Id { get; set; }

    [Required]
    [StringLength(64)]
    [Display(Name = "Display Name")]
    public string DisplayName { get; set; }

    [Required]
    [StringLength(64)]
    [Display(Name = "Reported Name")]
    public string ReportedName { get; set; }

    [Required]
    [StringLength(10)]
    public string Type { get; set; }

    [StringLength(64)]
    public string Tooltip { get; set; }

    [Required]
    public bool Required { get; set; }

    public decimal? MinNumericValue { get; set; }

    public decimal? MaxNumericValue { get; set; }

    public DateTime? MinimumDate { get; set; }

    public DateTime? MaximumDate { get; set; }

    public byte? MaxStringLength { get; set; }

    public byte? MinStringLength { get; set; }
}

Then I exposed the models through a Web API endpoint. To call the service I utilized the angular http service. In the controller for my form I use the http service to get the dynamic fields. Those dynamic fields populate a collection on the scope; then that collection is used in a repeater that looks like this:


<div ng-repeat="dataField in dynamicData" class="col-md-6 col-lg-3 form-row">
    <div ng-switch="dataField.Type">
        <div ng-switch-when="String">

            <label class="control-label">{{dataField.DisplayName}}</label>
            <i class="fa fa-question-circle clickable-tooltip" title="{{dataField.Tooltip}}"></i>
            <span ng-show="dataField.Required">(<span style="color:red;">required</span>)</span>
            <input type="text" class="form-control" ng-required="dataField.Required" ng-maxlength="dataField.MaxStringLength" ng-minlength="dataField.MinStringLength" dynamic-name="dataField.ReportedName" ng-model="appointmentEntity[dataField.ReportedName]" />
            <span class="field-validation-error" ng-show="memberDetailsForm[dataField.ReportedName].$error.minlength">The {{dataField.DisplayName}} field is too short.</span>
            <span class="field-validation-error" ng-show="memberDetailsForm[dataField.ReportedName].$error.maxlength">The {{dataField.DisplayName}} field exceeded the maximum field length limit of {{dataField.MaxStringLength}}.</span>

         </div>
         <div ng-switch-when="Numeric">

             <label class="control-label">{{dataField.DisplayName}}</label>
             <i class="fa fa-question-circle clickable-tooltip" title="{{dataField.Tooltip}}"></i>
             <span ng-show="dataField.Required">(<span style="color:red;">required</span>)</span>
             <input type="text" numeric-input ng-required="dataField.Required" class="form-control" min="{{dataField.MinNumericValue}}" max="{{dataField.MaxNumericValue}}" dynamic-name="dataField.ReportedName" ng-model="appointmentEntity[dataField.ReportedName]" />
             <span class="field-validation-error" ng-show="memberDetailsForm[dataField.ReportedName].$error.min">The {{dataField.DisplayName}} field should be greater than {{dataField.MinNumericValue}}.</span>
             <span class="field-validation-error" ng-show="memberDetailsForm[dataField.ReportedName].$error.max">The {{dataField.DisplayName}} field exceeded the maximum field size limit of {{dataField.MaxNumericValue}}.</span>

         </div>
         <div ng-switch-when="Date">

             <label class="control-label">{{dataField.DisplayName}}</label>
             <i class="fa fa-question-circle clickable-tooltip" title="{{dataField.Tooltip}}"></i>
             <span ng-show="dataField.Required">(<span style="color:red;">required</span>)</span>
             <input type="text" class="form-control" datepicker ng-required="dataField.Required" mindate="{{dataField.MinimumDate}}" maxdate="{{dataField.MaximumDate}}" dynamic-name="dataField.ReportedName" ng-model="appointmentEntity[dataField.ReportedName]" />

        </div>
    </div>
</div>

Note: The reason I am using bracket notation for the model is so that the model’s properties will map to the server properly. There is a unique constraint in the DB so that there will not be a duplicate property name.

Directives:

DatePicker


angular.module('app').directive('datepicker', function () {
    return {
       require: 'ngModel',
       link: function (scope, el, attr, ngModel) {
           $(el).datepicker({
               onSelect: function (dateText) {
                   scope.$apply(function () {
                       ngModel.$setViewValue(dateText);
                   });
               },
               dateFormat: "DD, MM d, yy",
               minDate: $.datepicker.parseDate('yy-mm-dd', attr.mindate.substring(0, 10)),
               maxDate: $.datepicker.parseDate('yy-mm-dd', attr.maxdate.substring(0, 10))
           });
       }
    };
});

Numeric Input


angular.module('app').directive('numericInput', function () {
    return {
        require: 'ngModel',
        link: function (scope, elem, attrs, ctrl) {
            var minValue = Number(attrs.min);
            var maxValue = Number(attrs.max);

            ctrl.$parsers.push(function (value) {
                var value = parseFloat(value || '');
                ctrl.$setValidity('max', value <= maxValue);
                ctrl.$setValidity('min', value >= minValue);
                return value;
            });
        }
    };
});

Dynamic Form Name

Angular will not pick up on the dynamic form fields without explicitly compiling the element with the current scope. That way a watch on the form will pick up the new fields and validate them along with the existing fields.


angular.module('app').directive('dynamicName',['$compile', function ($compile) {
    return {
        restrict: 'A',
        terminal: true,
        priority: 1000,
        link: function (scope, element, attrs) {
            element.attr('name', scope.$eval(attrs.dynamicName));
            element.removeAttr('dynamic-name');
            $compile(element)(scope);
        }
    };
}]);

In the controller, the watch is setup and custom validation is run for field changes.


$scope.$watch('myForm.$valid', function (isValid) {
//custom validation logic
});

Server side I used a dynamic type in my Web API method to bind a type with unknown properties. Then I used the dynamic field names and extract the fields from the dynamic object.


public async Task<IHttpActionResult> Post(dynamic data)
{
    //omitted

    var dynamicFields = await this._context.DynamicDataFields.ToListAsync();
    foreach(var dynamicField in dynamicFields)
    {
        if (data[dynamicField.ReportedName] != null && data[dynamicField.ReportedName].Value.ToString() != String.Empty)
        {
            appointmentAdd.DynamicData.Add(new DynamicData
            {
                Name = dynamicField.ReportedName,
                Type = dynamicField.Type,
                Value = data[dynamicField.ReportedName].Value.ToString()
            });
        }
    }

    //omitted
}

Getting back to blogging

Last year I started a blog but ended up losing my content. Since I’m still speaking regularly I decided to go ahead and restart it. This is my first entry since coming back and I hope to provide good content moving forward. My primary focus is to work on embedded devices with Microsoft technologies to give a better view of the Internet of Things or what I like to call Embedded Cloud. 

I have taken an interest in Atmel embedded devices and how those can be used with an Internet of Things approach. In August I am presenting at the Microsoft .NET Users group in Atlanta about how to use SignalR with embedded devices.

Twitter Auto Publish Powered By : XYZScripts.com