This article discusses implementation of server-side input controls validations in HTML page using AngularJS 1.5+ version. This article also provides full details for the Database and Server-side implementation.




Problem Definition

We needed to ensure that UPC and barcode are unique across multiple tables. As part of the requirements the user's input in HTML pages needed to be validated right away.

Research


The following blogs were pivotal in creating the final solution:

Working with Validators and Messages in AngularJS

How to use ngMessages in AngularJS

$asyncValidators, $touched and ngMessages: Form Validation In AngularJS 1.3

AngularJS 1.3 Taste: Async Validators

These blogs introduced the concept of AsyncValidators and ng-messages that were used for building Web portion of the uniqueness validation.

Implementation


1. SQL Server Database Code

1.a Database Constraints


As a first line of defense the following unique constraints were added to 3 tables that required validation of the barcode and UPC. Note, that it was perfectly OK to have empty UPC/Barcode values as uniqueness was only needed for non-empty values. Luckily, SQL Server 2008 and up allows to add filters to the indexes.

CREATE UNIQUE NONCLUSTERED INDEX barcode_unique
ON dbo.i_items(barcode ASC)
WHERE barcode!='';
GO
 
CREATE UNIQUE NONCLUSTERED INDEX UPC_unique
ON dbo.i_items(UPC ASC)
WHERE UPC!='';
 
GO

It is rather obvious, that this is a partial solution. It doesn't do a cross-columns (UPC and Barcode) check and it doesn't do cross-tables check. Implementing a true database solution for the above requirements seems a non-trivial task and that solution is not apparent. However, it is better than no validation at all and we had to correct duplicates that already existed in one of the tables.

1.b Stored procedure to check for Uniqueness

A new stored procedure to check for uniqueness of the passed code (UPC or barcode) was also added. It returned a descriptive information about a possible duplicate.

-- Create date: 08/25/2017
-- Modify date:
-- Description: Checks if barcode or UPC is unique across items, i_items and i_mcode tables
-- Example Call: execute [dbo].[SiriusSP_CheckBarcodeUPCUniqueness] @code = '10'
-- =============================================
CREATE PROCEDURE [dbo].[SiriusSP_CheckBarcodeUPCUniqueness]
    (@code varchar(100))
AS
    set nocount on;
    select i.item_Id as Id, i.Department, i.Category, i.Item, i.descrip as ItemDescription,
    cast('' as varchar(100)) as MatrixDescription,  i.barcode as Code, 'Items' as TableName, 'Barcode' as ColumnToTest
    from dbo.items i where i.barcode = @code
    UNION ALL
    select i.item_Id as Id, i.Department, i.Category, i.Item, i.descrip as ItemDescription,
    cast('' as varchar(100)) as MatrixDescription,  i.UPC as Code, 'Items' as TableName, 'UPC' as ColumnToTest
    from dbo.items i where i.upc = @code
    UNION ALL
    select ii.invent_id as Id,  i.Department, i.Category, i.Item, i.descrip as ItemDescription,
    ii.descrip as MatrixDescription,    ii.barcode as Code, 'I_Items' as TableName, 'Barcode' as ColumnToTest
    from dbo.items i inner join dbo.i_items ii on i.item_id = ii.ItemId
    where ii.barcode = @code
    UNION ALL
    select ii.invent_id as Id,  i.Department, i.Category, i.Item, i.descrip as ItemDescription,
    ii.descrip as MatrixDescription,    ii.UPC as Code, 'I_Items' as TableName, 'UPC' as ColumnToTest
    from dbo.items i inner join dbo.i_items ii on i.item_id = ii.ItemId
    where ii.upc = @code
    UNION ALL
    select m.pri_key as Id,  i.Department, i.Category, i.Item, i.descrip as ItemDescription,
    coalesce(ii.descrip,'') as MatrixDescription, m.code as Code, 'I_Mcode' as TableName, 'Code' as ColumnToTest
    from dbo.i_mcode m inner join dbo.items i on m.ItemId = i.item_id
    LEFT join dbo.i_items ii on m.invent_id = ii.invent_id 
    where m.code = @code;
return 0;

The procedure code is relatively simple and it returns an existing value or no rows if this is a unique code.

2. Server Side implementation    

     

2.a Repository Code using Entity Framework


The web project has several layers (and several separate projects) as part of one solution. 

There are Models and Data projects which are created based on the existing SQL Server database using excellent Reverse POCO Generator by Simon Hudges.

There is also Repository project with classes using Entity Framework for the database operations.

This is the Item Repository class code that calls the procedure described above:

/// <summary>
        /// Checks code for uniqueness using SiriusSP_CheckBarcodeUPCUniqueness
        /// </summary>
        /// <param name="existingCode"></param>
        /// <returns></returns>
        public string CheckCodeUniqueness (UniquenessTest existingCode)
        {
            StringBuilder sb = new StringBuilder("");
 
            SqlParameter codeParameter = new SqlParameter("@code", SqlDbType.VarChar, 100);
            codeParameter.Value = existingCode.Code;
 
            var result = _siriusContext.CoreContext.ExecuteStoreQuery<UniquenessTest>("execute dbo.SiriusSP_CheckBarcodeUPCUniqueness @code = @code",
               codeParameter);
 
            foreach (var row in result)
            {
                if (existingCode.ColumnToTest.Equals(row.ColumnToTest, StringComparison.OrdinalIgnoreCase) &&
                    existingCode.TableName.Equals(row.TableName, StringComparison.OrdinalIgnoreCase) &&
                    existingCode.Id == row.Id)
                {
                    // It's an existing row - do nothing
                }
                else
                {
                    var description = row.ItemDescription.Trim();
                    if (!String.IsNullOrWhiteSpace(row.MatrixDescription))
                    {
                        description = " " + row.MatrixDescription.Trim();
                    }
                    
                    var itemInfo = string.Format("\"{0}\" ({1} {2} {3})", description,
                        row.Department.Trim(), row.Category.Trim(), row.Item.Trim());
 
                    sb.AppendLine(string.Format(Messages.valueAlreadyExists, itemInfo, row.TableName.Trim(), row.ColumnToTest.Trim()));
                }
            }
               
            return sb.ToString();
        }


The code is relatively straightforward and returns an error message in case it's the duplicate code. It uses a specially created class for passing code along with some other attributes and checking its uniqueness:
 
public partial class UniquenessTest
   {
       public int Id { get; set; }
       public String Department { get; set; }
       public String Category { get; set; }
 
       public String Item { get; set; }
 
       public String ItemDescription { get; set; }
 
       public String MatrixDescription { get; set; }
 
       public String Code { get; set; }
 
       public String TableName { get; set; }
 
       public String ColumnToTest { get; set; }
   }

2.b Web API implementation


Since there was a  need to call the uniqueness check from two API Controller classes (ItemAPIController and IItemsApiController) a new base class was developed called BaseItemApiController. This class has the following method:

protected void ValidateBarcodeUPC(String barcode, String upc, int Id, String tableName = "items")
       {
           if (!String.IsNullOrWhiteSpace(barcode))
           {
               UniquenessTest model = new UniquenessTest();
 
               model.Id = Id;
               model.ColumnToTest = "Barcode";
               model.Code = barcode;
               model.TableName = tableName;
 
               String response = _itemAdapter.CheckCodeUniqueness(model);
 
               if (!String.IsNullOrEmpty(response))
               {
                   var errorResponse = Request.CreateErrorResponse(HttpStatusCode.InternalServerError, response);
                   throw new HttpResponseException(errorResponse);
               }
           }
 
           if (!String.IsNullOrWhiteSpace(upc))
           {
               UniquenessTest model = new UniquenessTest();
 
               model.Id = Id;
               model.ColumnToTest = "upc";
               model.Code = upc;
               model.TableName = tableName;
 
               String response = _itemAdapter.CheckCodeUniqueness(model);
 
               if (!String.IsNullOrEmpty(response))
               {
                   var errorResponse = Request.CreateErrorResponse(HttpStatusCode.InternalServerError, response);
                   throw new HttpResponseException(errorResponse);
               }
           }
       }

This method is called when the item (or i_item) is about to be saved. That method was added to be called there as an extra protection in case two users attempt to add/save data at the same time.

The main work is done in another method of the ItemApiController class:

[Route("checkCodeUniqueness")]
       [HttpPut]
       public IHttpActionResult CheckCodeUniqueness(UniquenessTestViewModel testModel)
       {
           if (String.IsNullOrWhiteSpace(testModel.Code))
           {
               testModel.ErrorMessage = "";
               testModel.IsValid = true;
           }
           else
           {
               UniquenessTest model = AutoMapperConfig.Mapper.Map<UniquenessTest>(testModel);
               String errorMessage = _itemAdapter.CheckCodeUniqueness(model);
 
               testModel.ErrorMessage = errorMessage;
               testModel.IsValid = String.IsNullOrEmpty(errorMessage);
           }
           return Ok(testModel);
       }

This is the method used to do the interactive asynchronous validation from the Web page. As we can see, the method is very simple and it always returns OK status while the returned model will tell if the code is valid or not and provide an error message in the latter case. This is not discussed in any of the blogs listed in the Research section of this article and that was a new idea in the presented solution in order to implement dynamic message (and not just some static error text). We'll see later in this article how that implementation is used.

3. Front-End Code


Now with the above code in place we'll see the actual front-end implementation. 
   

3.a  New AngularJS directive


In order to check for uniqueness of Barcode/UPC in a Web page, the following new directive was created.

(function (angular) {
    "use strict";
 
    var services,
        $log;
 
    angular.module("sysMgrApp").directive("smCodeUniqueValidator", ["ServiceLoader", smCodeUniqueValidator]);
 
    /**
     * smCodeUniqueValidator Directive
     * @param {} serviceLoader
     * @returns {Object}
     */
    function smCodeUniqueValidator(serviceLoader) {
        services = serviceLoader;
        $log = services.Log.getInstance("smCodeUniqueValidator");
 
        return {
 
            require: "ngModel",
 
            scope: {
                primaryKey: "=?",
                tableName: "@",
                columnToTest: "@",
                errorMessage: "=?",
                valueToCompare: "=?"
            },
 
            link: function ($scope, element, attrs, ngModel) {
                if (!ngModel) return;
 
                $scope.originalValue = ngModel.$modelValue;
 
                $scope.$watch('valueToCompare', function (valueToCompare) {
                    // watches for changes from valueToCompare binding
                    ngModel.$validate();
                });
 
                ngModel.$validators.codeUnique = function (modelValue, viewValue) {
                     
                    let status = true;
                    if (viewValue && $scope.valueToCompare) { // make sure both values are defined
                        if (_.isEmpty(viewValue.trim()) || _.isEmpty($scope.valueToCompare.trim())) {
                            status = true;
                        }
                        else {
                            if (viewValue.trim().toUpperCase() === $scope.valueToCompare.trim().toUpperCase()) {
                                status = false;
                            }
                        }
                    }
                    return status;
                };
 
                ngModel.$asyncValidators.smCodeUnique = function (modelValue, viewValue) {
                    
                    if (!viewValue || _.isEmpty(modelValue.trim()) || modelValue === $scope.originalValue) {
                        return services.Q.when(true);
                    }
 
                    var deferred = services.Deferred;
                    $log.info("Firing server-side validations for " + $scope.tableName + '.' + $scope.columnToTest + " value=" + viewValue);
                    let codeObject = {
                        id: $scope.primaryKey, tableName: $scope.tableName,
                        columnToTest: $scope.columnToTest, code: viewValue
                    };
 
                    $scope.originalValue = ""; // We need to clear the value at this point
                    return services.Http.put('api/items/checkCodeUniqueness', codeObject).then(
                        function (response) {
                            if (!response.data.isValid) {
                                $scope.errorMessage = response.data.errorMessage;
                                deferred.reject(response.data.errorMessage);
                            }
                            else {
                                deferred.resolve(response.data);
                            }
                            return deferred.promise;
                        }
                    );
                };
                //     }
            }
        };
    }
})(angular);


The above is the whole code of the directive. We can see, that this directive has multiple duties. The part that deals with the async validators starts with the following code:

ngModel.$asyncValidators.smCodeUnique = function (modelValue, viewValue) {
                     
                    if (!viewValue || _.isEmpty(modelValue.trim()) || modelValue === $scope.originalValue) {
                        return services.Q.when(true);
                    }

We can see, that the original value is saved at the top and then compared with the current model's value. The reason for such implementation is that the async validators are firing too many times when the page is loading (and when there is no need for them to fire). This seems to be a bug in the current AngularJs version and the issue is documented here. By introducing this original value and saving it up front the extra calls to the back-end were eliminated.

As we can also see, the directive also checks against valueToCompare using regular validators. This is done because we have both UPC and Barcode on the same page and we want to make sure the newly entered values are not the same.

Finally, there is a watch for the valueToCompare. This was done in order to make sure that if, for example, the same values are entered by the user in Barcode and UPC input textboxes, then an error is returned in UPC textbox and then the value is cleared in the Barcode input control, the error will be gone from the UPC input control at this point. This is the idea implemented based on one of the stackoverflow questions.

3.b. Using new directive in HTML page


Now let's see the HTML code for the page that is using this new directive.

<div class="col-xs-6">
            <div class="controls">
                <label class="control-label"
                       ng-hide="editRetailTrackingForm.barcode.$dirty && (editRetailTrackingForm.barcode.$error.maxlength || editRetailTrackingForm.barcode.$error.codeUnique || editRetailTrackingForm.barcode.$error.smCodeUnique)">
                    @Labels.barcode:
                </label>
 
                <label class="field-validation-error control-label-error animate-show"
                       ng-show="editRetailTrackingForm.barcode.$dirty && (editRetailTrackingForm.barcode.$error.codeUnique || editRetailTrackingForm.barcode.$error.smCodeUnique)">
                    @String.Format(Labels.duplicateX, Labels.barcode)
                </label>
 
                <input type="text" name="barcode" id="barcode"
                       ng-model="currentItem.barcode"
                       sm-code-unique-validator
                       table-name="items"
                       column-to-test="barcode"
                       error-message="itemBarcodeErrorMessage"
                       value-to-compare="currentItem.upc"
                       primary-key="currentItem.itemId"
                       ng-model-options="{  debounce: { default : 500, blur: 0 }}"
                       class="form-control"
                       ng-maxlength="100" />
                 
                <div class="loading" ng-if="editRetailTrackingForm.barcode.$pending">@String.Format(Messages.validatingX, Labels.barcode)</div>
 
                <div ng-if="editRetailTrackingForm.barcode.$dirty">
                    <div ng-messages="editRetailTrackingForm.barcode.$error">
                        <div ng-message="maxlength">
                            <label class="field-validation-error control-label-error animate-show">
                                @String.Format(Messages.cannotExceed, Labels.barcode, "100")
                            </label>
                        </div>
                        <div ng-message="codeUnique">
                            <label class="field-validation-error control-label-error animate-show">
                                @String.Format(Messages.xMustBeDifferentThanY, Labels.barcode, Labels.upc)
                            </label>
                        </div>
                        <div ng-message="smCodeUnique">
                            <div class="info-text-block info-text-block-error">
                                {{itemBarcodeErrorMessage}}
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
 
    <div class="form-group">
        <div class="controls">
            <div class="col-xs-6">
                <div ng-repeat="n in [1,2]" ng-form="udfTextChange">
                    <label class="control-label"
                           ng-hide="udfTextChange.iText.$error.maxlength && udfTextChange.iText.$dirty">
                        {{metaData.prefsRl['ilText' + n]}}
                    </label>
 
                    <label class="field-validation-error control-label-error animate-show"
                           ng-show="udfTextChange.iText.$error.maxlength && udfTextChange.iText.$dirty">
                        @String.Format(Messages.cannotExceed, String.Format(Labels.userDefinedX, Labels.text), "100")
                    </label>
                    <input type="text" name="iText"
                           ng-model="currentItem['iText' + n]"
                           class="form-control"
                           ng-maxlength="100" />
                </div>
            </div>
        </div>
        <div class="col-xs-6">
            <div class="controls">              
 
                <label class="control-label"
                       ng-hide="editRetailTrackingForm.upc.$dirty && (editRetailTrackingForm.upc.$error.maxlength || editRetailTrackingForm.upc.$error.codeUnique || editRetailTrackingForm.upc.$error.smCodeUnique)">
                    @Labels.upc:
                </label>
 
                <label class="field-validation-error control-label-error animate-show"
                       ng-show="editRetailTrackingForm.upc.$dirty && (editRetailTrackingForm.upc.$error.codeUnique || editRetailTrackingForm.upc.$error.smCodeUnique)">
                    @String.Format(Labels.duplicateX, Labels.upc)
                </label>
 
                <input type="text" name="upc" id="upc"
                       ng-model="currentItem.upc"
                       sm-code-unique-validator
                       table-name="items"
                       column-to-test="upc"
                       error-message="itemUpcErrorMessage"
                       primary-key="currentItem.itemId"
                       value-to-compare="currentItem.barcode"
                       ng-model-options="{  debounce: { default : 500, blur: 0 }}"
                       class="form-control"
                       ng-maxlength="100" />
 
                <div class="loading" ng-if="editRetailTrackingForm.upc.$pending">@String.Format(Messages.validatingX, Labels.upc)</div>
 
                <div ng-if="editRetailTrackingForm.upc.$dirty">
                    <div ng-messages="editRetailTrackingForm.upc.$error">
                        <div ng-message="maxlength">
                            <label class="field-validation-error control-label-error animate-show">
                                @String.Format(Messages.cannotExceed, Labels.upc, "100")
                            </label>
                        </div>
                        <div ng-message="codeUnique">
                            <label class="field-validation-error control-label-error animate-show">
                                @String.Format(Messages.xMustBeDifferentThanY, Labels.upc, Labels.barcode)
                            </label>
                        </div>
                        <div ng-message="smCodeUnique">
                            <div class="info-text-block info-text-block-error">
                                {{itemUpcErrorMessage}}
                            </div>
                        </div>
                    </div>
                </div>
 
            </div>


We can see in this implementation the usage of ng-messages with ng-message for each possible error condition. We can also see that the message associated with the smCodeUnique error is dynamic and displayed using {{}} syntax through controller's property.

3.c. Controller.js code


  The only code that we needed to add in the controller.js for that implementation was initializing of our error messages:

self.scope.itemBarcodeErrorMessage = "";
self.scope.itemUpcErrorMessage = "";
 
self.scope.iItemBarcodeErrorMessage = "";
self.scope.iItemUpcErrorMessage = "";

We have 4 variables because there are two actual HTML pages using that directive: RetailTracking page and RetailMatrix page. The above code showed implementation in the RetailTracking page, while the implementation of RetailMatrix page is very similar.

   

4. Seeing it all in Action



Now we need to see it all in action. Few screen shots demonstrate the behavior in run-time.



The above screen shot demonstrates server-side async validators with dynamic error message.



This screen shot demonstrates the regular validators when we try to type the same value in both input controls.


Conclusion


This article demonstrated a real world scenario of implementing asynchronous validators in HTML pages using AngularJs 1.5+.  


See Also


This article participated in the TechNet Guru Contributions for October 2017 and won the gold prize.