What is angularjs directives? How to create a custom form input and custom validations

At a high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS’sHTML compiler ($compile) to attach a specified behavior to that DOM element (e.g. via event listeners), or even to transform the DOM element and its children.

Angular comes with a set of these directives built-in, like ngBind, ngModel, and ngClass. Much like you create controllers and services, you can create your own directives for Angular to use. When Angular bootstraps your application, the HTML compiler traverses the DOM matching directives against the DOM elements.

How to create a custom form input and custom validations with a Directive.

Some times ago someone asked me to implement a date selector with 3 select inputs instead of a date input.

I thought about multiple problems with that implementation:

  • No form input validation with ngMessages
  • No 28/30/31 days a month check out of the box
  • No leap year check out of the box

Directive to the rescue


At the end of this post we should have:

  • A single directive to handle 3 select inputs (month/day/year)
  • A valid javascript date format as input and output linked to a ng-model
  • A ‘required’ error handling with 3 sub-errors (year required, month required, day required) to interact with ngMessages

I’ll assume that you know how to install and use ngMessages.

main.html

<multi-select-date name="birthdate" ng-model="user.birthdate" year-order="desc" start-year="1980" end-year="2000" required></multi-select-date>

as you can see the “multi-select-date” has multiple attributes:

  • name, so you can intercept it from ngMessages
  • ng-model, to bind data to a model in your controller
  • year-order, asc = 1980 -> 2015, desc = 2015 -> 1980, default is desc
  • start-year & end-year, let you specify the min and max year, by default end-year = the actual year and start-year = the actual year minus 100 years

multi-select-date.html

This is the directive template, 3 select inputs that populate through ng-options, and an ”orderBy” filter on the year’s select to handle the “year-order” using the reverse option of the filter.

<div class="form-inline">
  <select ng-model="date.month" ng-options="month for month in selects.months()" class="form-control">
    <option value="" disabled>--</option>
  </select>

  <select ng-model="date.day" ng-options="day for day in selects.days()" class="form-control">
    <option value="" disabled>--</option>
  </select>

  <select ng-model="date.year" ng-options="year for year in selects.years() | orderBy: year:yearOrder" class="form-control">
    <option value="" disabled>----</option>
  </select>
</div>

Note that the sources of the ng-options are not arrays but functions that will generate dynamically:

  • The right years number based on “start-year” and “end-year”
  • 12 months with a leading zero
  • And the right count of days based on the year and month previously selected (or 31 days if nothing selected yet)

multiSelectDate.directive.js

Now let see the actual directive.

  • restrict: ‘E’ to tell angular that we want a element.
  • require: ‘?ngModel’ to tell angular that we require ng-model, it will be available through the ngModel’s link’s function parameter.
  • templateUrl to target our directive template.
  • The link function where the magic happens

As you can see the directive has 4 major parts:

  • getting the ng-model content and create our local scope with a string equivalent of year/month/day (in case that you specified a date in your controller for example).
// GET FROM NG MODEL AND PUT IT IN LOCAL SCOPE
ngModel.$render = function () {
  scope.date = {
    day: $filter('date')(ngModel.$viewValue, 'dd'),
    month: $filter('date')(ngModel.$viewValue, 'MM'),
    year: $filter('date')(ngModel.$viewValue, 'yyyy')
  };
};
  • getting the directive options or defined default options if no attributes have been set in your tag.
// ATTRIBUTES (with default values if not set)
scope.yearOrder = (attrs.yearOrder && attrs.yearOrder === 'asc') ? false : true; // year order: 'asc' or 'desc', default: desc
var endYear = attrs.endYear || new Date().getFullYear(); // default: this year
var startYear = attrs.startYear || startYear - 100; // default: this year - 100
  • generating years, month and days list.
// INIT YEARS, MONTHS AND DAYS NUMBER
scope.selects = {

  days: function(){

    // Get number of days based on month + year 
    // (January = 31, February = 28, April = 30, February 2000 = 29) or 31 if no month selected yet
    var nbDays = new Date(scope.date.year, scope.date.month, 0).getDate() || 31;

    var daysList = [];
    for( var i = 1; i <= nbDays ; i++){
      var iS = i.toString();
      daysList.push( (iS.length < 2) ? '0' + iS : iS ); // Adds a leading 0 if single digit
    }
    return daysList;
  },
  months: function(){
    var monthList = [];
    for( var i = 1; i <= 12 ; i++){
      var iS = i.toString();
      monthList.push( (iS.length < 2) ? '0' + iS : iS ); // Adds a leading 0 if single digit
    }
    return monthList;
  },
  years: function(){
    var yearsList = [];
    for( var i = endYear; i >= startYear ; i--){
      yearsList.push( i.toString() );
    }
    return yearsList;
  }
};
  • watching “scope.date” model changes and setting validation rules to interact with the main form, and update the ng-model if everything is allright.
// WATCH FOR scope.date CHANGES
scope.$watch('date', function(date) {

  // IF REQUIRED
  if(attrs.required){

    // VALIDATION RULES
    var yearIsValid = !!date.year && parseInt(date.year) <= endYear && parseInt(date.year) >= startYear;
    var monthIsValid = !!date.month;
    var dayIsValid = !!date.day;

    console.log(yearIsValid, monthIsValid, dayIsValid);

    // SET INPUT VALIDITY
    ngModel.$setValidity('required', yearIsValid || monthIsValid || dayIsValid ? true : false );
    ngModel.$setValidity('yearRequired', yearIsValid ? true : false);
    ngModel.$setValidity('monthRequired', monthIsValid ? true : false);
    ngModel.$setValidity('dayRequired', dayIsValid ? true : false);

    // UPDATE NG MODEL
    if(yearIsValid && monthIsValid && dayIsValid){
      ngModel.$setViewValue( new Date(date.year, date.month - 1, date.day) );
    }
  }

  // IF NOT REQUIRED (still need the 3 values filled to update the model)
  else if(date.year && date.month && date.day){
    ngModel.$setViewValue( new Date(date.year, date.month - 1, date.day) );
  }

}, true);

 

Trackback URL: http://www.getcreativeweb.com/what-is-angularjs-directives-how-to-create-a-custom-form-input-and-custom-validations/trackback/

Leave a comment:

Your email address will not be published. Required fields are marked *