Designing Basic and Advanced Dynamic Search Forms using Angular Material and Grid-UI Example
Designing Basic and Advanced Dynamic Search Forms using Angular Material and Grid-UI Example
In this tutorial we will discuss how to design basic and advanced dynamic search form techniques using Angular Material and Grid UI concentrating on the front-end components. In our last tutorial, “Implementing Basic and Advanced Search using Angular Material Design, Grid-UI, Spring MVC REST API and MongoDB Example” we focused on back-end components using Java, Spring Framework, Jackson, log4J and MongoDB.
In our advanced tab, I have added the ability to add rows dynamically to the form and perform the necessary validations. AngularJS supports dynamic form creating using ng-repeat and ng-form directives. We will discuss these in more details in this tutorial.
What is Covered in this Tutorial ?
- What is Angular Material and Google’s Material Design ?
- Getting Started
- Complete Project Overview
- RESTful Web Service End Points
- The Basic HTML Template Structure
- Angular Material Toolbar
- Adding Company Logo to our Toolbar
- Adding Responsive Title to our Toolbar using Angular/Flexbox
- Angular Material md-menu Directive
- Using Angular Material’s md-tabs and md-tab Directive
- Using Angular’s ng-change Directive
- Dynamic Advanced Search Form in AngularJS using ng-repeat and ng-form Directives
- Validations Correctly Displayed with ngForm
- Validations Incorrectly Displayed without ngForm
- The Complete Main Application Page (index.jsp)
- Our AngularJS application file (app.js)
- Cascading Style Sheets (styles.css)
- Testing out the application using Web Services
- Download the Complete Source Code
What is Angular Material and Google’s Material Design ?
Angular Material is a reference implementation of Google’s Material Design specification but it is also a user interface (UI) component framework. Google’s Material Design has made it a priority to utilize good design principles with their goals stated in their specifications document. These goals are encompassed in the following concepts:
MATERIAL DESIGN GOALS
- Develop a single underlying system that allows for a unified experience across platforms and devices.
- Mobile precepts are fundamental.
- Touch, voice, mouse, and keyboard are all first-class input methods.
Angular Material Associate Search Application
Getting Started
In order to run this tutorial yourself, you will need the following:
- Java JDK 1.6 or greater
- Favorite IDE Spring Tool Suite (STS), Eclipse IDE or NetBeans (I happen to be using STS because it comes with a Tomcat server built-in)
- Tomcat 7 or greater or other popular container (Weblogic, Websphere, Glassfish, JBoss, VMWare vFabric, etc). For this tutorial I am using VMware vFabric tc Server Developer Edition which is essentially an enhanced Tomcat instance integrated with Spring STS
- Angular Material – reference implementation of Google’s Material Design using AngularJS.
- Angular UI Grid – Native AngularJS implementation of dynamic grid with standard support of sorting, filtering, column pinning, grouping, in-place editing, expandable rows, internationalization, customizable templates,large set support and plug-in support.
- Font Awesome provides over 600 icons all in one font. No javascript required as Font-Awesome utilizes CSS. I am using for some icons. If you like you can use material icons instead but code (html) changes will be required.
- JQuery is a fast, small, and feature-rich JavaScript library. In this example, we are using for simple document traversal.
- MongoDB is an open-source document database designed for ease of development and scaling.
- Jackson Mapper for Object to JSON and vice-versa serialization/deserialization
- log4J (for logging purposes)
Required Front-End Libraries
angular-material/ angular-material-icons/ angularjs/ jquery angular-animate.js angular-spinner.js angular-touch.js angular-tree-control.js angular.js animate.min.css app.js font-awesome.css font-awesome.min.css jquery-3.1.0.min.js main.css mask.js material-icons.css moment.js spin.js styles.css ui-grid.css ui-grid.eot ui-grid.js ui-grid.min.css ui-grid.svg ui-grid.ttf ui-grid.woff
Complete Project Overview
I have added the project overview to give you a full view of the structure and show you all files contained in this sample project from the UI perspective this time (starting from the WebContent).
RESTful Web Service End Points
# | URI | Method | Description |
---|---|---|---|
1 | /rest/status | GET | Displays the standard status message. |
2 | /rest/employees | GET | Retrieves all employee objects from MongoDB returning them as a JSON array. |
3 | /rest/getemployeebyid | GET | Retrieves an employee given the ID, returning the employee as JSON object. |
4 | /rest/standardSearch | GET | Retrieves all employee objects from MongoDB that match either the firstName, lastName or both, returning matches as a JSON array. |
5 | /rest/advancedSearch | GET | Retrieves employee objects from MongoDB that match selection criteria returning matches as a JSON array. |
6 | /rest/employee/add | POST | Inserts the employee into our MongoDB data store based on the contents of the JSON object |
7 | /rest/employee/update | PUT | Updates employee in our MongoDB data store based on the contents of the JSON object |
8 | /rest/employee/delete | DELETE | Delete employee in our MongoDB data store based on the ID |
The Basic HTML Template Structure
Our basic structure is as follows and has the following features:
- The ngApp directive is used to auto-bootstrap our AngularJS application.
- The viewport metatag is the user’s visible area of a web page.
- The ngCloak directive is used to ensure that AngularJS waits until the page is compiled prior to being rendered in the view.
<!DOCTYPE html> <html ng-app="app"> <head> <title>Associates Search</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Angular Material requires Angular.js Libraries --> <script src="include/angularjs/1.5.x/angular.min.js"></script> <script src="include/angularjs/1.5.x/angular-animate.min.js"></script> <script src="include/angularjs/1.5.x/angular-aria.min.js"></script> <script src="include/angularjs/1.5.x/angular-messages.min.js"></script> <!-- Additional dependent JS files --> <script src="include/xxxxx/1.x/xxxx.js"></script> <!-- Our Application Angular Library Global App File--> <script src="include/app.js"></script> </head> <body ng-cloak> <xx-directive></xx-directive> </body> </html>
Angular Material Toolbar
Use the md-toolbar directive to place a toolbar in your app.
We use toolbars to display the company/application logo and title of the application and show any buttons for that application. In my opinion, it also adds a more professional look and feel for the application or page.
Our toolbar consists of three distinct areas: Our company logo on the left hand side, the application title and vertical icon for our application menu.
Adding Company Logo to our Toolbar
I have placed the company logo inside span tags as then using CSS have made the image responsive based on certain screen sizes.
<div class="md-toolbar-tools"> <span> <img src="images/lark.png" alt="Lark Productions" /> </span> </div>
Adding Responsive Title to our Toolbar using Angular/Flexbox
By placing our application title inside of a span tag with flex attribute we allow it to take up the full screen width upon resizing. Our subtitle css class also has unique properties as I have defined it to be responsive to allow the application title to change in size.
Additionally, I have added @media CSS rule and defined different viewport sizes and span/subtitle classes which are to be used for each of those viewport sizes. Based on these different viewport sizes we change the subtitle class font-size and image sizes.
<span flex> <span class="subtitle">Associate Search</span> </span>
Angular Material md-menu Directive
Angular Material provides the md-menu directive to create menus that open when they are clicked. md-menu have two child elements. The first child element defines the anchor point that is used to open the menu. The second child element is md-menu-content defines the contents of the menu which it is open. Inside of the md-menu-content directive, a typical menu will contain several md-menu-item elements.
In my particular case, my menu contains three menu items:
<md-menu md-position-mode="target-right target" > <md-button id="about" aria-label="Open demo menu" class="md-icon-button" ng-click="$mdOpenMenu($event)"> <i class="material-icons">more_vert</i> </md-button> <md-menu-content width="4"> <md-menu-item> <md-button ng-click="toggleDebugMode()"> <i class="fa fa-wrench" aria-hidden="true"></i> Toggle Debug Mode </md-button> </md-menu-item> <md-menu-item> <md-button ng-click="toggleJSONOutput()"> <i class="fa fa-cogs" aria-hidden="true"></i> Toggle JSON Output </md-button> </md-menu-item> <md-menu-divider></md-menu-divider> <md-menu-item> <md-button ng-click="showAbout($event)"> <i class="fa fa-info-circle" aria-hidden="true"></i> About Employee Directory </md-button> </md-menu-item> </md-menu-content> </md-menu>
Using Angular Material’s md-tabs and md-tab Directive
In Angular Material the md-tabs directive is used as a container for the md-tab child directives to create tabs in the application.
md-tabs Attributes
# | Attribute | Description |
---|---|---|
1 | md-selected | Index of the active/selected tab starting with 0 |
2 | md-no-ink | If present, disables ink ripple effects when tab is clicked |
3 | md-no-ink-bar | If present, disables the selection ink bar |
4 | md-align-tabs | Attribute to set the position of tab buttons (top or bottom); |
5 | md-stretch-tabs | Attribute to indicate whether or not to stretch tabs: auto, always, or never; |
6 | md-dynamic-height | When enabled, the tab wrapper will resize based on the contents of the selected tab. |
7 | md-border-bottom | If present, shows a solid 1px border between the tabs and their content |
8 | md-center-tabs | When enabled, tabs will be centered provided there is no need for pagination |
9 | md-no-pagination | When enabled, tab pagination will remain off. Default is pagination which will automatically show < and > on either side of the tab bar when number of tab exceed page width. |
10 | md-swipe-content | When enabled, swipe gestures will be enabled for the content area to jump between tabs. |
11 | md-enable-disconnect | When enabled, scopes will be disconnected for tabs that are not being displayed. |
12 | md-autoselect | When present, any tabs added after the initial load will be automatically selected |
13 | md-no-select-click | When enabled, click events will not be fired when selecting tabs |
md-tab Attributes
# | Attribute | Description |
---|---|---|
1 | label | Optional attribute of String type to specify the label of the tab |
2 | ng-disabled | if the boolean expression evaluates to true |
3 | md-on-select | Evaluates the expression after the tab has been selected |
4 | md-on-deselect | Evaluates the expression after the tab has been deselected |
5 | md-active | Retrieves an employee given the ID, returning the employee as JSON object. |
Using Angular’s ng-change Directive
In the standard associate search form we are able to enter either the first name, last name or both. In this example, I have added the ng-change directive which calls the firstLastNameChange() function on change event. In this function we will set the variable $scope.isStandardRequired to either true or false. We use this to determine whether or not the Search Associate and Clear are either activated or disabled.
<%-- Standard Search Functionality --%> <md-tab label="Standard"> <md-content class="md-padding"> <div layout="row" layout-align="center start"> <div layout-fill> <form class="form-horizontal" id="standardForm" name="standardForm" novalidate="true"> <md-card> <md-content> <md-card-title> <md-card-title-text> <span class="md-headline"> Standard Associate Search<br/> </span> <span class="md-body-1"> Please fill in search criteria on click on Search button. </span> </md-card-title-text> </md-card-title> <div layout="column" ng-cloak class="md-inline-form"> <span us-spinner spinner-key="spinner-1"></span> <md-content layout-gt-sm="row" layout-padding> <div layout-gt-xs="row"> <md-input-container> <label>First Name</label> <input name="firstName" style="width: 250px;" ng-required="isStandardRequired" ng-change="firstLastNameChange()" ng-model="firstName" minlength="1" maxlength="50"> </md-input-container> </div> <div layout-gt-xs="row"> <md-input-container> <label>Last Name</label> <input name="lastName" style="width: 450px;" ng-required="isStandardRequired" ng-change="firstLastNameChange()" ng-model="lastName" minlength="1" maxlength="80"> </md-input-container> </div> </md-content> </div> <md-card-actions layout="row" layout-align="end center"> <md-button class="md-raised md-primary" ng-disabled="standardForm.$invalid" ng-click="standardSearch(firstName, lastName)"> <i class="fa fa-search"></i> Search Associate </md-button> <md-button class="md-raised md-primary" ng-disabled="standardForm.$invalid" ng-click="clearStandardForm()"> <i class="fa fa-times"></i> Clear </md-button> </md-card-actions> </md-content> </md-card> </form> <md-card> <%-- <md-content md-theme="docs-dark"> --%> <md-content> <md-card-title> <md-card-title-text> <span class="md-headline">Search Results</span> </md-card-title-text> </md-card-title> <div ui-grid="gridStdOptions" ui-grid-selection class="grid" ui-grid-resize-columns ui-grid-move-columns ui-grid-pagination ui-grid-edit></div> </md-content> </md-card> </md-content> </md-tab>
Dynamic Advanced Search Form in AngularJS using ng-repeat and ng-form Directives
In our advanced associate search form we allow multiple criteria objects to be created each with a field name, operator and a field value. We do this by using the ng-repeat directive on our multipleCriteria array. In order for our Angular validations to work properly we are forced to use ng-form directive. Without it, our validations don’t work correctly as Angular does not like us having the same name attribute repeated on mulitple lines within the same form.
Here is a small snippet to show the general use:
<div ng-repeat="criteria in multipleCriteria"> <div layout="column" ng-cloak class="md-inline-form"> <md-content layout-gt-sm="row" layout-padding-minimal> <ng-form name="fieldForm"> <div layout-gt-xs="row"> <md-input-container md-no-float> <label>Field Name</label> <md-select required="" style="width: 200px;" name="field" ng-model="criteria.field" ng-change="setSelectedField(criteria.field)"> <md-option ng-value="field" ng-repeat="field in fields" ng-disabled="field.selected">{{field.name}}</md-option> </md-select> <div ng-messages="fieldForm.field.$error" ng-show="fieldForm.field.$touched"> <div ng-message="required">Concept is required!</div> </div> </md-input-container> </div> </ng-form> </md-content> </div> </div>
Validations Correctly Displayed with ngForm
Using Angular Material Validations
Incorrect Dynamic Forms Validations
Prior to using name attribute in our form we would have been doing the form validation as stated below. However, doing so prevents the validations for work correctly in Angular.
In this code snippet below you will notice that we are NOT using the ng-form directive. We are therefore required to use the form name, which in our case is advancedForm {{ advancedForm.field.$error }} and {{ advancedForm.field.$touched }}, in our ng-messages directive. This would work fine if this was a static form and our form was not going to allow the same name to be used elsewhere in the form. However, since we are building our form dynamically using ng-repeat our code breaks as every additional criteria line on the form contains the same name attribute. A validation error in one criteria line would generate validation error message in the same attribute on all the criteria lines in the form (as shown in our screenshot below).
<div ng-repeat="criteria in multipleCriteria"> <div layout="column" ng-cloak class="md-inline-form"> <md-content layout-gt-sm="row" layout-padding-minimal> <div layout-gt-xs="row"> <md-input-container md-no-float> <label>Field Name</label> <md-select required="" style="width: 200px;" name="field" ng-model="criteria.field" ng-change="setSelectedField(criteria.field)"> <md-option ng-value="field" ng-repeat="field in fields" ng-disabled="field.selected">{{field.name}}</md-option> </md-select> <div ng-messages="advancedForm.field.$error" ng-show="advancedForm.field.$touched"> <div ng-message="required">Concept is required!</div> </div> </md-input-container> </div> </md-content> </div> </div>
Validations Incorrectly Displayed without ngForm
The Complete Main Application Page (index.jsp)
This is the index jsp file for our application which is rendered by the client browser.
<%@ page language="java"%> <!DOCTYPE html> <html ng-app="app"> <head> <title>Associates Search</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="include/angular-material/1.0.9/angular-material.css"> <script src="include/jquery-3.1.0.min.js"></script> <!-- Angular Material requires Angular.js Libraries --> <script src="include/angularjs/1.5.5/angular.min.js"></script> <script src="include/angularjs/1.5.5/angular-animate.min.js"></script> <script src="include/angularjs/1.5.5/angular-aria.min.js"></script> <script src="include/angularjs/1.5.5/angular-messages.min.js"></script> <script src="include/ui-grid.js"></script> <link rel="stylesheet" href="include/ui-grid.css"> <!-- Angular Material Library --> <script src="include/angular-material/1.0.9/angular-material.min.js"> </script> <!-- Get Material Icons/Font Awesome Icons --> <link rel="stylesheet" href="include/material-icons.css"> <link rel="stylesheet" href="include/font-awesome.css"> <link rel="stylesheet" href="include/animate.min.css"> <link rel="stylesheet" href="include/styles.css"> <link rel="stylesheet" href="include/main.css"> <script src="include/spin.js"></script> <script src="include/moment.js"></script> <script src="include/angular-spinner.js"></script> <!-- Our Application Angular Library --> <script src="include/app.js"></script> </head> <% String fullProtocol = request.getProtocol().toLowerCase(); String protocol[] = fullProtocol.split("/"); String baseUrl = protocol[0] + "://" + request.getHeader("Host"); String params = ""; boolean isDebug = false; String debugParam = request.getParameter("debug"); if (debugParam != null && (debugParam.toLowerCase().equals("true") || debugParam.toLowerCase().equals("yes") || debugParam.equals("1"))) { isDebug = true; } %> <body ng-controller="MainCtrl" ng-cloak> <md-toolbar> <div class="md-toolbar-tools"> <h2> <img src="images/lark.png" alt="Lark Productions" /> </h2> <span flex=""><span class="subtitle">Associate Search</span></span> <md-menu md-position-mode="target-right target" > <md-button id="about" aria-label="Open demo menu" class="md-icon-button" ng-click="$mdOpenMenu($event)"> <i class="material-icons">more_vert</i> </md-button> <md-menu-content width="4"> <md-menu-item> <md-button ng-click="toggleDebugMode()"> <i class="fa fa-wrench" aria-hidden="true"></i> Toggle Debug Mode </md-button> </md-menu-item> <md-menu-item> <md-button ng-click="toggleJSONOutput()"> <i class="fa fa-cogs" aria-hidden="true"></i> Toggle JSON Output </md-button> </md-menu-item> <md-menu-divider></md-menu-divider> <md-menu-item> <md-button ng-click="showAbout($event)"> <i class="fa fa-info-circle" aria-hidden="true"></i> About Employee Directory </md-button> </md-menu-item> </md-menu-content> </md-menu> </div> </md-toolbar> <md-content id="myContainer"> <md-tabs md-dynamic-height md-border-bottom> <%-- Standard Search Functionality --%> <md-tab label="Standard"> <md-content class="md-padding"> <div layout="row" layout-align="center start"> <div layout-fill> <form class="form-horizontal" id="standardForm" name="standardForm" novalidate="true"> <md-card> <md-content> <md-card-title> <md-card-title-text> <span class="md-headline"> Standard Associate Search<br/> </span> <span class="md-body-1"> Please fill in search criteria on click on Search button. </span> </md-card-title-text> </md-card-title> <div layout="column" ng-cloak class="md-inline-form"> <span us-spinner spinner-key="spinner-1"></span> <md-content layout-gt-sm="row" layout-padding> <div layout-gt-xs="row"> <md-input-container> <label>First Name</label> <input name="firstName" style="width: 250px;" ng-required="isStandardRequired" ng-change="firstLastNameChange()" ng-model="firstName" minlength="1" maxlength="50"> </md-input-container> </div> <div layout-gt-xs="row"> <md-input-container> <label>Last Name</label> <input name="lastName" style="width: 450px;" ng-required="isStandardRequired" ng-change="firstLastNameChange()" ng-model="lastName" minlength="1" maxlength="80"> </md-input-container> </div> </md-content> </div> <md-card-actions layout="row" layout-align="end center"> <md-button class="md-raised md-primary" ng-disabled="standardForm.$invalid" ng-click="standardSearch(firstName, lastName)"> <i class="fa fa-search"></i> Search Associate </md-button> <md-button class="md-raised md-primary" ng-disabled="standardForm.$invalid" ng-click="clearStandardForm()"> <i class="fa fa-times"></i> Clear </md-button> </md-card-actions> </md-content> </md-card> </form> <md-card> <md-content> <md-card-title> <md-card-title-text> <span class="md-headline">Search Results</span> </md-card-title-text> </md-card-title> <div ui-grid="gridStdOptions" ui-grid-selection class="grid" ui-grid-resize-columns ui-grid-move-columns ui-grid-pagination ui-grid-edit></div> </md-content> </md-card> </md-content> </md-tab> <%-- Advanced Search Functionality --%> <md-tab label="Advanced"> <md-content class="md-padding"> <div layout="row" layout-align="center start"> <div layout-fill> <form class="form-horizontal" id="advancedForm" name="advancedForm"> <md-card> <%-- <md-content md-theme="docs-dark"> --%> <md-content> <md-card-title> <md-card-title-text> <span class="md-headline">Advanced Associate Search<br/></span> <span class="md-body-1"> Please choose <em>Field Name</em> criteria from dropdown, select <em>Operator</em>, enter search <em>Value</em>. Include additional items as per your needs by hitting '+' icon.</span> </md-card-title-text> </md-card-title> <span us-spinner spinner-key="spinner-1"></span> <div ng-repeat="criteria in multipleCriteria"> <div layout="column" ng-cloak class="md-inline-form"> <md-content layout-gt-sm="row" layout-padding-minimal> <ng-form name="fieldForm"> <div layout-gt-xs="row"> <md-input-container md-no-float> <label>Field Name</label> <md-select required="" style="width: 200px;" name="field" ng-model="criteria.field" ng-change="setSelectedField(criteria.field)"> <md-option ng-value="field" ng-repeat="field in fields" ng-disabled="field.selected">{{field.name}}</md-option> </md-select> <div ng-messages="fieldForm.field.$error" ng-show="fieldForm.field.$touched"> <div ng-message="required">Concept is required!</div> </div> </md-input-container> </div> </ng-form> <div layout-gt-xs="row"> <md-input-container class="md-block"> <span class="label">is</span> </md-input-container> </div> <ng-form name="operatorForm"> <div layout-gt-xs="row"> <md-input-container> <label>Operator</label> <md-select required="" name="operator" ng-model="criteria.operator" style="min-width: 150px;"> <md-option ng-value="operator" ng-repeat="operator in operations | filter: { type: criteria.field.type }"> {{operator.name}} </md-option> </md-select> <div ng-messages="operatorForm.operator.$error" ng-show="operatorForm.operator.$touched"> <div ng-message="required">Operator is required!</div> </div> </md-input-container> </div> </ng-form> <div ng-if="criteria.field.type != 'date'"> <ng-form name="valueForm"> <div layout-gt-xs="row"> <md-input-container> <label>Value for {{criteria.field.name}}</label> <input required="" name="searchValue" style="width: 350px;" ng-model="criteria.value" minlength="1" maxlength="80"> <div ng-messages="valueForm.searchValue.$error" ng-show="valueForm.searchValue.$touched"> <div ng-message="required">Value is required!</div> </div> </md-input-container> </div> </ng-form> </div> <div ng-if="criteria.field.type == 'date'"> <ng-form name="valueForm"> <div layout-gt-xs="row"> <md-input-container> <label>Date of Hire (MM/DD/YYYY)</label> <label>Date Value</label> --> <input required="" type="date" style="width: 350px;" ng-model="criteria.value" /> <div ng-messages="valueForm.searchValue.$error" ng-show="valueForm.searchValue.$touched"> <div ng-message="required">Value is required!</div> </div> </md-input-container> </div> </ng-form> </div> <div layout-gt-xs="row"> <md-button class="md-fab md-mini md-primary md-hue-1" aria-label="add" ng-click="addNewCriteria($index)"> <md-tooltip md-direction="bottom">Add New Row</md-tooltip> <i class="fa fa-plus"></i> </md-button> <md-button class="md-fab md-mini md-primary md-hue-1" aria-label="remove" ng-click="removeCriteria($index)" ng-hide="multipleCriteria.length==1"> <md-tooltip md-direction="bottom">Remove Current Row</md-tooltip> <i class="fa fa-minus "></i> </md-button> </div> </md-content> </div> </div> <md-card-actions layout="row" layout-align="end center"> <md-button class="md-raised md-primary" ng-disabled="advancedForm.$invalid" ng-click="advancedSearch(multipleCriteria)"> <i class="fa fa-search"></i> Search Associate </md-button> <md-button class="md-raised md-primary" ng-click="clearAdvancedForm()"> <i class="fa fa-times"></i> Clear </md-button> </md-card-actions> </md-content> </md-card> </form> <md-card> <%-- <md-content md-theme="docs-dark"> --%> <md-content> <md-card-title> <md-card-title-text> <span class="md-headline">Search Results</span> </md-card-title-text> </md-card-title> <div ui-grid="gridAdvOptions" ui-grid-selection class="grid" ui-grid-resize-columns ui-grid-move-columns ui-grid-pagination ui-grid-edit></div> </md-content> </md-card> <div ng-if="enableJSON"> <div class="debug"> <div class="subtitle">JSON DEBUG OUTPUT</div> {{multipleCriteria}} </div> </div> </div> </div> </md-content> </md-tab> </md-tabs> <div ng-element-ready="setDefaults('<%=isDebug%>', '<%=baseUrl%>')"></div> </body> </html>
Our AngularJS application file (app.js)
var app = angular.module('app', ['ngMaterial', 'ngMessages', 'angularSpinner', 'ui.grid', 'ui.grid.resizeColumns', 'ui.grid.moveColumns']); app.config(['usSpinnerConfigProvider', function (usSpinnerConfigProvider) { usSpinnerConfigProvider.setDefaults({ lines: 13, // The number of lines to draw length: 5, // The length of each line width: 4, // The line thickness radius: 8, // The radius of the inner circle corners: 1, // Corner roundness (0..1) rotate: 0, // The rotation offset direction: 1, // 1: clockwise, -1: counterclockwise color: '#333', // #rgb or #rrggbb or array of colors speed: 1, // Rounds per second trail: 80, // Afterglow percentage shadow: false, // Whether to render a shadow hwaccel: false, // Whether to use hardware acceleration className: 'spinner', // The CSS class to assign to the spinner zIndex: 2e9, // The z-index (defaults to 2000000000) top: '50%', // Top position relative to parent left: '50%' // Left position relative to parent }); }]); app.config(function($mdThemingProvider) { $mdThemingProvider.theme("success-toast"); $mdThemingProvider.theme("error-toast"); $mdThemingProvider.theme("warning-toast"); }); app.filter('titlecase', function() { return function(s) { s = ( s === undefined || s === null ) ? '' : s; return s.toString().toLowerCase().replace( /\b([a-z])/g, function(ch) { return ch.toUpperCase(); }); }; }); app.directive('focusOn', function() { return function(scope, elem, attr) { scope.$on(attr.focusOn, function(e) { elem[0].focus(); }); }; }); app.config(function($mdDateLocaleProvider) { $mdDateLocaleProvider.formatDate = function(date) { return moment(date).format('MM/DD/YYYY'); }; }); app.config(function($mdThemingProvider) { // Configure a dark theme with primary foreground yellow $mdThemingProvider.theme('docs-dark', 'default') .primaryPalette('yellow') .dark(); }); app.service('ajaxService', function($http) { this.getData = function(URL, ajaxMethod, ajaxParams) { var restURL = URL + ajaxParams; console.log("Inside ajaxService GET..."); console.log("Connection using URL=[" + restURL + "], Method=[" + ajaxMethod + "]"); return $http({ method: ajaxMethod, url: restURL, }); }; this.postData = function(URL, ajaxMethod, jsonData, ajaxParams) { var restURL = URL + ajaxParams; console.log("Inside ajaxService POST..."); console.log("Connection using URL=[" + restURL + "], Method=[" + ajaxMethod + "]"); return $http({ method: ajaxMethod, url: restURL, headers: {'Content-Type': 'application/json'}, data: jsonData, }); }; }); app.directive('ngElementReady', [function() { return { priority: Number.MIN_SAFE_INTEGER, restrict: "A", link: function($scope, $element, $attributes) { $scope.$eval($attributes.ngElementReady); } }; }]); /* ----------------------------------------------------------------------- ** MAIN CONTROLLER *-------------------------------------------------------------------------*/ app.controller('MainCtrl', function ($scope, $rootScope, $http, $log, $mdDialog, $mdSidenav, $timeout, $filter, ajaxService, usSpinnerService) { $scope.firstName = ""; $scope.lastName = ""; $scope.isStandardRequired = true; $scope.enableJSON = true; $scope.advanced = {}; $scope.gridActive = false; console.log("$scope.gridActive....: " + $scope.gridActive); $scope.fields = [ {id: "firstName", name:"First Name", type: "string", selected: false}, {id: "lastName", name:"Last Name", type: "string", selected: false}, {id: "preferredName", name:"Preferred First Name", type: "string", selected: false}, {id: "jobDesc", name:"Job Description", type: "string", selected: false}, {id: "titleDesc", name:"Title Description", type: "string", selected: false}, {id: "companyName", name:"Company Name", type: "string", selected: false}, {id: "costCenter", name:"Cost Center", type: "string", selected: false}, {id: "hireDate", name:"Date of Hire", type: "date", selected: false} ]; $scope.operations = [ {id: "like", name:"like", type: "string"}, {id: "equalTo", name:"equal to", type: "string"}, {id: "notEqualTo", name:"not equal to", type: "string"}, {id: "equalTo", name:"equal to", type: "date"}, {id: "greaterThan", name:"greater than", type: "date"}, {id: "lessThan", name:"less than", type: "date"} ]; $scope.multipleCriteria = [{}]; $scope.gridOptions = { enableFiltering: true, enablePaginationControls: true, enableSorting: true, enableRowSelection: true, enableRowHeaderSelection: false, enableColumnResizing: true, paginationPageSizes: [10, 12, 15, 18], //18 paginationPageSize: 18, //18 }; $scope.gridOptions.columnDefs = [ { name: 'firstName', cellClass: 'gridField', displayName: 'First Name', width: 140, maxWidth: 160, minWidth: 130, enableHiding: false, enableCellEdit: false }, { name: 'lastName', cellClass: 'gridField', displayName: 'Last Name', width: 250, maxWidth: 480, minWidth: 220, enableHiding: false, enableCellEdit: false }, { name: 'jobDesc', cellClass: 'gridField', displayName: 'Job Desc', width: 160, maxWidth: 200, minWidth: 120, enableHiding: false, enableCellEdit: false }, { name: 'titleDesc', cellClass: 'gridField', displayName: 'Title Desc', width: 220, maxWidth: 300, minWidth: 150, enableHiding: false, enableCellEdit: false }, { name: 'costCenter', cellClass: 'gridField', displayName: 'Cost Center', width: 170, maxWidth: 200, minWidth: 90, enableHiding: false, enableCellEdit: false }, { name: 'hireDate', cellClass: 'gridField', displayName: 'Hire Date', width: 160, maxWidth: 200, minWidth: 120, enableHiding: false, enableCellEdit: false } ]; $scope.gridStdOptions = {}; $scope.gridAdvOptions = {}; angular.copy($scope.gridOptions, $scope.gridStdOptions); angular.copy($scope.gridOptions, $scope.gridAdvOptions); $scope.gridStdOptions.data = []; $scope.gridAdvOptions.data = []; $scope.addNewCriteria = function(index) { console.log("Row ID: " + index); $scope.multipleCriteria.splice(index+1, 0, {}); }; $scope.removeCriteria = function(index) { console.log("Row ID: " + index); $scope.multipleCriteria.splice(index, 1); }; $scope.setSelectedField = function(name) { console.log("fieldName: " + name); //var i = $scope.fields.indexOf(name); //$scope.fields[i].selected = true; //$scope.fields.splice(index, 1); }; $scope.startSpin = function(key) { usSpinnerService.spin(key); }; $scope.stopSpin = function(key) { usSpinnerService.stop(key); }; $scope.setTitleCase = function(input) { if (input != null ) { return input.replace(/\w\S*/g, function(txt){ return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); } }; $scope.setLowerCase = function(input) { if (input != null ) { return input.replace(/\w\S*/g, function(txt){ return txt.toLowerCase(); }); } }; $scope.setUpperCase = function(input) { if (input != null ) { return input.replace(/\w\S*/g, function(txt){ return txt.toUpperCase(); }); } }; $scope.currentTimeMillis = function(ts) { var date = new Date().getTime(); return date; }; $scope.toggleLeftSideNav = buildToggler('left'); $scope.closeLeftSideNav = function() { $mdSidenav('left').close() .then(function () { $log.debug("Closing Left Side Nav"); }); }; $scope.$on('compareTo-broadcast', function() { $scope.showPasswordMatchToast(); }); function buildToggler(navID) { return function() { $mdSidenav(navID) .toggle() .then(function () { $log.debug("toggle " + navID + " is done"); }); } }; $scope.$on('$viewContentLoaded', function() { console.log("viewContentLoaded event triggered..."); loadUserDefaults(); }); $scope.clearStandardForm = function () { $scope.firstName = ""; $scope.lastName = ""; $scope.firstLastNameChange(); $scope.standardForm.$setPristine(); $scope.standardForm.$setUntouched(); }; $scope.clearAdvancedForm = function() { $scope.multipleCriteria = [{}]; $scope.gridAdvOptions.data = []; $scope.advanced.$setPristine(); $scope.advanced.$setUntouched(); }; $scope.toggleDebugMode = function() { $scope.debugFlag = !$scope.debugFlag; console.log("Inside toggleDebugMode...: " + $scope.debugFlag); }; $scope.toggleJSONOutput = function() { $scope.enableJSON = !$scope.enableJSON; console.log("Inside toggleJSONOutput...: " + $scope.enableJSON); }; $scope.firstLastNameChange = function() { if ($scope.firstName || $scope.lastName) { $scope.isStandardRequired = false; } else { $scope.isStandardRequired = true; } } $scope.showAbout = function(ev) { console.log("Inside showAbout()..."); $mdDialog.show( $mdDialog.alert() .parent(angular.element(document.querySelector('#myContainer'))) .clickOutsideToClose(false) .title('About Employee Directory Demo') .textContent('This application will show you many of the features available in Angular Material, Spring Framework and MongoDB...') .ariaLabel('Employee Directory Demo') .ok('Ok') .openFrom('#about') .closeTo('#about') .targetEvent(ev) ); }; $scope.standardSearch = function (firstName, lastName) { var url = ""; console.log("Inside standardSearch()..."); console.log("firstName value...: " + firstName); console.log("lastName value....: " + lastName); $scope.gridActive = true; function onSuccess(response) { console.log("+++++standardSearch SUCCESS++++++"); if ($scope.debugFlag == 'true' || $scope.debugFlag) { console.log("Inside standardSearch response..." + JSON.stringify(response.data)); } else { console.log("Inside standardSearch response... (XML response is being skipped, debug=false)"); } if (response.data.success != false) { $scope.gridStdOptions.data = response.data; } else { $scope.gridStdOptions.data = []; } $scope.stopSpin('spinner-1'); }; function onError(response) { console.log("-------gridStdOptions FAILED-------"); $scope.stopSpin('spinner-1'); }; // Clear Grid Data $scope.gridStdOptions.data = []; $scope.startSpin('spinner-1'); var addl_params ='?firstName=' + firstName + '&lastName=' + lastName + '&etc=' + new Date().getTime(); //----MAKE AJAX REQUEST CALL to POST DATA---- ajaxService.postData(standardSearchUrl, 'POST', null, addl_params).then(onSuccess, onError); }; $scope.advancedSearch = function (criteriaSearch) { var url = ""; console.log("Inside advancedSearch()..."); console.log("multipleCriteria JSON...: " + JSON.stringify(criteriaSearch)); $scope.gridActive = true; function onSuccess(response) { console.log("+++++advancedSearch SUCCESS++++++"); if ($scope.debugFlag == 'true' || $scope.debugFlag) { console.log("Inside advancedSearch response..." + JSON.stringify(response.data)); } else { console.log("Inside advancedSearch response... (XML response is being skipped, debug=false)"); } if (response.data.success != false) { $scope.gridAdvOptions.data = response.data; } else { $scope.gridAdvOptions.data = []; } $scope.stopSpin('spinner-1'); }; function onError(response) { console.log("-------advancedSearch FAILED-------"); $scope.stopSpin('spinner-1'); }; // Clear Grid Data $scope.gridAdvOptions.data = []; $scope.startSpin('spinner-1'); var addl_params ='etc='+new Date().getTime(); //----MAKE AJAX REQUEST CALL to POST DATA---- ajaxService.postData(advancedSearchUrl, 'POST', criteriaSearch, '').then(onSuccess, onError); }; $scope.setDefaults = function(debugFlag, baseUrl) { standardSearchUrl = baseUrl + "/EmployeeDirectory/rest/standardSearch"; advancedSearchUrl = baseUrl + "/EmployeeDirectory/rest/advancedSearch"; $scope.debugFlag = debugFlag; console.log("Setting Defaults"); console.log("DebugFlag...............: " + $scope.debugFlag); } });
Cascading Style Sheets (styles.css)
.main_text { font-family: Arial Black; font-weight: bold; font-size: 25pt; color: #000 } .date_text { font-family: Arial; font-size: 8pt; font-weight: bold; color: #000 } .div_default { /* T R B L */ padding: 10px 0px 0px 10px; text-align: left; background: #FFF; } div.debug { padding: 10px 0px 10px 20px; color: white; background: #3c4dac; border-radius: 5px; -moz-border-radius: 10px; -webkit-border-radius: 10px; border: 1px solid #B0B0B0; } span.subtitle { color: #f7d763; /* font-size: 32px; */ font-weight: bold; position: absolute; margin: -10px 0px 0px 10px; } /* Extra small devices (phones, less than 768px) */ @media (max-width:767px) { span.subtitle {font-size: 24px; margin:-5px 0px 0px -35px;} img { max-width: 80%; height: auto;} } /* Extra Extra small devices (smaller phones, less than 400px) */ @media (max-width:400px) { span.subtitle {font-size: 18px; margin: 0px 0px 0px -34px;} img { max-width: 80%; height: auto;} } /* Small devices (tablets, 768px and up) */ @media (min-width:768px) { span.subtitle {font-size: 26px; margin:-5px 0px 0px -10px;} img { max-width: 90%; height: auto;} } /* Medium devices (desktops, 992px and up) */ @media (min-width:992px) { span.subtitle {font-size: 32px; margin:-10px 0px 0px 10px;} img { max-width: 100%; height: auto;} } /* Large devices (large desktops, 1200px and up) */ @media (min-width:1200px) { span.subtitle {font-size: 32px; margin:-10px 0px 0px 10px;} img { max-width: 100%; height: auto;} } div.subtitle { color: #f3d583; font-family: fantasy; font-size: 24px; padding-bottom: 10px; } .label_text { font-family: Arial; font-size: 12px; padding: 0px 0px 0px 0px; color: #000; } .subheading_text { font-family: Verdana; font-size: 14px; font-weight: bold; padding: 0px 0px 0px 0px; color: #000; } .toptable { padding: 0px 0px 0px 0px !important; } fieldset { border: none; margin: 0px; padding: 0px; background: #ff0; background-color: #f0f; } [layout-padding-minimal], [layout-padding-minimal] > [flex], [layout-padding-minimal] > [flex-gt-sm], [layout-padding-minimal] > [flex-md], [layout-padding-minimal] > [flex-lt-lg] { padding: 0px 8px 0px 8px; } .button_default { font-family: Arial; font-size: 12px; color: #FFF; padding: 5px 20px 5px 20px; border: none; background: #2589CE; } .button_default:hover { font-family: Arial; font-size: 12px; color: #FFF; padding: 5px 20px 5px 20px; border: none; background: #259AEE; } .button_default:active { font-family: Arial; font-size: 12px; color: #FFF; border: none; background: #256585; } .animateText { -webkit-animation-duration: 3s; -webkit-animation-delay: 3s; -webkit-animation-iteration-count: 1; } .datepickerCustom {} .datepickerCustom md-content { padding-bottom: 200px; } .datepickerCustom .validation-messages { font-size: 12px; color: #dd2c00; margin: 2px 0 0 25px; } input.inputBorderOnSuccess.md-input { border-bottom: 3px solid #5Cb801; padding-left: 20px; } span.input-group-addon.successCheckbox { position: absolute; margin-top: 2px; color: #5Cb801; } .md-button.md-fab.md-tiny { width: 36px; height: 36px; line-height: 20px; width: 30px; height: 20px; } span.label { margin-bottom: 10px; line-height: 36px; color: black; } md-select .selectBorderOnSuccess.md-select-value { border-bottom: 3px solid; }
MongoDB Employee Collection
{ "id" : "00001", "jobDesc" : "IT Development", "employeeType" : "permanent", "employeeStatus" : "active", "locationType" : "domestic", "titleDesc" : "Senior Developer", "altTitle" : "", "costCenter" : "1025", "workingShift" : 1, "firstName" : "Amaury", "preferredName" : "Amaury", "middle" : "", "lastName" : "Valdes", "fullName" : "Amaury Valdes", "country" : "USA", "companyName" : "Lark Productions", "hireDate" : "2012-05-18T04:00:00.0001Z", "isActive" : false } { "id" : "00002", "jobCode" : "IT Management", "employeeType" : "permanent", "employeeStatus" : "active", "locationType" : "domestic", "titleDesc" : "Senior Manager", "altTitle" : "", "costCenter" : "1025", "workingShift" : 1, "firstName" : "Steven", "preferredName" : "Steve", "middle" : "J", "lastName" : "Adelson", "fullName" : "Steven Adelson", "country" : "USA", "companyName" : "Lark Productions", "hireDate" : "2010-03-02T04:00:00.0001Z", "isActive" : true } { "id" : "00003", "jobDesc" : "Senior Management", "employeeType" : "permanent", "employeeStatus" : "active", "locationType" : "domestic", "titleDesc" : "Senior Group Manager", "altTitle" : "", "costCenter" : "1025", "workingShift" : 1, "firstName" : "Robert", "preferredName" : "Bob", "middle" : "", "lastName" : "Paterson", "fullName" : "Robert Paterson", "country" : "USA", "companyName" : "Lark Productions", "hireDate" : "2010-09-04T04:00:00.0001Z", "isActive" : true } { "id" : "00004", "jobDesc" : "Receptionist", "employeeType" : "temp", "employeeStatus" : "active", "locationType" : "domestic", "titleDesc" : "Front Desk Reception", "altTitle" : "", "costCenter" : "1025", "workingShift" : 1, "firstName" : "Sandra", "preferredName" : "Sandy", "middle" : "", "lastName" : "Jeffries", "fullName" : "Sandra Jeffries", "country" : "USA", "companyName" : "Kelly Temps", "hireDate" : "2008-12-23T04:00:00.0001Z", "isActive" : true } { "id" : "00005", "jobDesc" : "Developer", "employeeType" : "permanent", "employeeStatus" : "active", "locationType" : "domestic", "titleDesc" : "Front-End Developer", "altTitle" : "", "costCenter" : "982", "workingShift" : 1, "firstName" : "Christopher", "preferredName" : "Chris", "middle" : "", "lastName" : "Smith", "fullName" : "Christopher Smith", "country" : "USA", "companyName" : "Lark Productions", "hireDate" : "2010-05-02T04:00:00.0001Z", "isActive" : true } { "id" : "00006", "jobDesc" : "Developer", "employeeType" : "consultant", "employeeStatus" : "active", "locationType" : "domestic", "titleDesc" : "Front-End Developer", "altTitle" : "", "costCenter" : "982", "workingShift" : 1, "firstName" : "Christa", "preferredName" : "Chrissy", "middle" : "", "lastName" : "Barnes", "fullName" : "Christa Barnes", "country" : "USA", "companyName" : "Sapient Technologies", "hireDate" : "2012-07-13T04:00:00.0001Z", "isActive" : true } { "id" : "00007", "jobDesc" : "Developer", "employeeType" : "permanent", "employeeStatus" : "active", "locationType" : "domestic", "titleDesc" : "Java Developer", "altTitle" : "", "costCenter" : "960", "workingShift" : 1, "firstName" : "Christine", "preferredName" : "Christine", "middle" : "", "lastName" : "Verde", "fullName" : "Christine Verde", "country" : "USA", "companyName" : "Lark Productions", "hireDate" : "2006-03-15T04:00:00.0001Z", "isActive" : true }
Testing out the application using Web Services
In addition to using our AngularJS/Angular Material/Grid-UI web application to test out our restful services I used Postman which is a Google Chrome Application. Using this tool I validated each of the REST API calls. Please review the screen shots below:
Testing Application and POSTMAN Chrome Extension
Download the Complete Source Code
That’s It!
I hope you enjoyed this tutorial. It was certainly a lot of fun putting it together and testing it out. Please continue to share the love and like us so that we can continue bringing you quality tutorials. Happy Coding!!!
Related Spring Posts
- Creating Hello World Application using Spring MVC on Eclipse IDE
In this tutorial we will go into some detail on how to set up your Eclipse IDE environment so that you can develop Spring MVC projects. In this post, we will create our first Spring MVC project with the all to familiar “Hello World” sample program. - Spring MVC Form Handling Example
The following tutorial will guide you on writing a simple web based application which makes use of forms using Spring Web MVC framework. With this web application you will be able to interact with the customer entry form and enter all of the required values and submit them to the backend processes. I have taken the liberty of using CSS to beautify and transform the HTML page from a standard drab look and feel to a more appealing view. - Spring @RequestHeader Annotation ExampleIn this tutorial, we will discuss the different ways that Spring MVC allow us to access HTTP headers using annotation. We will discuss how to access individual header fields from the request object as well accessing all the headers by supplying Map and then iterating through the LinkedHashMap collection. We will also show you how to set the headers in the response object.
- Spring MVC Exception Handling using @ExceptionHandler with AngularJS GUIGood exception handling is a essential part of any well developed Application Framework and Spring MVC is no exception — pardon the pun. Spring MVC provides several different ways to handle exceptions in our applications. In this tutorial, we will cover Controller Based Exception Handling using the @ExceptionHandler annotation above the method that will handle it.
- Spring RESTful Web Service Example with JSON and Jackson using Spring Tool Suite
For this example, I will be using Spring Tool Suite (STS) as it is the best integrated development environment for building the Spring framework projects. Spring is today's leading framework for building Java, Enterprise Edition (Java EE) applications. One additional feature that makes Spring MVC so appealing is that it now also supports REST (REpresentational State Transfer) for build Web Services. - Spring MVC RESTful Web Service Example with Spring Data for MongoDB and ExtJS GUI
This post will show another example of how to build a RESTful web service using Spring MVC 4.0.6, Spring Data for MongoDB 1.6.1 so that we can integrate the web application with a highly efficient datastore (MongoDB 2.6). In this tutorial we will walk you through building the web service and NoSQL database backend and show you how to implement CRUD (Create, Read, Update and Delete) operations. - Building DHTMLX Grid Panel User Interface with Spring MVC Rest and MongoDB Backend
In this tutorial we will show how easy it is to use DHTMLX dhtmlxGrid component while loading JSON data with Ajax pulling in data from the Spring MVC REST web service from our MongoDB data source. You will see how simple it is to create a visually appealing experience for your client(s) with minimal javascript coding. - Spring MVC with JNDI Datasource for DB2 on AS/400 using Tomcat
In this tutorial we will discuss how to set up Spring MVC web services and configure a JNDI Datasource using Tomcat and connect to IBM DB2 Database on a AS/400. JNDI (Java Naming and Directory Interface) provides and interface to multiple naming and directory services. - Java Spring MVC Email Example using Apache Velocity
In this tutorial we will discuss how to set up a Java Spring MVC RESTful Webservice with Email using Apache Velocity to create a Velocity template that is used to create an HTML email message and embed an image, as shown below, using MIME Multipart Message. - Implementing Basic and Advanced Search using Angular Material Design, Grid-UI, Spring MVC REST API and MongoDB Example
In this tutorial we will discuss how to implement basic and advanced search techniques in MongoDB using AngularJS and Google’s Material Design with Spring MVC REST API backend. The advanced search user interface (UI) will use logical operators and build a JSON object which contains the search field name, boolean or logical operator and the search value. - Spring MVC Interceptor using HandlerInterceptorAdapter Example
In this tutorial we will discuss how to use the HandlerInterceptorAdapter abstract class to create a Spring MVC interceptor. These interceptors are used to apply some type of processing to the requests either before, after or after the complete request has finished executing.
Leave a Reply