AngularJS Tutorial - 9.2, ngGrid Server filtering

In this section we will discover

The samples can be run with Firefox, in general or by running, into the directory,

Download

The source for this part is here: 09filternggrid.zip.

Screenshot

Filtering

Data service

We should add the handling of filters on the customersDataService. Since our API accept queryes of type "field=value & field1=value1" we willl consider a filter with the following values

var filter = {
    first_name:"",
    last_name:""
}

When a filter value is empty or undefined we will do nothing! The "for prop in filter" seeks all of the properties of the filter and loads only the defined and not empty properties. Then the filter is converted to JSON to be added to the query string.

    this.list=function(currentPage,pageSize,count,filter){
        var realFilter = {};
        var didSomething = false;
        for(var prop in filter){
            var value=filter[prop];
            if(value!=undefined && value !==null && value.length>0){
                realFilter[prop] = value;
                didSomething = true;
            }
        }
        
        var start = currentPage * pageSize;
        var end = start + count;
        var result = "/customers?range=["+start+","+end+"]";
        if(didSomething){
            result+="&filter="+encodeURI(JSON.stringify(realFilter));
        }
        return result;
    };

The generic controller

We will add a new variable for the list controller

    $scope.filter = {};

We will need to add two functions on the service, to reset the search and to do the search

        $scope.resetSearch = function(){
            $scope.filter = {};
            $scope.loadData(0);
        }
        
        $scope.search = function(){
            $scope.loadData(0);
        }

At the same time we will add to the data service function the filter, as parameter

    var address= dataService.list(requiredPage,$scope.pageSize,$scope.pageSize+1,$scope.filter);

After this we will enable the pagination. To do this we must watch the paging options

When the page size changes we must reload the grid data

       
        $scope.$watch('pagingOptions', function (newVal, oldVal) {
            if(newVal.pageSize !== oldVal.pageSize){
                $scope.loadData(1);
            }else if (newVal !== oldVal && (
                newVal.currentPage !== oldVal.currentPage||newVal.pageSize !== oldVal.pageSize) ) {
                
                $scope.loadData(newVal.currentPage);
            }
        }, true);

The customers controller

We will change the page sizes in the paging options from only 10 to an array of possible values

pageSizes: [10, 25, 50, 100]

The list template

On the list template we will add two input fields connected to the $scope.filter. a reset button and a search button:

        <div class="form-group">
                <input class="form-control input-sm" type="text" ng-model="filter.first_name" placeholder="First Name"/>
        </div>
        <div class="form-group">
                <input class="form-control input-sm" type="text" ng-model="filter.last_name" placeholder="Last Name"/>
        </div>
        <div class="form-group">
            <div class="input-group">
                <a class="btn btn-default btn-sm" ng-click="search()"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></a>
                <a class="btn btn-default btn-sm" ng-click="resetSearch()"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span></a>
            </div>
        </div>

At this point

The combo directive

A combo box/select exist for angular-ui but is dependant on JQuery, here is a pure angular implementation

Directive css

We will add to the common css a class to hide the buttons

.menuVisible{ display:block; }

Directive Template

To draw the combo we will use the standard bootstrap css, creating a "app/common/dropdown.html" template.

We use the additional "inDropdown" class to check if a click was made inside the dropdown. We will show a specific placeholder defined on the directive. The class "menuVisible" described befor will be used for when the dropdown is open. When selecting an item the dropdown will call the "select(item)" callback.

The check with the item[value] is used to allow the usage of index/label pairs inside the combo, when we have an index for the item and a descriptive label.

<div class="dropdown inDropdown">
    <button class="btn btn-default btn-sm dropdown-toggle inDropdown" type="button" aria-expanded="true"
            ng-click="show();">
        <span class="inDropdown" 
            ng-if="!isPlaceholder">{{selectedValue}}</span><span 
            ng-if="isPlaceholder" class="placeholder inDropdown" >{{placeholder}}</span>
        <span class="caret inDropdown"></span>
    </button>
    <ul class="dropdown-menu inDropdown" role="menu" ng-class="{'sgDropdownVisible':listVisible}">
        <li ng-repeat="item in list" role="presentation" class="inDropdown"><a role="menuitem" class="inDropdown" tabindex="-1" ng-click="select(item)" >
            {{value !== undefined ? item[value] : item}}</a></li>
    </ul>
</div>
</div>

### Dropdown directive 

First we define a way to use a specific template for the dropdown as we did for the dialog. In which we 
add a flag to verify if a opened dropdown exists. Is normal that only one dropdown is opend at a given
time.

<pre>
common.service("sgDropdown.config",[function(){
    return {
        //The default dropdown template
        dropdownTemplate : "sgDialogTemplate.html",
        dropdownOpened:false
    }
}])

Then goes the directive. We use an element directive, with a placeholder, a list of items to show and a binded selected variable. Optionally we could select a key and a value. If they are not present the list[index] item is taken both as key and value.

Finally we add an optional function to invoke when the selection is changed (note the & and ? markers on the binding.

We get the template as we did for the dialog with $http.get and $compile, association to the scope, Then we add an event handler on the root scope that will hide the dropdown when invoked.

$rootScope.$on("documentClicked", function(inner, target)

Other than this we will watch the "selected" variable and when it changes we change the value of the current item and invoke the eventual "on change" callback.

We have sevarl internal variables,

The majority of the code is made to handle the two ways to use the combo:

We will create an "E" (element) item. The parameters will be

The item will require the presence on the directive itself of the directive "ng-model"

.directive("sgDropdown",['$rootScope','$http','sgDropdown.config','$templateCache','$compile', 
        function($rootScope,$http,sgDropdownConfig,$templateCache,$compile) {
    
    return {
        restrict: "E",
        //Create an isolated scope
        scope: {
            //When nothing is selected
            placeholder: "@sgPlaceholder",
            //Items to show
            list: "=sgList",
            //Allow null
            nullable: "=?sgNullable",
            //Optional key field
            key: "@?sgKey",
            //Optinal value field
            value: "@?sgValue",
            //To call on selecton changed
            selChange:"&?sgChange"
        },
        require: 'ngModel',
        ....

We use an isolated scope. We can then declare every variable we need on the scope. The object passed into the require is passed as the 4th parameter on the link function.

NOTE: When requiring more than one thing (like both form and ngModel) we will declare like this
    require: ['form','ngModel'],
And the 4th parameter will be of the format
    [formObject,modelObject]

First the parameters are checked, then the various functions are created:

        ...
        link: function(scope,element, attrs,model) {
            scope.selectedItem = undefined;
            scope.selectedValue = undefined;
            scope.listVisible = false;
            scope.isPlaceholder = true;
            
            
            //Initialize the required attribute
            var required = false;
            if(attrs.required==""){
                required = true;
            }
            var nullable = false;
            if(scope.nullable){
                nullable = scope.nullable;
            }
            
            // If key is specified even value must be specified and vice-versa
            if( (scope.key && !scope.value) || (!scope.key && scope.value) ){
                throw "sg-key and sg-value must be both/none declared! in sg-dropdown element!";
            }
            
            //When selection 
            scope.select = function(item) {
                scope.listVisible = false;
                var newValue = undefined;
                if(item){
                    newValue = scope.key?item[scope.key]:item;
                }
                model.$setViewValue(newValue);
            };

            //Verify that the item is selected
            scope.isSelected = function(item) {
                if(scope.key) return item[scope.key] === scope.selectedItem[scope.key];
                return item === scope.selectedItem;
            };

            //Show the dropdown list
            scope.show = function() {
                scope.listVisible = !scope.listVisible;
                sgDropdownConfig.dropdownOpened = scope.listVisible;
            };
            ....

We will need then to watch for changes on the currently selected item, or better on the model.$modelValue.

This function will handle setting the dirty flag on the model (with model.$dirty), then if the field is required, the validity of the model will be change through the model.$setValidity.

The parameter of setValidity are the validity constraint name, and a boolean telling if the value is correct or not

Finally the connected values are changed, for example the content of the currently selected item, and the optional on change function is called.

            //Watcher function for the selected value
            var selectValue = function(newValue,prevValue) {
                var matching = undefined;
                //Set the model direty
                if(newValue!=prevValue){
                    model.$dirty=true;
                }

                //Set the validation option
                if(required){
                    if(!newValue && !nullable){
                        model.$setValidity('required',false);
                    }else{
                        model.$setValidity('required',true);
                    }
                }
            
                //If no list is defined no other operations are needed
                if(!scope.list) return;
                
                //Find matching element
                for(var i=0;i<scope.list.length;i++){
                    var item = scope.list[i];
                if(scope.key){
                        if(item[scope.key]==newValue){
                            matching = item; 
                            break;
                        }
                }else{
                        if(item == newValue){
                            matching = item;
                            break;
                        }
                    }
                }
                
                //Select the items
                if(!matching){
                    scope.isPlaceholder = true;
                    scope.selectedItem = undefined;
                    scope.selectedValue = undefined;
                }else{
                    scope.isPlaceholder = false;
                    scope.selectedItem = item;
                    scope.selectedValue = scope.value?item[scope.value]:item;
                }
                
                //Invoke the selChange
                if(scope.selChange && attrs['sgChange']){
                    scope.selChange()(scope.selectedItem);
                }
        }

Another thing we need is closing the dropdown when someone click outside the dropdown anywhere. If a dropdown is opened the documentClicked event is broadcasted to all the scopes of the application and is intercepted by the directive that closes the dropdown.

Inside the directive this will be handled by an event handler


            //Clicking anywhere out of the item will close the dropdown
            $rootScope.$on("documentClicked", function(inner, target) {
                if(!scope.listVisible) return;
                scope.listVisible = false;
                if (!scope.$$phase) {
                    scope.$apply();
                }
            });

While a global service will handle the registration of the event

.run(["sgDropdown.config",'$rootScope',function(sgDropdownConfig,$rootScope){
    //loosely based on https://www.codementor.io/angularjs/tutorial/create-dropdown-control
    //Handle the click on the rest of document to close the dropdown
    angular.element(document).on("click", function(e) {
        if(!sgDropdownConfig.dropdownOpened) return;
        if(e.target.className.indexOf('inDropdown')>0) return;
        $rootScope.$broadcast("documentClicked", angular.element(e.target));
    });
}])

Using the combo

Now into the list.html we will add after the filters section the following part. Into the sg-list we could use a variable or a function or directly an array. This way changing the combo value will change the pageSize with a combo separated from the standard one in ngGrid

    <div class="form-group">
        <sg-dropdown sg-placeholder="Size..." sg-list="[10,25,50,100]" ng-model="pagingOptions.pageSize"></sg-dropdown>
    </div>

Last modified on: May 04, 2015