When you are developing web apps once in a while you need a flow of multiple pages just like a shopping cart at Amazon. Since there are different ways to implement such a multipage flow in Grails I thought of recording our experiences here.
The scenario
We model the process of submission of a proposal for different kinds of technologies. First you have to decide which type of proposal (once, long-term, in house) you want to submit. On the second page you give your personal infos and a general description of your intentions using the chosen technologies. Here you also choose the technologies you want. This has an impact on the following page which holds the information for the different technologies. Last but not least you save the proposal and show it to the user so he can see what data he entered. This flow could look like this:
-> chooseType -> enterGeneralInfo -> enterSpecificInfo -> save -> show
In each of the steps we need different parts of the full proposal. So we decided to use Command Objects in Grails. These objects are filled and are validated automatically by Grails. To use them you just have to delare them as parameter to the closure:
def enterSpecificInfo = {GeneralPartCommand cmd -> if (!cmd.hasErrors()) { Proposal proposal = new Proposal(cmd.properties) proposal.specificInfos = determineSpecificInfos(proposal.technologies) render(view: 'enterSpecificInfo', model: ['proposal': proposal]) } else { render(view: 'enterGeneralInfo', model: ['proposal': cmd]) } }
You can then bind the properties in the command object to the proposal itself. The GeneralPartCommand has the properties and constraints which are needed for the general information of the proposal. This imposes a violation of the DRY (Don’t Repeat Yourself) principle as the properties and the constraints are duplicated in the proposal and the command object but more about this in another post.
How to collect all the data needed
Since you can only and possibly want to save the proposal at the end of the flow when it is complete you have to track the information entered in the previous steps. Here hidden form fields come in handy. These have to store the information of all previous steps taken so far. This can be a burden to do by hand and is a place where bugs arise.
How to go from one state to another
To transition from one state to the next you just validate the command object (or the proposal in the final state) to see if all data required is there. If you have no errors you render the view of the next state. When something is missing you just re-render the current view:
if (!cmd.hasErrors()) { Proposal proposal = new Proposal(cmd.properties) proposal.specificInfos = determineSpecificInfos(proposal.technologies) render(view: 'enterSpecificInfo', model: ['proposal': proposal]) } else { render(view: 'enterGeneralInfo', model: ['proposal': cmd]) }
The errors section in the view takes the errors and prints them out. The forms in the views then use the next state for the action parameter.
Summary
An advantage of this solution is you can use it without any add-ons or plugins. Also the URLs are simple and clean. You can use the back button and jump to almost every page in the flow without doing any harm. Almost – the transition which saves the proposal causes the same proposal to be saved again. This duplicates the proposal because the only key is the internal id which is set in the save action.
Besides the DRY violation sketched above another problem arises from the fact that the logic of the states and the transitions are scattered between several controller actions and the action parameters in the forms. Also you have to remember to track every data in hidden form fields.
In a future post we take a look at how Spring (WebFlow) solves some of these problems and also introduces others.