[4/4] Docker: Front-end development w/ Java, SpringBoot MVC & RESTful Web API


Today we are going to talk about adding front end user interface to our application, from scratch. We can add the front end to our application using something called view resolvers. Our options are Apache Tiles, JavaServer Pages (JSP), etc. there are many other options, as well. Spring Boot supports FreeMarker templates, Groovy Templates and Themyleaf via “AutoConfiguration”, as the first class citizens. As the name suggests, we should not need to do a whole lot to get going with one of these. In this video we would be looking at Thymeleaf. I find it easy to use and feature rich at the same time. Thymeleaf is mostly HTML. Finally, we will talk about Web JARs & How to add Branding to our web application, using responsive web design.

Now, the way I see it, we have 3 options to add front-end to our web app from architecture perspective:

  1. Pure MVC application
  2. RESTful Web API driven client side MV* single page app
  3. Hybrid MVC and Web API application.

Each approach has its pros and cons so please do take your time before you make your choice, or … ask your questions in the comment section down below. In this demo, I’d go for option C, to cover both. Nevertheless, in our previous episode we saw how to create an API controller.

Now, by default, SpringBoot looks for the views to be located in resources/templates directory. Let’s start with MVC controller. I prefer to keep my MVC and API controllers in separate packages, and I prefer to prefix my API URI paths with ‘api’ – this, also, is my personal taste.

I recommend referring to your stylsheets in the head section and JavaScript libraries towards the bottom, after your body tag ends. Doing so, your browser applies CSS styles faster and downloads your JavaScript libraries later the page is rendered. This makes your web app load much quicker. You may want to dabble with this idea.

Code:

SpringBoot REST API controller

package edu.gatech.epidemics.api;
import edu.gatech.epidemics.entities.Person;
import edu.gatech.epidemics.service.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author atalati
*/
@RestController
public class PersonApiController {
@Autowired
Environment environment;
@Autowired
private PersonService personService;
@GetMapping(value = "/api/persons")
public List<Person> get() {
return personService.findAll();
}
@GetMapping(value = "/api/persons/{id}")
public Person get(@PathVariable int id) {
Person person;
if (id == 0) {
person = new Person();
} else {
person = personService.findById(id).get();
}
return person;
}
@RequestMapping(value = "/api/persons", params = "username")
public List<Person> findByUsername(@RequestParam("username") String username) {
return personService.findByUsername(username);
}
@PostMapping(path = "/api/persons", consumes = "application/json", produces = "application/json")
public Person addPerson(@RequestBody Person person) {
return personService.add(person);
}
@DeleteMapping(path = "/api/persons/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity deletePerson(@PathVariable int id){
try {
personService.deleteById(id);
return new ResponseEntity(HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

SpringBoot MVC controller

package edu.gatech.epidemics.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author atalati
*/
@Controller
public class PersonController {
@GetMapping(value = {"/", "/person/index"})
public String index(){
System.out.println("List all people");
return "person/index";
}
}

Thymeleaf MVC fragments master template

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head lang="en" th:fragment="head">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html"; charset="UTF-8">
<link rel="stylesheet" href="/webjars/bootswatch/3.3.7/flatly/bootstrap.min.css">
<link rel="stylesheet" href="/webjars/toastr/2.1.0/toastr.min.css">
</head>
<body>
<div class="container">
<div th:fragment="header">
<!--Bootstrap navigation top menu starts-->
<!--Bootstrap navigation menu begins-->
<nav class="navbar navbar-default unselectable">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1"
aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Epidemics</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active">
<a href="/person/list">Patients
<span class="sr-only">(current)</span>
</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li>
<a href="/help/index">Help</a>
</li>
</ul>
</div>
<!-- /.navbar-collapse -->
</div>
<!-- /.container-fluid -->
</nav>
<!--Bootstrap navigation top menu ends-->
</div>
</div>
<div th:fragment="scripts">
<script type="text/javascript" src="/webjars/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript" src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/webjars/toastr/2.1.0/toastr.min.js"></script>
<script type="text/javascript" src="/webjars/jquery-validation/1.17.0/dist/jquery.validate.min.js"></script>
<script type="text/javascript" src="/webjars/jquery-validation/1.17.0/dist/additional-methods.min.js"></script>
</div>
</body>
</html>
view raw header.html hosted with ❤ by GitHub

Thymeleaf MVC Index view template, using fragments

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>All patients</title>
<!--/*/ <th:block th:include="fragments/header :: head"></th:block> /*/-->
</head>
<body>
<!--/*/ <th:block th:include="fragments/header :: header"></th:block> /*/-->
<div class="table-responsive">
<table id="tblPatient" class="table table-hover table-bordered table-striped">
<thead>
<tr>
<th style="display:table-cell; width: 3%;"></th>
<th>
<h4 class="text-center">Name
<button class="btn btn-xs btn-primary" title="Toggle filter" onclick="toggleFilter()"><span
class="glyphicon glyphicon-filter" aria-hidden="true"></span></button>
</h4>
</th>
<th colspan="3"><h4 class="text-center">Actions</h4></th>
</tr>
</thead>
<tbody id="tbodyPatient">
<!--filter row-->
<tr>
<td colspan="5"><input type="text" name="patientNameFilter" id="tbpatientNameFilter"
class="form-control">
</td>
</tr>
<!--Show this row when site is loading the data, else hide it-->
<tr id="loadingRecords" class="loadingRecords" style="display: none;">
<td colspan="5">
<div class="row text-center">
<i class="fa-spin glyphicon glyphicon-refresh"></i>Loading data ...
</div>
</td>
</tr>
<!--Show this row when there are no records to display, else hide it-->
<tr id="noRecords" class="noRecords" style="display: none;">
<td colspan="5">
<div class="row text-center">
<span class="glyphicon glyphicon-info-sign"></span>&nbsp;Nothing to show here.
</div>
</td>
</tr>
<!--insert patients here-->
</tbody>
<tfoot>
<tr>
<td colspan="5">
<div>
<button id="btnaddPatient" onClick="redirect('upsert',-1 )"><span
class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Patient
</button>
<button id="btnImportPatient" onclick="redirect('import', -1)"><span
class="glyphicon glyphicon-save" aria-hidden="true"></span>
Import Patient from FHIR
</button>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
<!--/*/ <th:block th:include="fragments/header :: scripts"></th:block> /*/-->
</body>
<script type="text/javascript">
var patients = [];
var deletePatientId = undefined;
$(function () {
$('#loadingRecords').show();
$('#tblPatient tbody tr:first-child').hide();
loadPatients();
});
var toggleFilter = function () {
$('#tbpatientNameFilter').val('');
$('#tblPatient tbody tr:first-child').toggle();
if ($('#tblPatient tbody tr:first-child').css('display') === 'none') {
showPatients(patients);
}
};
$('#tbpatientNameFilter').keyup(function () {
var filterText = $(this).val();
if (filterText.length < 3) {
return;
}
$('#tblPatient').find('tr:gt(2):lt(-1)').remove();
var filterdRecords = [];
$.each(patients, function (key, val) {
var matchesFirstName = val.firstName.toLowerCase().indexOf(filterText.toLowerCase()) >= 0;
var matchesLastName = val.lastName.toLowerCase().indexOf(filterText.toLowerCase()) >= 0;
if (matchesFirstName || matchesLastName) {
filterdRecords.push(val);
}
});
showPatients(filterdRecords);
toastr.info('To clear filter, click again.');
});
var showPatients = function (records) {
$('#tblPatient').find('tr:gt(2):lt(-1)').remove();
$.each(records, function (key, val) {
var row = getPatientRowHtml(val);
$('#tbodyPatient').append(row);
});
};
var loadPatients = function () {
var url = '/api/persons/';
$.get(url, function () {
}).done(function (data) {
$('#loadingRecords').hide();
if (data.length === 0) {
$('#noRecords').show();
}
else {
patients = data;
$('#noRecords').hide();
showPatients(patients);
}
toastr.success('Patients loaded.');
}).fail(function () {
toastr.error('Can\'t load patients.');
});
};
var togglePatientVisits = function (patientId) {
var iconSpanElement = $('#btnChildTable' + patientId);
iconSpanElement.toggleClass('glyphicon-triangle-bottom glyphicon-triangle-right');
$('#visitsForPatient' + patientId).toggle();
};
var getPatientRowHtml = function (patient) {
var childTableRowHtml = '';
if (patient.visits && patient.visits.length > 0) {
var validVisits = patient.visits.filter(visitsWithAssessment);
if (validVisits && validVisits.length > 0) {
childTableRowHtml =
"<tr id='visitsForPatient" + patient.id.toString() + "' style='display: none;'>" +
" <td colspan=\"5\">" +
" <table class=\"table table-hover table-bordered table-striped\">" +
" <thead>" +
" <tr>" +
" <th>Visit #</th>" +
" <th>Date</th>" +
" <th>Impairment</th>" +
" <th>Risk</th>" +
" <th>Details</th>" +
" </tr>" +
" </thead>" +
" <tbody>";
validVisits.forEach(function (visit) {
var visitDate = (new Date(visit.visitDate)).toLocaleString();
childTableRowHtml +=
" <a>" +
" <td>" + visit.id + "</td>" +
" <td>" + visitDate + "</td>" +
" <td>" + visit.assessments[0].assessmentLevel + "</td>" +
" <td>" + visit.assessments[1].assessmentLevel + "</td>" +
" <td><a href='#' class='text-primary' onclick=\"redirect('visit', " + visit.id + ")\">view</a></td>" +
" </tr>";
});
childTableRowHtml +=
" </tbody>" +
" <tfoot></tfoot>" +
" </table>" +
" </td>" +
"</tr>";
}
}
function visitsWithAssessment(v) {
return v.assessments.length === 2;
}
var cheveronButtonHtml = '';
var start = "redirect('start','" + patient.id.toString() + "')";
var edit = "redirect('upsert','" + patient.id.toString() + "')";
var editButtonHtml = '<button class="personEditButton" onClick="' + edit + '"><span class="glyphicon glyphicon-edit" aria-hidden="true"></span> Edit</button>';
var deleteConfirmButtonHtml = '<button type=\"button\" data-toggle=\"modal\" data-target=\".patient-delete-confirm\" data-id="' + patient.id.toString();
deleteConfirmButtonHtml += '"><span class=\"glyphicon glyphicon-trash\" aria-hidden=\"true\"></span> Delete</button>';
if (childTableRowHtml && childTableRowHtml.length > 0) {
cheveronButtonHtml = '<button type="button" onClick="togglePatientVisits(' + patient.id.toString() + ')"><span id="btnChildTable' + patient.id.toString() + '" class="glyphicon glyphicon-triangle-right"></span></button>';
} else {
// cheveronButtonHtml = '<button type="button"><span class="glyphicon glyphicon-triangle-right" disabled=""></span></button>';
cheveronButtonHtml = '';
}
var del = "";
var rowHtml = '';
rowHtml += '<tr><td>';
rowHtml += cheveronButtonHtml;
rowHtml += '</td><td>';
rowHtml += (patient.firstName + ' ' + patient.lastName);
rowHtml += '</td><td>';
rowHtml += editButtonHtml;
rowHtml += '</td><td>';
rowHtml += deleteConfirmButtonHtml;
rowHtml += '</td><td>';
rowHtml += '<button class="personQuestionnaireButton" onClick="' + start + '">Start Assessment</button>';
rowHtml += '</td></tr>';
if (childTableRowHtml && childTableRowHtml.length > 0) {
rowHtml += childTableRowHtml;
}
return rowHtml;
};
</script>
</html>
view raw index.html hosted with ❤ by GitHub

2 Comments

  1. We have an integration project for new wireless iot hybrid with Sprint. We seeking help on APIs to connect building automation systems to our new technology. DZ

    Like

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s