Implementing the review creation web page

Thus far, we have created views for user registration and login, as well as a homepage for logged in users to peruse reviews posted on the platform. We must now work on the view that facilitates the creation of these reviews. As always, first, before creating the view, let us work on an action that will be in charge of rendering our to-be-developed view to users. Open up the ApplicationController class and add the following method to it:

@GetMapping("/create-review")
fun createReview(model: Model, principal: Principal): String {
model.addAttribute("principal", principal)
return "create-review"
}

The createReview() action handles HTTP GET requests to the /create-review request path by returning a create-review.html template to the client for rendering. Go ahead and add a create-review.html file to the project template directory. 

Similar to what we did before, let's begin by adding external styles and scripts to create-review.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>New review</title>
<!-- Addition of external stylesheets -->
<link rel="stylesheet" th:href="@{/css/app.css}"/>
<link rel="stylesheet" href="/webjars/bootstrap/4.0.0-beta.3
/css/bootstrap.min.css"
/>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com
/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<link href="https://fastcdn.org/Buttons/2.0.0/css/buttons.css"
rel="stylesheet">

<!-- Inclusion of external Javascript -->
<script src="/webjars/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js
/1.12.6/umd/popper.min.js"
></script>
<script src="/webjars/bootstrap/4.0.0-beta.3/
js/bootstrap.min.js"
></script>
<script src="https://fastcdn.org/Buttons/2.0.0/js/buttons.js"></script>
<script type="text/javascript" src="https://maps.googleapis.com/
maps/api/js?key={{API_KEY}}&libraries=places"
>
</script>

Now, add our required internal stylesheet for the webpage:

    <!-- Definition of internal styles -->
<style>
#map {
height: 400px;
}

#container-place-data {
height: 0;
visibility: hidden;
}

#container-place-info {
font-size: 14px;
}

#container-selection-status {
visibility: hidden;
}
</style>
</head>

The next thing on our agenda is the creation of the necessary form for the input of review data. Continue the create-review.html template with the following code: 

<body>
<div th:insert="fragments/navbar :: navbar"> </div>
<div class="container-fluid">
<div class="row">
<div class="col-sm-12 col-xs-12">
<!-- Review form creation -->
<form class="form-group col-sm-12 form-vertical form-app"
id="form-login" method="post" th:action="@{/reviews}">
<div class="col-sm-12 mt-2 lead">Write your review</div>
<div th:if="${error != null}" class="text-danger"
th:text="${error}"> </div>
<hr>
<input class="form-control" type="text" name="title"
placeholder="Title" th:value="${title}" required/>
<textarea class="form-control mt-4" rows="13" name="body"
placeholder="Review" th:value="${body}" required></textarea>
<div class="form-group" id="container-place-data">
<!-- Input fields for location specific form data -->
<!-- Form input data for the fields below are
provided by the Google Places API -->
<input class="form-control" id="place_address"
th:value="${placeAddress}" type="text" name="placeAddress"
required/>
<input class="form-control" id="place_name" type="text"
name="placeName" th:value="${placeName}" required/>
<input class="form-control" id="place_id" type="text"
name="placeId" th:value="${placeId}" required/>
<input id="location-lat" type="number" name="latitude"
step="any" th:value="${latitude}" required/>
<input id="location-lng" type="number" name="longitude"
step="any" th:value="${longitude}" required/>
</div>
<div class="form-group mb-3">
<button class="button button-pill" type="button"
data-toggle="modal" data-target="#mapModal">
<i class="fa fa-map-marker" aria-hidden="true"></i>
Select Location
</button>
<button class="button button-pill button-primary">
Submit Review</button>
</div>
<div class="text-success ml-2" id="container-selection-status">
Location selected</div>
</form>
</div>
</div>

Now, let us add a modal that will enable the user to select the review location from a map. Do not worry too much about the details of the selection process as of now. We shall talk more on it shortly:

      <!-- Map Modal -->
<div class="modal fade" id="mapModal">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Select place to review</h5>
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container-fluid">
<div id="map"> </div>
<div class="row mt-2" id="container-place-info">
<div class="col-sm-12" id="container-place-name">
<b>
Place Name:</b>
</div>
<div class="col-sm-12" id="container-place-address">
<b>
Place Address:</b>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary"
data-dismiss="modal">Done</button>
</div>
</div>
</div>
</div>
</div>

And lastly, we finish up the template by including its internal JavaScript, as shown in the code snippets that follow: 

      <script>
// form field reference creation
var formattedAddressField = document
.getElementById('place_address');
var placeNameField = document.getElementById('place_name');
var placeIdField = document.getElementById('place_id');
var latitudeField = document.getElementById('location-lat');
var longitudeField = document.getElementById('location-lng');

// container reference creation
var containerPlaceName = document.getElementById
('container-place-name');
var containerPlaceAddress = document.getElementById
('container-place-address');
var containerSelectionStatus = document.getElementById
('container-selection-status');

In the preceding code snippet, we created references to important DOM elements that exist on the page. These references include references to place specific input fields (the fields for the address, name, ID, and latitudinal and longitudinal coordinates of a place). In addition, we added references for the containers displaying the details of a selected place, such as the place name and place address. At this juncture, we will declare a few functions. These functions are initialize(), getPlaceDetailsById(), updateViewData(), setFormValues(), showSelectionsStatusContainer(), and setContainerText().

Start by adding the initialize() and getPlaceDetailsById() functions shown here to the template:

        //invoked to initialize Google map
function initialize() {

navigator.geolocation.getCurrentPosition(function(location) {
var latitude = location.coords.latitude;
var longitude = location.coords.longitude;

var center = new google.maps.LatLng(latitude, longitude);

var map = new google.maps.Map(document.getElementById('map'), {
center: center,
zoom: 15,
scrollwheel: false
});

var service = new google.maps.places.PlacesService(map);

map.addListener('click', function(data) {
getPlaceDetailsById(service, data.placeId);
});
});

}

We have added the function below to enable us to get the details of a particular place from the Google Places API Invoked to retrieve the details of a place:


function getPlaceDetailsById(service, placeId) {
var request = {
placeId: placeId
};

service.getDetails(request, function (place, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
updateViewData(place)
}
});
}

Now, add updateView() and setFormValues() as shown here:

        //Invoked to update view information
function updateViewData(place) {
setFormValues(
place.formatted_address,
place.name,
place.place_id,
place.geometry.location.lat(),
place.geometry.location.lng()
);

setContainerText('<b>Place Name: </b>' + place.name,
'<b>Place Address: </b>' + place.formatted_address);

showSelectionStatusContainer();
}

The function below is called to update view form data:


function setFormValues(formattedAddress, placeName, placeId,
latitude, longitude) {
formattedAddressField.value = formattedAddress;
placeNameField.value = placeName;
placeIdField.value = placeId;
latitudeField.value = latitude;
longitudeField.value = longitude;
}

Finally, finish up the template by adding the code shown here:

        function showSelectionStatusContainer() {
containerSelectionStatus.style.visibility = 'visible'
}

function setContainerText(placeNameText, placeAddressText) {
containerPlaceName.innerHTML = placeNameText;
containerPlaceAddress.innerHTML = placeAddressText;
}

// Initializes map upon window load completion
google.maps.event.addDomListener(window, 'load', initialize);
</script>
</body>
</html>

Similar to the previous template, we begin create-review.html  with the addition of both external and internal CSS and JavaScript required by the template in the HTML <head> tag. Further into the template, we create a form that takes the following form data as its input:

  • title: A user defined title for the review being created.
  • body: The body of the review. This is the main review text.
  • placeAddress: The address of the place being reviewed.
  • placeName: The name of the place being reviewed.
  • placeId: The unique ID of the location being reviewed.
  • latitude: The latitudinal coordinate of the reviewed location.
  • longitude: The longitudinal coordinate of the reviewed location.

There will be no need for the user to provide form input for placeAddress, placeName, placeId, latitude, and longitude. As such, we have hidden the parent <div> of the aforementioned input elements. We shall utilize the Google Places API to retrieve place-specific information. Make sure to note that in the template we are using a modal to display a map for location selection. The modal is toggled by a button we added in our template as follows:

<button class="button button-pill" type="button" data-toggle="modal" data-target="#mapModal">
<i class="fa fa-map-marker" aria-hidden="true"></i> Select Location
</button>

Clicking on the button will display the map modal to the user. Upon rendering the map, a user can click on their desired review location from the map. Performing such a click action will trigger the map's click event, which in turn will be handled by the listener we defined in the template as follows:

map.addListener('click', function(data) {
getPlaceDetailsById(service, data.placeId);
});

getPlacesDetailsById() takes two arguments: an instance of google.maps.places.PlacesService and the ID of the place whose information is to be retrieved. The PlacesService instance is then used to retrieve the information of the place. After this information retrieval, the view is duly updated with the information retrieved: place-specific form data is set, the place name and address container within the map modal is updated, and a message indicating that a location has successfully been selected is shown to the user. Upon selection of a location and the input of all required form data, the user can then submit their review.

We are almost ready to try out the review creation page. Before we do, we must create a review validator, as well as a controller action that handles POST requests sent to the /reviews path. Let's start with ReviewValidator. Add the ReviewValidator class shown here to com.example.placereviewer.component:

package com.example.placereviewer.component

import com.example.placereviewer.data.model.Review
import org.springframework.stereotype.Component
import org.springframework.validation.Errors
import org.springframework.validation.ValidationUtils
import org.springframework.validation.Validator

@Component
class ReviewValidator: Validator {

override fun supports(aClass: Class<*>?): Boolean {
return Review::class == aClass
}

override fun validate(obj: Any?, errors: Errors) {
val review = obj as Review

ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title",
"Empty.reviewForm.title", "Title cannot be empty")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "body",
"Empty.reviewForm.body", "Body cannot be empty")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "placeName",
"Empty.reviewForm.placeName")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "placeAddress",
"Empty.reviewForm.placeAddress")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "placeId",
"Empty.reviewForm.placeId")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "latitude",
"Empty.reviewForm.latitude")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "longitude",
"Empty.reviewForm.longitude")

if (review.title.length < 5) {
errors.rejectValue("title", "Length.reviewForm.title",
"Title must be at least 5 characters long")
}

if (review.body.length < 5) {
errors.rejectValue("body", "Length.reviewForm.body",
"Body must be at least 5 characters long")
}
}
}

As we have previously explained the workings of custom validators, there is little need to explain how this validator works. Without taking time, let us implement a controller class for HTTP requests pertaining to reviews. Create a ReviewController class in com.example.placereviewer.controller and add the following code to it:

package com.example.placereviewer.controller

import com.example.placereviewer.component.ReviewValidator
import com.example.placereviewer.data.model.Review
import com.example.placereviewer.service.ReviewService
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.validation.BindingResult
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import javax.servlet.http.HttpServletRequest

@Controller
@RequestMapping("/reviews")
class ReviewController(val reviewValidator: ReviewValidator,
val
reviewService: ReviewService) {

@PostMapping
fun create(@ModelAttribute reviewForm: Review, bindingResult: BindingResult,
model: Model, request: HttpServletRequest): String {
reviewValidator.validate(reviewForm, bindingResult)

if (!bindingResult.hasErrors()) {
val res = reviewService.createReview(request.userPrincipal.name,
reviewForm)

if (res) {
return "redirect:/home"
}
}

with (model) {
addAttribute("error", bindingResult.allErrors.first().defaultMessage)
addAttribute("title", reviewForm.title)
addAttribute("body", reviewForm.body)
addAttribute("placeName", reviewForm.placeName)
addAttribute("placeAddress", reviewForm.placeAddress)
addAttribute("placeId", reviewForm.placeId)
addAttribute("longitude", reviewForm.longitude)
addAttribute("latitude", reviewForm.latitude)
}

return "create-review"
}
}

Having added the ReviewValidator and ReviewController classes, build and run the project, log in as a user, and navigate to http://localhost:5000/create-review from your favorite browser.

Upon page load, you will be presented with a form you can use to add a new review:

Users are required to select a review location before a review can be submitted. To select a review location, click the Select Location button:

Clicking the Select Location button will present the user with a modal containing a map from which they can select a location of choice to review. Clicking on a location from the map will bring up an information window on the map containing data pertaining to the clicked location. In addition, the modal container for holding the selected place name and address will be updated:

After a user selects their review location of choice, they can close the modal by clicking Done and proceed to filling in the title and body of their review:

Notice that the review form now indicates that a review location has been selected successfully. Once the user fills in all the necessary review information, they can proceed to submit the review by clicking Submit Review:

After review submission, the user is redirected to their home page, from which they can now see the review submitted. Clicking on the View location button of any review displayed on the home page will render a modal containing a map displaying the exact location reviewed:

The map displayed to the user possesses a marker indicating the exact location reviewed by the reviewer.

At this stage, we have concluded all the core functionality of the Place Reviewer application. Before we wrap up this chapter, let us explore how to test Spring applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.110.155