Get ahead
VMware offers training and certification to turbo-charge your progress.
Learn moreTo see updates to this code, visit our React.js and Spring Data REST tutorial. |
In the previous session, you found out how to stand up a backend payroll service to store employee data using Spring Data REST. A key feature it lacked was using the hypermedia controls and navigation by links. Instead, it hard coded the path to find data.
Feel free to grab the code from this repository and follow along. This session is based on the previous session’s app with extra things added.
I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC….What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?
So, what exactly ARE hypermedia controls, i.e. hypertext, and how can you use them? To find out, let’s take a step back and look at the core mission of REST.
The concept of REST was to borrow ideas that made the web so successful and apply them to APIs. Despite the web’s vast size, dynamic nature, and low rate that clients, i.e. browsers, are updated, the web is an amazing success. Roy Fielding sought to use some of its constraints and features and see if that would afford similar expansion of API production and consumption.
One of the constraints is to limit the number of verbs. For REST, the primary ones are GET, POST, PUT, DELETE, and PATCH. There are others, but we won’t get into them here.
These are standardized HTTP verbs with well written specs. By picking up and using already coined HTTP operations, we don’t have to invent a new language and educate the industry.
Another constraint of REST is to use media types to define the format of data. Instead of everyone writing their own dialect for the exchange of information, it would be prudent to develop some media types. One of the most popular ones to be accepted is HAL, media type application/hal+json. It is Spring Data REST’s default media type. A keen value is that there is no centralized, single media type for REST. Instead, people can develop media types and plug them in. Try them out. As different needs become available, the industry can flexibly move.
A key feature of REST is to include links to relevant resources. For example, if you were looking at an order, a RESTful API would include a link to the related customer, links to the catalog of items, and perhaps a link to the store from which the order was placed. In this session, you will introduce paging, and see how to also use navigational paging links.
To get underway with using frontend hypermedia controls, you need to turn on some extra controls. Spring Data REST provides paging support. To use it, just tweak the repository definition:
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
Your interface now extends PagingAndSortingRepository
which adds extra options to set page size, and also adds navigational links to hop from page to page. The rest of the backend is the same (exception for some extra pre-loaded data to make things interesting).
Restart the application (./mvnw spring-boot:run
) and see how it works.
$ curl localhost:8080/api/employees?size=2 { "_links" : { "first" : { "href" : "http://localhost:8080/api/employees?page=0&size=2" }, "self" : { "href" : "http://localhost:8080/api/employees" }, "next" : { "href" : "http://localhost:8080/api/employees?page=1&size=2" }, "last" : { "href" : "http://localhost:8080/api/employees?page=2&size=2" } }, "_embedded" : { "employees" : [ { "firstName" : "Frodo", "lastName" : "Baggins", "description" : "ring bearer", "_links" : { "self" : { "href" : "http://localhost:8080/api/employees/1" } } }, { "firstName" : "Bilbo", "lastName" : "Baggins", "description" : "burglar", "_links" : { "self" : { "href" : "http://localhost:8080/api/employees/2" } } } ] }, "page" : { "size" : 2, "totalElements" : 6, "totalPages" : 3, "number" : 0 } }
The default page size is 20, so to see it in action, ?size=2
applied. As expected, only two employees are listed. In addition, there is also a first, next, and last link. There is also the self link, free of context including page parameters.
If you navigate to the next link, you’ll then see a prev link as well:
$ curl "http://localhost:8080/api/employees?page=1&size=2" { "_links" : { "first" : { "href" : "http://localhost:8080/api/employees?page=0&size=2" }, "prev" : { "href" : "http://localhost:8080/api/employees?page=0&size=2" }, "self" : { "href" : "http://localhost:8080/api/employees" }, "next" : { "href" : "http://localhost:8080/api/employees?page=2&size=2" }, "last" : { "href" : "http://localhost:8080/api/employees?page=2&size=2" } }, ...
Note
|
When using "&" in URL query parameters, the command line thinks it’s a line break. Wrap the whole URL with quotation marks to bypass that. |
That looks neat, but it will be even better when you update the frontend to take advantage of that.
That’s it! No more changes are needed on the backend to start using the hypermedia controls Spring Data REST provides out of the box. You can switch to working on the frontend. (That’s part of the beauty of Spring Data REST. No messy controller updates!)
Note
|
It’s important to point out, this application isn’t "Spring Data REST-specific." Instead, it uses HAL, URI Templates, and other standards. That’s how using rest.js is a snap: that library comes with HAL support. |
In the previous session, you hardcoded the path to /api/employees
. Instead, the ONLY path you should hardcode is the root.
...
var root = '/api';
...
With a handy little follow()
function, you can now start from the root and navigate to where you need!
componentDidMount: function () {
this.loadFromServer(this.state.pageSize);
},
In the previous session, the loading was done directly inside componentDidMount()
. In this session, we are making it possible to reload the entire list of employees when the page size is updated. To do so, we have moved things into loadFromServer()
.
loadFromServer: function (pageSize) {
follow(client, root, [
{rel: 'employees', params: {size: pageSize}}]
).then(employeeCollection => {
return client({
method: 'GET',
path: employeeCollection.entity._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).then(schema => {
this.schema = schema.entity;
return employeeCollection;
});
}).done(employeeCollection => {
this.setState({
employees: employeeCollection.entity._embedded.employees,
attributes: Object.keys(this.schema.properties),
pageSize: pageSize,
links: employeeCollection.entity._links});
});
},
loadFromServer
is very similar the previous session, but instead if uses follow()
:
client
object used to make REST calls.
The array of relationships can be as simple as ["employees"]
, meaning when the first call is made, look in _links for the relationship (or rel) named employees. Find its href and navigate to it. If there is another relationship in the array, rinse and repeat.
Sometimes, a rel by itself isn’t enough. In this fragment of code, it also plugs in a query parameter of ?size=<pageSize>. There are other options that can be supplied, as you’ll see further along.
After navigating to employees with the size-based query, the employeeCollection is at your fingertips. In the previous session, we called it day and displayed that data inside <EmployeeList />
. Today, you are performing another call to grab some JSON Schema metadata found a /api/profile/employees
.
You can see the data yourself:
$ curl http://localhost:8080/api/profile/employees -H 'Accept:application/schema+json' { "title" : "Employee", "properties" : { "firstName" : { "title" : "First name", "readOnly" : false, "type" : "string" }, "lastName" : { "title" : "Last name", "readOnly" : false, "type" : "string" }, "description" : { "title" : "Description", "readOnly" : false, "type" : "string" } }, "definitions" : { }, "type" : "object", "$schema" : "http://json-schema.org/draft-04/schema#" }
Note
|
The default form of metadata at /profile/employees is ALPS. In this case, though, you are using content negotation to fetch JSON Schema. |
By capturing this information in the`<App />` component’s state, you can make good use of it later on when building input forms.
Equipped with this metadata, you can now add some extra controls to the UI. Create a new React component, <CreateDialog />
.
var CreateDialog = React.createClass({
handleSubmit: function (e) {
e.preventDefault();
var newEmployee = {};
this.props.attributes.forEach(attribute => {
newEmployee[attribute] = React.findDOMNode(this.refs[attribute]).value.trim();
});
this.props.onCreate(newEmployee);
// clear out the dialog's inputs
this.props.attributes.forEach(attribute => {
React.findDOMNode(this.refs[attribute]).value = '';
});
// Navigate away from the dialog to hide it.
window.location = "#";
},
render: function () {
var inputs = this.props.attributes.map(attribute =>
<p key={attribute}>
<input type="text" placeholder={attribute} ref={attribute} className="field" />
</p>
);
return (
<div>
<a href="#createEmployee">Create</a>
<div id="createEmployee" className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>
<h2>Create new employee</h2>
<form>
{inputs}
<button onClick={this.handleSubmit}>Create</button>
</form>
</div>
</div>
</div>
)
}
});
This new component has both a handleSubmit()
function as well as the expected render()
function.
Let’s dig into these functions in reverse order, and first look at the render()
function.
Your code maps over the JSON Schema data found in the attributes property and converts it into an array of <p><input></p>
elements.
This represents the dynamic nature of the component, driven by loading data from the server.
Inside this component’s top-level <div>
is an anchor tag and another <div>
. The anchor tag is the button to open the dialog. And the nested <div>
is the hidden dialog itself. In this example, you are use pure HTML5 and CSS3. No JavaScript at all! You can see the CSS code used to show/hide the dialog. We won’t dive into that here.
Nestled inside <div id="createEmployee">
is a form where your dynamic list of input fields are injected followed by the Create button. That button has an onClick={this.handleSubmit}
event handler. This is the React way of registering an event handler.
Note
|
React doesn’t create a fistful of event handlers on every DOM element. Instead, it has a much more performant and sophisticated solution. The point being you don’t have to manage that infrastructure and can instead focus on writing functional code. |
The handleSubmit()
function first stops the event from bubbling further up the hierarchy. It then uses the same JSON Schema attribute property to find each <input>
using React.findDOMNode(this.refs[attribute])
.
this.refs
is a way to reach out and grab a particular React component by name. In that sense, you are ONLY getting the virtual DOM component. To grab the actual DOM element you need to use React.findDOMNode()
.
After iterating over every input and building up the newEmployee
object, we invoke a callback to onCreate()
the new employee. This function is up top inside App.onCreate
and was provided to this React component as another property. Look at how that top-level function operates:
onCreate: function (newEmployee) {
follow(client, root, ['employees']).then(employeeCollection => {
return client({
method: 'POST',
path: employeeCollection.entity._links.self.href,
entity: newEmployee,
headers: {'Content-Type': 'application/json'}
})
}).then(response => {
return follow(client, root, [
{rel: 'employees', params: {'size': this.state.pageSize}}]);
}).done(response => {
this.onNavigate(response.entity._links.last.href);
});
},
Once again, use the follow()
function to navigate to the employees resource where POST operations are performed. In this case, there was no need to apply any parameters, so the string-based array of rels is fine. In this situation, the POST call is returned. This allows the next then()
clause to handle processing the outcome of the POST.
New records are typically added to the end of the dataset. Since you are looking at a certain page, it’s logical to expect the new employee record to not be on the current page. To handle this, you need to fetch a new batch of data with the same page size applied. That promise is returned for the final clause inside done()
.
Since the user probably wants to see the newly created employee, you can then use the hypermedia controls and navigate to the last entry.
This introduces the concept of paging in our UI. Let’s tackle that next!
First time using a promise-based API? Promises are a way to kick of asynchronous operations and then register a function to respond when the task is done. Promises are designed to be chained together to avoid "callback hell". Look at the following flow:
when.promise(async_func_call())
.then(function(results) {
/* process the outcome of async_func_call */
})
.then(function(more_results) {
/* process the previous then() return value */
})
.done(function(yet_more) {
/* process the previous then() and wrap things up */
});
For more details, check out this tutorial on promises.
The secret thing to remember with promises is that then()
functions need to return something, whether it’s a value or another promise. done()
functions do NOT return anything, and you don’t chain anything after it. In case you haven’t noticed yet, client
(which is an instance of rest
from rest.js) as well as the follow
function return promises.
You set up paging on the backend and have already starting taking advantage of it when creating new employees.
In the previous section, you used the page controls to jump to the last page. It would be really handy to dynamically apply it to the UI and let the user navigate as desired. Adjusting the controls dynamically based on available navigation links would be great.
First, let’s check out the onNavigate()
function you used.
onNavigate: function(navUri) {
client({method: 'GET', path: navUri}).done(employeeCollection => {
this.setState({
employees: employeeCollection.entity._embedded.employees,
attributes: this.state.attributes,
pageSize: this.state.pageSize,
links: employeeCollection.entity._links
});
});
},
This is defined at the top, inside App.onNavigate
. Again, this is to allow managing the state of the UI in the top component. After passing onNavigate()
down to the <EmployeeList />
React component, the following handlers are coded up to handle clicking on some buttons:
handleNavFirst: function(e){
e.preventDefault();
this.props.onNavigate(this.props.links.first.href);
},
handleNavPrev: function(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.prev.href);
},
handleNavNext: function(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.next.href);
},
handleNavLast: function(e) {
e.preventDefault();
this.props.onNavigate(this.props.links.last.href);
},
Each of these functions intercepts the default event and stops it from bubbling up. Then it invokes the onNavigate()
function with the proper hypermedia link.
Now conditionally display the controls based on which links appear in the hypermedia links in EmployeeList.render
:
render: function () {
var employees = this.props.employees.map(employee =>
<Employee key={employee._links.self.href} employee={employee} onDelete={this.props.onDelete}/>
);
var navLinks = [];
if ("first" in this.props.links) {
navLinks.push(<button key="first" onClick={this.handleNavFirst}>&lt;&lt;</button>);
}
if ("prev" in this.props.links) {
navLinks.push(<button key="prev" onClick={this.handleNavPrev}>&lt;</button>);
}
if ("next" in this.props.links) {
navLinks.push(<button key="next" onClick={this.handleNavNext}>&gt;</button>);
}
if ("last" in this.props.links) {
navLinks.push(<button key="last" onClick={this.handleNavLast}>&gt;&gt;</button>);
}
return (
<div>
<input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>
<table>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Description</th>
<th></th>
</tr>
{employees}
</table>
<div>
{navLinks}
</div>
</div>
)
}
As in the previous session, it still transforms this.props.employees
into an array of <Element />
components. Then it builds up an array of navLinks
, an array of HTML buttons.
Note
|
Because React is based on XML, you can’t put "<" inside the <button> element. You must instead use the encoded version.
|
Then you can see {navLinks}
inserted towards the bottom of the returned HTML.
Deleting entries is much easier. Get a hold of its HAL-based record and apply DELETE to its self link.
var Employee = React.createClass({
handleDelete: function () {
this.props.onDelete(this.props.employee);
},
render: function () {
return (
<tr>
<td>{this.props.employee.firstName}</td>
<td>{this.props.employee.lastName}</td>
<td>{this.props.employee.description}</td>
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
</tr>
)
}
})
This updated version of the Employee component shows an extra entry at the end of the row, a delete button. It is registered to invoke this.handleDelete
when clicked upon. The handleDelete()
function can then invoke the callback passed down while supplying the contextually important this.props.employee
record.
Important
|
This shows again that it is easiest to manage state in the top component, in one place. This might not always be the case, but oftentimes, managing state in one place makes it easier to keep straight and simpler. By invoking the callback with component-specific details (this.props.onDelete(this.props.employee) ), it is very easy to orchestrate behavior between components.
|
Tracing the onDelete()
function back to the top at App.onDelete
, you can see how it operates:
onDelete: function (employee) {
client({method: 'DELETE', path: employee._links.self.href}).done(response => {
this.loadFromServer(this.state.pageSize);
});
},
The behavior to apply after deleting a record with a page-based UI is a bit tricky. In this case, it reloads the whole data from the server, applying the same page size. Then it shows the first page.
If you are deleting the last record on the last page, it will jump to the first page.
One way to see how hypermedia really shines is to update the page size. Spring Data REST fluidly updates the navigational links based on the page size.
There is an HTML element at the top of ElementList.render
: <input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>
.
ref="pageSize"
makes it easy to grab that element via this.refs.pageSize.
defaultValue
initializes it with the state’s pageSize.
onInput
registers a handler as shown below.
handleInput: function (e) {
e.preventDefault();
var pageSize = React.findDOMNode(this.refs.pageSize).value;
if (/^[0-9]+$/.test(pageSize)) {
this.props.updatePageSize(pageSize);
} else {
React.findDOMNode(this.refs.pageSize).value =
pageSize.substring(0, pageSize.length - 1);
}
},
It stops the event from bubbling up. Then it uses the ref attribute of the <input>
to find the DOM node and extract its value, all through React’s findDOMNode()
helper function. It tests if the input is really a number by checking if it’s a string of digits. If so, it invokes the callback, sending the new page size to the App
React component. If not, the character just entered is stripped off the input.
What does App
do when it gets a updatePageSize()
? Check it out:
updatePageSize: function (pageSize) {
if (pageSize !== this.state.pageSize) {
this.loadFromServer(pageSize);
}
},
Because a new page size causes changes to all the navigation links, it’s best to refetch the data and start from the beginning.
With all these nice additions, you now have a really vamped up UI.
You can see the page size setting at the top, the delete buttons on each row, and the navigational buttons at the bottom. The navigational buttons illustrate a powerful feature of hypermedia controls.
Down below, you can see the CreateDialog
with the metadata plugged into the HTML input placeholders.
This really shows the power of using hypermedia coupled with domain-driven metadata (JSON Schema). The web page doesn’t have to know which field is which. Instead, the user can see it and know how to use it. If you added another field to the Employee
domain object, this pop-up would automatically display it.
In this session:
Issues?
You made the webpage dynamic. But open another browser tab and point it at the same app. Changes in one tab won’t update anything in the other.
That is something we can address in the next session. Until then, happy coding!