In the last two weeks we have gone from zero to HTML5 hero with the Kendo UI JSP Wrappers. That was corny I know, but lets take a look at what we did in each of the previous two parts...
Part 1 (Read It)
- Created a basic JSP Dynamic Web Application
- Added a data access layer
- Created the first servlet
- Used the Kendo UI TreeView to lazy load hierarchical data
Part 2 (Read It)
- Expanded the data access layer to include products with relationships to the categories and suppliers tables
- Wired up a Kendo UI Grid to the products table
- Implemented server side paging for wicked fast presentation of large data sets
In last week's post, we also talked about how a great many applications are not much more than a grid laid on top of a relational database. The grid needs to expose an editing surface for users where they can safely manage their own data in an interface that stitches back together the data that has been broken apart for storage.
Editing In A Kendo UI Grid
Today we will look at enabling more editing features in the Kendo UI grid with the JSP wrappers.
You can grab the code for today's article from the GitHub Repo (It’s part 3).
To turn editing on in the grid, it's pretty trivial. You just need to set the editable: true attribute on the grid.
<kendo:grid name="products" pageable="true" editable="true"> <kendo:dataSource pageSize="5" serverPaging="true"> <kendo:dataSource-transport read="api/products"> <kendo:dataSource-transport-parameterMap> .....
What you will notice when you do that is that when you click on an item in the grid, it becomes editable. What's even better is that Kendo UI is smart enough to recognize that you have numeric data types so it gives you a numeric textbox. It also automatically gives you a checkbox for the boolean field.
However, the Supplier and Category are plain text fields. We don't want this. We want them to be able to choose from a list of already predefined values for categories and suppliers in the database. Also, the grid is giving us a numeric textbox for the price, but we don't want a plain number there, we want currency.
Formatting grid columns
We can solve the price issue right away by specifying a format for the column. In this case, we want currency so all we need to do is specify a "c". Format are specified as {0:format}.
<kendo:grid-columns> <kendo:grid-column title="Name" field="ProductName" /> <kendo:grid-column title="Supplier" field="Supplier.SupplierName" /> <kendo:grid-column title="Category" field="Category.CategoryName" /> <kendo:grid-column title="Price" field="UnitPrice" format="{0:c}" /> <kendo:grid-column title="# In Stock" field="UnitsInStock" /> <kendo:grid-column title="Discontinued" field="Discontinued" /> </kendo:grid-columns>
We could specify a more restrictive format for the column You can refer to the Kendo UI formatting docs for all the available options and formats.
The supplier and category fields seem fine right now, but when we go into edit mode, they are displayed as plain text. We need dropdowns instead. We are currently bound to the SupplierName and CategoryName fields. Instead, we are going to bind to the Supplier and Category objects themselves. Since we are now bound to an object instead of a value, we need to specify a template so the grid doesn't give us [object Object].
<kendo:grid-columns> <kendo:grid-column title="Name" field="ProductName" /> <kendo:grid-column title="Supplier" field="Supplier" template="#: Supplier.SupplierName #" /> <kendo:grid-column title="Category" field="Category" template="#: Category.CategoryName #" /> <kendo:grid-column title="Price" field="UnitPrice" format="{0:c}" /> <kendo:grid-column title="# In Stock" field="UnitsInStock" /> <kendo:grid-column title="Discontinued" field="Discontinued" /> </kendo:grid-columns>
Kendo UI Templates are a way to specify how the data is output. You use #: on the left, put your binding expression in the middle, then close it off with another #. The binding expression is the specific field in the source object you want the value from.
We could mix HTML in with this as well if we wanted to. For instance, if we wanted to display a checkmark in the Discontinued column instead of just true/false, we could give the grid a template.
<kendo:grid-columns> <kendo:grid-column title="Name" field="ProductName" /> <kendo:grid-column title="Supplier" field="Supplier" template="#: Supplier.SupplierName #" /> <kendo:grid-column title="Category" field="Category" template="#: Category.CategoryName #" /> <kendo:grid-column title="Price" field="UnitPrice" format="{0:c}" /> <kendo:grid-column title="# In Stock" field="UnitsInStock" /> <kendo:grid-column title="Discontinued" field="Discontinued" template=" # if (data.Discontinued) { # <span class='k-icon k-i-tick'></span> # } #" /> </kendo:grid-columns>
These template can execute JavaScript logic like I have included above. To identify JavaScript blocks, you open with a # and the close with a # before you return to straight HTML. Refer to the Kendo UI Template documentation for more information on how you can use templates to get complete control over your UI.
Now that we have the Products and Categories templates working, we need to address their edit mode. Right now when the grid goes into edit mode, it will display [object Object] again for Supplier and Category. We need to specify Custom Editors for these fields.
Drop down below the grid and open a script tag. Inside of it create a function called supplierEditor that takes in a container parameter and an options parameter.
..... </kendo:grid-columns> </kendo:grid> <script> function supplierEditor(container, options) { $("<input data-text-field='SupplierName' data-value-field='SupplierID' data-bind='value:" + options.field + "' />") .appendTo(container) .kendoDropDownList({ dataSource: { transport: { read: "api/suppliers" } } }); }; </script>
This function does a few things so lets break it down.
- Creates a new input element with jQuery. This new element has all of it's configuration in the HTML by way of data attributes. Any setting on a Kendo UI Widget can be declared in the HTML by using a data attribute. Any camel cased settings are separated by dashes (i.e. dataTextField becomes data-text-field)
- The new input is appended to the container which is the grid row
- The input is transformed into a Kendo UI DropDown List and reads from the api/suppliers endpoint which doesn't exist yet.
Since the Category field is nearly identical to the Supplier, just copy the function and change any references of Supplier to Category.
function categoryEditor(container, options) { $("<input data-text-field='CategoryName' data-value-field='CategoryID' data-bind='value:" + options.field + "' />") .appendTo(container) .kendoDropDownList({ dataSource: { transport: { read: "api/categories" } } }); };
Lets now define the two endpoints that we need for the DropDown Lists. This would be the api/suppliers and api/categories.
Creating the necessary repositories
If you have been following this series, you know that we will need to create data access endpoints for our Suppliers and Categories.
Right-click the repositories package and create a new class called SuppliersRepository. Make sure you change the Superclass to our Repository base class.
Create a doList method that returns a list of all the Suppliers from the database.
Remember that I am passing the path string in for my file based database. You will most likely not have to do this so you wouldn't need the constructor like I have it.
package repositories; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class SuppliersRepository extends Repository { public SuppliersRepository(String path) { super(path); } public ListlistSuppliers() throws SQLException { PreparedStatement stmt = null; ResultSet rs = null; // prepare a list of suppliers to populate as a return value List suppliers = new ArrayList (); try { // set sql statement String sql = "SELECT SupplierID, CompanyName AS SupplierName FROM Suppliers"; // prepare the string for safe execution stmt = super.conn.prepareStatement(sql); // execute the statement into a ResultSet rs = stmt.executeQuery(); // loop through the results while(rs.next()) { // create a new supplier object models.Supplier supplier = new models.Supplier(); // populate it with the values from the database supplier.setSupplierID(rs.getInt("SupplierID")); supplier.setSupplierName(rs.getString("SupplierName")); // add the supplier to the return list suppliers.add(supplier); } } finally { // close out all connection related instances stmt.close(); rs.close(); } // return the list of suppliers return suppliers; } }
Now create a repository for the Categories. It's nearly identical to the Suppliers repository with a few subtle differences.
package repositories; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class CategoriesRepository extends Repository { public CategoriesRepository(String path) { super(path); } public List<models.Category> listCategories() throws SQLException { PreparedStatement stmt = null; ResultSet rs = null; // create a list of categories to return List<models.Category> categories = new ArrayList<models.Category>(); try { // create the sql string String sql = "SELECT CategoryID, CategoryName FROM Categories"; // prepare the string for safe execution stmt = super.conn.prepareStatement(sql); // execute the sql and return the results to the ResultSet rs = stmt.executeQuery(); // iterate through the result set while(rs.next()) { // create a new category object models.Category category = new models.Category(); // populate it's values from the database category.setCategoryID(rs.getInt("CategoryID")); category.setCategoryName(rs.getString("CategoryName")); // add it to the list of categories categories.add(category); } } finally { // close out all connection related instances stmt.close(); rs.close(); } // return the list of categories return categories; } }
Adding the servlets
By now you should be comfortable creating new servlets. Right-click the api package and select New/Servlet. Call it Suppliers and change the url mapping to /api/suppliers.
Include a reference to the SupplierRepository at the top. Also include the Gson library for returning JSON. Again, if you have been following along, this should all be old hat. Remember that I do some server path magic in my samples because I am using a file based database. In the doGet method, return the list of suppliers in JSON format.
package api; import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.google.gson.Gson; import repositories.SuppliersRepository; /** * Servlet implementation class Suppliers */ @WebServlet("/api/suppliers") public class Suppliers extends HttpServlet { private static final long serialVersionUID = 1L; private repositories.SuppliersRepository _repository; private Gson _gson = new Gson(); /** * @see HttpServlet#HttpServlet() */ public Suppliers() { super(); // TODO Auto-generated constructor stub } public void init() throws ServletException { super.init(); _repository = new SuppliersRepository(this.getServletContext().getRealPath("data/sample.db")); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // create a list of suppliers to send back as JSON List<models.Supplier> suppliers = new ArrayList<models.Supplier>(); try { // get the suppliers from the database suppliers = _repository.listSuppliers(); // set the content type we are sending back as JSON response.setContentType("application/json"); // print the content to the response response.getWriter().print(_gson.toJson(suppliers)); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); response.sendError(500); } } }
Now we need a Categories servlet.
package api; import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import repositories.CategoriesRepository; import com.google.gson.Gson; /** * Servlet implementation class Categories */ @WebServlet("/api/categories") public class Categories extends HttpServlet { private static final long serialVersionUID = 1L; private repositories.CategoriesRepository _repository = null; private Gson _gson = new Gson(); /** * @see HttpServlet#HttpServlet() */ public Categories() { super(); } public void init() throws ServletException { super.init(); _repository = new CategoriesRepository(this.getServletContext().getRealPath("data/sample.db")); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { List<models.Category> categories = new ArrayList<models.Category>(); try { categories = _repository.listCategories(); // set the content type we are sending back as JSON response.setContentType("application/json"); // print the content to the response response.getWriter().print(_gson.toJson(categories)); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); response.sendError(500); } } }
With all that plumbing work out of the way, we just need to have the grid use these new custom editors. We do that by specifying the editor attribute for the Supplier and Category columns.
<kendo:grid-columns> <kendo:grid-column field="ProductName" title="Product"></kendo:grid-column> <kendo:grid-column field="Supplier" title="Supplier" editor="supplierEditor" template="#: Supplier.SupplierName #"></kendo:grid-column> <kendo:grid-column field="Category" title="Category" width="150px" editor="categoryEditor" template="#: Category.CategoryName #"></kendo:grid-column> <kendo:grid-column field="UnitPrice" title="Price" format="{0:c}" width="75px"></kendo:grid-column> <kendo:grid-column field="UnitsInStock" title="# In Stock" width="80px"></kendo:grid-column> <kendo:grid-column field="Discontinued" title="Discontinued" width="100px"></kendo:grid-column> <kendo:grid-column> <kendo:grid-column-command> <kendo:grid-column-commandItem name="edit"></kendo:grid-column-commandItem> <kendo:grid-column-commandItem name="destroy"></kendo:grid-column-commandItem> </kendo:grid-column-command> </kendo:grid-column> </kendo:grid-columns>
I also adjusted the column widths a bit to make things look a tad cleaner. Edit mode now displays a drop down for the Suppliers and Categories.
We have successfully joined all of the backend data into a cohesive interface for our users. However lets tweak the edit interface just a bit before we actually wire everything up.
Altering the default edit mode
We can edit a row just by clicking into it, but then each cell is in edit mode individually. I want the whole row in edit mode. To do this, we need to set the grid edit mode to "inline".
<kendo:grid name="products" pageable="true"> <kendo:grid-editable mode="inline" /> ....
Now clicking on a row will do nothing. We need to add a button which will put the row into edit mode. We do this with Command Columns. Let's also add a delete button while we're at it. Delete is referred to as Destroy throughout Kendo UI as delete is a JavaScript reserved word.
.... <kendo:grid-column field="UnitsInStock" title="# In Stock" width="80px"></kendo:grid-column> <kendo:grid-column title="Discontinued" field="Discontinued" width="100px" template="# if (data.Discontinued) { # <span class='k-icon k-i-tick'></span> # } #" /> <kendo:grid-column> <kendo:grid-column-command> <kendo:grid-column-commandItem name="edit"></kendo:grid-column-commandItem> <kendo:grid-column-commandItem name="destroy"></kendo:grid-column-commandItem> </kendo:grid-column-command> </kendo:grid-column> </kendo:grid-columns>
Now we get a column with Edit and Delete buttons.
Now clicking a row will put it in update mode. When the grid switches to update mode, the Edit and Delete buttons will turn to Update and Cancel.
I don't like the way this looks though. The numeric editors are cutting off their content and the buttons stack on each other inside of the column. We can get into adjusting column widths here, but that's tedious. Another option is to just display a popup window for the user to edit in. The grid allows us to do this by simply specifying popup as the edit mode.
<kendo:grid name="products" pageable="true"> <kendo:grid-editable mode="popup" />
And just like that you get a nice modal window with all the fields aligned, all the labels aligned right and plenty of room for editing.
Grab the source
As always, you can grab the source for this project from the GitHub repo. This is part 3 by the way.
Coming up next
Next we'll look at wiring all this up to the server. We also need to add the ability to create items which is going to take us into DataSource Model territory where we will also be able to leverage the Kendo UI validation framework for easy field validation.