New version Angular 9
Angular JQueryLess Server Grid Filter and Combo Directive
In this section we will discover
- How to add filters to the sgGridService
- How to build a pure AngularJS combo/options box
The samples can be run with Firefox, in general or by running, into the directory,
- If you have Python 2.x: python -m SimpleHTTPServer 8000
- If you have Python 3.x: python -m http.server 8000
- If you have Perl:
- cpan HTTP::Server::Brick # install dependency
- perl -MHTTP::Server::Brick -e '$s=HTTP::Server::Brick->new(port=>8000); $s->mount("/"=>{path=>"."}); $s->start'
- If you are under windows: "C:\Program Files (x86)\IIS Express\iisexpress.exe" /path:%CD% /port:8000
Download
The source for this part is here: 09_filter_combo.zip.

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);
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
- Any time we call the loadData (even from pagination buttons) the function will use the filters setted
- When we add/delete/update items the filters on the grid will be kept
The combo directive
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
Directive css
We will add to the common css a class to hide the buttons
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.
### 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.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,
- selectedItem: The currently selected object (undefined if not present)
- selectedValue: The label of the currently selected item (undefined if not present)
- listVisible: If the dropdown is open
- isPlaceholder: If should show the placeholder
The majority of the code is made to handle the two ways to use the combo:
- With key/value
- With a single string/value
We will create an "E" (element) item. The parameters will be
- placeholder: The placeholder to show when nothing is selected
- list: The function called to retrieve the data to show
- nullable: Set to true if the null is allowed
- key: (optional) the key of the list item
- value: (optional) the diescription to show for the list item
- selChange: (optional) the function to call when selection changes
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:
- select: To selected the current item. Note that at the end we call "model.$setViewValue(newValue);"
This is made to trigger the changes on the model value! - isSelected: To verify if the current item is selected
- show: To show the dropdown list.
...
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
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
The grid directive
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)
})