New version Angular 9
In this section we will discover
The samples can be run with Firefox, in general or by running, into the directory,
The source for this part is here: 09filtercombo.zip.
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; };
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);
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
One thing that is often seen is a way and apparatus to change the page size of the grid. We setted a scope variable for that: the pageSize, with two-ways binding.
A combo box/select exist for angular-ui but is dependant on JQuery, here is a pure angular implementation
We will add to the common css a class to hide the buttons
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)); }); }])
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="pageSize"></sg-dropdown> </div>
In the grid directive we setted the watch on pageSize, now it will become useful!! It would reload the data when the page size changes!!
scope.$watch(function(){ return scope.sgPageSize; },function(){ scope.sgLoadData()(0) })