Angular Data-driven Form elements

Home / Angular Data-driven Form elements

In many of my latest applications, it’s necessary to have data drive what a form-element actually lets the user enter. For example, I may have a drop-down to select a property, and then, based on that property, a common text-box is used for its entry.

Sometimes its nice to have the data-type of the property drive a user-friendly experience.


One good example is simply toggling between free-form text entry and date entry. In terms of user-friendliness, it would be nice to have a date picker for the property that is of type date. To facilitate this, I wanted a reusable component and created an Angular directive to handle it.

With my use case in mind, what I want, essentially is a drop-down where a user can select a property and then an input text box. The text box would react to the user’s selection. The user’s selection and subsequent entry is going be used as part of a search filter mechanism. Since I’m writing a directive, I want my page’s HTML to be simple and clean.

<div ng-controller="myCtrl as vm">
    <form>
        <search-term config="vm.searchParamConfig" ng-model="vm.searchParam"></search-term>
        <pre ng-bind="vm.searchParam | json" />
    </form>
</div>

The directive itself should take a list of options (with data types on each option) to display a drop down and handle events when the selection is changed. To simplify things, I use a relatively simple HTML template for the directive that uses an ‘ng-if’ to toggle which input element is displayed. In my mind, this was the simplest and most straight-forward way to handle this since I only have 3-4 data types (numeric, text, date). Each of the separate elements can handle their own validation, and invoke our directives, like date pickers, as needed.

Here’s the basic source code for the directive and its associated controller:

var searchTermCtrl = function ($scope, $element, $filter, $timeout) {
    var
        param,
        init = function () {
            $scope.param = $scope.ngModel;
            $scope.dateFormat = 'shortDate';
            $scope.dateOptions = {
                'year-format': "'yyyy'",
                'starting-day': 1
            };

            $scope.dateopen = { isopen: false };
            $scope.minDate = $filter('date')($scope.config.minDate, "shortDate");
            getOperators();
        },
        getOperators = function () {
            var optionsFilter = function (f) {
                return f.key === $scope.param.key;
            };
            var result = $scope.config.options.filter(optionsFilter);
            if (result.length !== 0) {
                param = result[0];

                $scope.param.key = param.key;
                $scope.param.type = param.type;
                $scope.param.value = param.value;

                if (param.type === 'numeric') {
                    $scope.param.filter = parseFloat($scope.param.filter);
                } else if (param.type === 'date') {
                    if ($scope.param.filter) {
                        $scope.param.filter = $filter('date')($scope.param.filter, "shortDate");
                    }
                };
            };
        };

    $scope.openDate = function ($event) {
        $event.preventDefault();
        $event.stopPropagation();
        $scope.dateopen.isopen = true;
    };

    $scope.criteriaChange = function (criteria) {
        getOperators();
        if ($scope.param.type === 'date') {
            $scope.param.filter = null;
        } else {
            $scope.param.filter = '';
        }
        $scope.ngModel = $scope.param;
    };

    $scope.searchTypeClicked = function (value) {
        if ($scope.param.searchType == value) {
            $scope.param.searchType = undefined;
        } else {
            $scope.param.searchType = value;
        }
    };

    $scope.filterChange = function (param) {
        $scope.ngChange($scope.param);
    };

    init();
};

var searchTerm = function () {
    var directive = {
        replace: true,
        restrict: 'E',
        require: '^ngModel',
        scope: {
            config: '=',
            ngModel: '=',
            ngChange: '&'
        },
        templateUrl: 'searchTerm.html',
        controller: 'searchTermCtrl',
        link: function (scope, element, attrs, ctrls) {
        }
    };

    return directive;
};

Here’s the select portion of the HTML template for which we listen to changes:

        <div class="form-group">
            <select name='criteria' ng-model='param.key' ng-change='criteriaChange(param)' ng-options='f.key as f.value for f in config.options'
                    class='form-control form-control-inline'>
                <option value="">Search By</option>
            </select>
        </div>

And, here’s our “ng-if” grouping to display different form elements based on our selected parameter’s data type

        <div class="form-group" ng-class="{ 'has-error': paramForm.paramText.$invalid || paramForm.paramNum.$invalid || paramForm.paramDate.$invalid }">
            <div ng-if='param.type == "text" || !param.type' class="form-group">
                <input name="paramText" type='text' placeholder="Search by Text" ng-model='param.filter' ng-change="filterChange(param)" class="form-control form-control-inline">
            </div>
            <div ng-if="param.type == 'numeric'" class="form-group">
                <input name="paramNum" type="number" placeholder="Search by Text" class="form-control form-control-inline" ng-model='param.filter' ng-change="filterChange(param)" ng-required="param.type == 'numeric'">
                <small id="paramNumReq" class="error" ng-show="paramForm.paramNum.$error.required">This is a required field.</small>
                <small id="paramNumInvalid" class="error" ng-show="paramForm.paramNum.$invalid && !paramForm.paramNum.$error.required">The entered value is invalid!</small>
            </div>
            <div ng-if="param.type == 'date'" class="input-group date">
                <input name="paramDate" type="text" placeholder="Search by Date" class="form-control" ng-model="param.filter" datepicker-popup="{{dateFormat}}"
                       datepicker-options="dateOptions" is-open="dateopen.isopen" ng-click="openDate($event)" min="minDate" datepicker-append-to-body="true">
                <small id="paramDateInvalid" class="error" ng-show="paramForm.paramDate.$invalid">This is an invalid date.</small>
            </div>
        </div>
    </div>
</script>

As you can see, the logic flow is pretty simple. When the select changes, we update our directive’s view model ($scope.param) and perform “ng-if” show and hiding on the “type” property. Despite being simple, it’s effective and the two-way binding allows our parent control to pick-up on the “key” and “filter’ properties and do whatever it is that it needs to do with them.

Finally, here’s a working fiddle that shows how it behaves:

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.