#{extends '../main.html'/}# #{set title:'Webpieces QuickStart'/}# #{set tab:'management'/}# #{renderTagArgs 'docHome.html'/}# #{renderTagArgs 'quickStartList.html'/}#

HTML CRUD

Last updated: Sept 14th, 2020

It's always best to start with basic html and keep things simple to start with when learning so this is the pure html, no javascript version of list all my Accounts, create an account, edit an account, and delete an account. The basic CRUD flow is so common, it deserves it's own example. If you want the advanced AJAX version, skip to the next page

First, create a new package called crudexamples in your org.webpieces.helloworld/web directory.

Next, let's create a new RouteId file called ExampleRouteId like so:

package org.webpieces.helloworld.web.crudexamples;

import org.webpieces.router.api.routes.RouteId;

public enum ExampleRouteId implements RouteId {
	//list
	LIST_ACCOUNTS,
	//add/edit
	GET_ADD_ACCOUNT_FORM, GET_EDIT_ACCOUNT_FORM, POST_ACCOUNT_FORM,
	//delete
	CONFIRM_DELETE_ACCOUNT, POST_DELETE_ACCOUNT
}
LIST_ACCOUNTS
This is the route for GET request to return the list accounts page
GET_ADD_ACCOUNT_FORM
This is the route for GET request to return the form to fill in a new account
GET_EDIT_ACCOUNT_FORM
This is the route for GET request to return the form to edit an existing account and may or may not be the same html/template as the GET_ADD_ACCOUNT_FORM route
POST_ACCOUNT_FORM
This route is for when you post from 1 of the 2 forms above. You could have 2 post routes, one for edit and one for create but we don't need to
CONFIRM_DELETE_ACCOUNT
When someone clicks delete, we render a 'are you sure page' to make sure they didn't accidentally click delete
POST_DELETE_ACCOUNT
When they click yes, on the 'are you sure' page, this is the POST route invoked

Next, let's create a RouteModule class called "ExampleRoutes." In here, there is a special method for a CRUD operation like so:

package org.webpieces.helloworld.web.crudexamples;

import org.webpieces.ctx.api.HttpMethod;
import org.webpieces.router.api.routebldr.DomainRouteBuilder;
import org.webpieces.router.api.routebldr.RouteBuilder;
import org.webpieces.router.api.routes.Ports;
import org.webpieces.router.api.routes.Routes;
import org.webpieces.router.api.routes.CrudRouteIds;

public class ExampleRoutes implements Routes {
	@Override
	protected void configure(DomainRouteBuilder bldr) {
		RouteBuilder b = bldr.getAllDomainsRouteBuilder();

		CrudRouteIds routeIds = new CrudRouteIds(
				ExampleRouteId.LIST_ACCOUNTS, ExampleRouteId.GET_ADD_ACCOUNT_FORM,
				ExampleRouteId.GET_EDIT_ACCOUNT_FORM, ExampleRouteId.POST_ACCOUNT_FORM,
				ExampleRouteId.CONFIRM_DELETE_ACCOUNT, ExampleRouteId.POST_DELETE_ACCOUNT);
		b.addCrud(Port.BOTH, "account", "ExampleAccountController", routeIds);
	}
}

The "account" we pass in becomes part of the 'standard url pattern' we happen to use in this addCrud method but feel free to create your own 'addCrud' method that your whole app can use to stay consistent. This method adds all these routes for you but more importantly keeps your urls consistent for CRUD operations:

  1. b.addRoute(Port.BOTH, HttpMethod.GET , "/account/list", "ExampleAccountController.accountList", listRoute);
  2. b.addRoute(Port.BOTH, HttpMethod.GET , "/account/new", "ExampleAccountController.accountAddEdit", addRoute);
  3. b.addRoute(Port.BOTH, HttpMethod.GET , "/account/edit/{id}", "ExampleAccountController.accountAddEdit", editRoute);
  4. b.addRoute(Port.BOTH, HttpMethod.POST, "/account/post", "ExampleAccountController.postSaveAccount", saveRoute);
  5. b.addRoute(Port.BOTH, HttpMethod.GET, "/account/confirmdelete/{id}", "ExampleAccountController.confirmDeleteAccount", confirmDelete);
  6. b.addRoute(Port.BOTH, HttpMethod.POST, "/account/delete/{id}", "ExampleAccountController.postDeleteAccount", deleteRoute);

Notice, that it names the url /account/*** and the methods accountList and accountAddEdit and all post methods then start with postXXXX keeping consistency so every GET request invokes normal methods but POST methods always start with postXXX keeping your app consistent. Next, let's create the AccountDbo.java entity bean like so:

As with before, let's add this RouteModule file to ProdServerMeta.java (org.webpieces.helloworld.base) to make those routes accessible:

*[@Override
public List getRouteModules() {
    return Lists.newArrayList(
            new LoginRoutes("/org/webpieces/helloworld/web/login/AppLoginController", "/secure/.*", "password"),
            new CrudRoutes(),
            new AjaxCrudRoutes(),
            new JsonRoutes(),
            new MyMainRoutes(),
            new ExampleRoutes()
            );
}]*

Next we create the database object for our accounts inside the db package (recall that Hibernate only scans a portion of your code for database objects!):

*[package org.webpieces.helloworld.db;

import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Query;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@Entity
@Table(name="ACCOUNT")
@NamedQueries({
	@NamedQuery(name = "findAllAccounts", query = "select u from AccountDbo u"),
})
public class AccountDbo {

	@Id
	@SequenceGenerator(name="account_id_gen",sequenceName="account_sequence" ,initialValue=1,allocationSize=10)
	@GeneratedValue(strategy=GenerationType.SEQUENCE,generator="account_id_gen")
	private Integer id;

	@Column(unique = true)
	private String name;

	private String contactsName;

	private int priority;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}

    public String getContactsName() {
		return contactsName;
	}

	public void setContactsName(String contactsName) {
		this.contactsName = contactsName;
	}

	public int getPriority() {
		return priority;
	}

	public void setPriority(int priority) {
		this.priority = priority;
	}

	@SuppressWarnings("unchecked")
	public static List findAll(EntityManager mgr) {
		Query query = mgr.createNamedQuery("findAllAccounts");
		return query.getResultList();
	}
}]*

Then, we can now create ExampleAccountController.java with our first method accountList like so:

*[package org.webpieces.helloworld.web.crudexamples;

import java.util.List;

import javax.inject.Singleton;
import javax.persistence.EntityManager;

import org.webpieces.plugins.hibernate.Em;
import org.webpieces.router.api.controller.actions.Action;
import org.webpieces.router.api.controller.actions.Actions;

@Singleton
public class ExampleAccountController {

	public Action accountList() {
		EntityManager mgr = Em.get();
		List accounts = AccountDbo.findAll(mgr);
		return Actions.renderThis("accounts", accounts);
	}
}]*

Then, of course, we need an html page called accountList.html since renderThis is being called:

*[#{extends '../mainTemplate.html'/}#
#{set title:'Awesome CRUD'/}#
#{set tab:'none'/}#

Accounts

#{list items:accounts, as:'entity'}# #{/list}# #{else}# #{/else}#
Account Name Contacts Name Priority
${entity.name}$ ${entity.contactsName}$ ${entity.priority}$ &{'Edit', 'link.edit'}& &{'Delete', 'link.delete'}&
There are no accounts, Add one now please.
Add Account ]*

Finally, boot up your DevelopmentServer.java main class (if it's not already running!) and open a browser and go to http://localhost:8080/account/list or go to https://localhost:8443/account/list

We can now go over this template piece by piece:

  1. *[#{extends '../base/main.html'/}#]* is extending a template which uses 'body' and 'title' and 'tab' variables
  2. *[#{set title:'Awesome CRUD'/}#]* is simply setting a title that main.html requires and will be displayed in the browser title
  3. *[#{set tab:'none'/}#]* We are not really using this one but if you look at main.html, it uses tab to set special css on the matching tab name in the list at the top of the page
  4. Next, we see a basic html table with some templating logic mixed in
  5. *[#{list items:accounts, as:'entity'}#]* This basically loops over each account storing it in the entity variable so that html in between the begin and end list tag will be generated
  6. *[ &{'Edit', 'link.edit'}& ]* This defines a reverse url lookup using the account's id (entity.id). The id of the a element is simply the index for css purposes if desired
  7. *[#{else}#]* If there are no accounts, whatever html is in the else will be displayed

Now, go to http://localhost:8080/account/list to view your work of art. You will notice you have no accounts so next it might be a good idea to create the controller method and the HTML form for creating some accounts. Let's start with our ExampleAccountController:

*[public Action accountAddEdit(Integer id) {
		if(id == null) {
			return Actions.renderThis("entity", new AccountDbo());
		}

		AccountDbo account = Em.get().find(AccountDbo.class, id);
		if(account == null)
			throw new NotFoundException("Account is not found");
		return Actions.renderThis("entity", account);
}]*

This method accepts the id (or you can use some other unique identifier) and if it is null, then it assumes a create and supplies back an empty Account to render the html page with. If however, there is an id, we look up the account and if it is not found, we throw NotFound which will in turn invoke our NotFound route to return a 404 and a nice looking page with the 404 to the browser. If the account is found, we render the same create form with all the fields filled in with the account data. The html for this create or edit page is as follows:

*[#{extends '../mainTemplate.html'/}#
#{set title:'Add/Edit'/}#
#{set tab:'none'/}#

#{form action:@[POST_ACCOUNT_FORM]@, class:'form-horizontal', style:'min-width:500px;max-width:800px;margin: 0 auto'}#
   
    
    
#{/form}#]*

The first 3 lines are the same as the accountList.html we wrote about above(hopefully you were reading and not spaced out). Then, we use the form tag along with the POST_ACCOUNT_FORM url lookup. Next, we have *[]* which is the special html for saving if this is an edit or a creation. If we are editing a value, entity.id on posting will be filled in and if we are creating an account, entity.id will be a 0 length string.

Finally, we reach the real meat, and yes, I spent shit loads of time perfecting this tag to work with arrays, and all sorts of things that you may run into saving your ass. You can thank me later(jk, but monetary donations accepted). The field tag works with *[ &{'Cancel', 'link.cancel'}& #{/form}#]*

This looks much like the POST for add/edit account except one minor difference in that we use the reverse route into url tokens with the entities id *[@[POST_DELETE_ACCOUNT, id:entity.id]@]* and pass the entity.id in there so the POST url ends up having the id. We could have instead done a hidden field as well like the add/edit. The choice is up to you. We could use a GET request to delete an entity but we prefer to use POST for deletes/creates/modifications and use GET for read with no side-affects to the database. There are definite exceptions to this like a confirmation email may contain a GET request link which would modify your database state of the email to be confirmed. So while, some developers stay in the theoretical world, there always seem to be exceptions since you can't do a POST from an email which we can redirect to a GET.

Now, if you really want, from an email, you can do a GET, then redirect to a POST and then redirect back to a GET to stay compliant with GET requests are only reads, not writes. Ideally, when you do a GET, it only reads but this type of GET/Redirect/POST/Redirect will still be a link that tries modify the database a second time (to the same value of course the second time)

Next, let's create the post method in our controller to actually delete the entity

*[public Redirect postDeleteAccount(int id) {
		AccountDbo ref = Em.get().find(AccountDbo.class, id);

		Em.get().remove(ref);
		Em.get().flush();
		Current.flash().setMessage("Account deleted");
		Current.flash().keep(true);
		return Actions.redirect(AccountRouteId.LIST_ACCOUNTS);
}]*

Now, go ahead and create and delete entities to your hearts content to see the flow of the standard CRUD code for webpieces. We hope to have a plugin to generate all this on fly in the future for you. Basically follow a wizard, type in your fields and we generate the list page, add/edit/delete pages, controllers, routes, etc. all in one tight little package.

In the next tutorial let's do an ajax CRUD in webpieces which is the same amount of code ironically.

Next Ajax CRUD