The coolest feature of Richfaces is of course its built in Ajax support (which has partly made its way into JSF 2.0). Part of that Ajax toolbox is the modal panel - a popup div you can drag and optionally resize. The modal panel is excellent for showing details and performing inserts/updates on records. It is a beast to get to grips with though. I've been through the pain, I hope this blog post can help some people to do it in less time.
Even though I am a user of JBoss Seam, I'll keep the code in this article pure JSF (and Richfaces of course). Also, this is not a JSF or Richfaces beginners tutorial. I expect you to know how JSF works and how to setup Richfaces. If you do not, the reference guide explains it quite well.
Finally, I assume you will use Facelets as the view technology, and I also assume you know how Facelets templating works.
A technical warning
Richfaces uses JQuery for some of its components - the modal dialog is one of them. If you include JQuery in the page yourself, you will most likely break the richfaces components. Don't say I didn't warn you!The basics of a modal panel
Panels are excellent to turn into reusable components. That's why the best strategy to take is to turn them into include files.<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:rich="http://richfaces.org/rich" xmlns:a4j="http://richfaces.org/a4j"> <rich:modalPanel id="edituser" width="420" height="420" resizeable="false"> <f:facet name="header">Add user</f:facet> <a4j:outputPanel layout="block" id="edituserouter"> <h:form id="edituserform"> <table> <tr> <td>Name:</td> <td> <h:inputText value="#{editUserForm.name}" size="20"/> </td> </tr> <tr> <td>Username:</td> <td> <h:inputText value="#{editUserForm.username}" size="20"/> </td> </tr> <tr> <td>Password:</td> <td> <h:inputText value="#{editUserForm.password}" size="20"/> </td> </tr> </table> <div class="panelbuttons"> <h:commandLink action="#{editUserForm.save}" value="Add user" /> <a4j:commandLink actionListener="#{editUserForm.cancel}" value="Close" oncomplete="#{rich:component('edituser')}.hide(); return false"/> </div> </h:form> </a4j:outputPanel> </rich:modalPanel> </ui:composition>
There; slap this in its own xhtml file and you can include it wherever you need the add user dialog. Like this:
<ui:include src="panels/edituser.xhtml"/>
(I tend to put my include files with panels in a panels subdirectory, adjust to your own environment).
This first example is going to be about a dialog to add a user - later on I'll expand it so the exact same dialog can be used to both add and edit a user.
Lets take a moment and examine the above basic skeleton, shall we?
The basic elements of a modal dialog
A modal dialog, at least mine, has three parts:
- A title bar
- A body with a form
- A button bar with action/close links
The title part is easy; Richfaces manages it for us. The only thing we need to do is give the thing a header facet and you're done. There are more options to explore here, but I like to keep it simple. The width and height attributes do exactly that: give the dialog a specific width and height. Finally the resizeable attribute prevents the user from being able to resize the dialog - by default this is enabled.
The edituserouter panel is actually paramount to the entire setup - this wraps the body of the dialog. This allows us to not only target it in a4j rerender actions (rerendering the entire dialog does not work like you would want it), but it also allows you to attach an overflow:auto CSS attribute to it to make the body of the dialog scrollable should you need it!
For the buttons I use commandLinks. The 'add user' link calls an action method while the cancel link invokes an action listener. The cancel link right now does its magic through an ajax call - the only thing it has to do is hide the dialog, but the action listener does an additional thing, which we'll see in a moment.
Notice the following piece of code:
#{rich:component('edituser')}.hide();
That will result in a piece of javascript that references a javascript object that backs the modal dialog, allowing us to hide it. Incidentally, in the page you want to actually show the dialog you can and must do the following:
<a4j:commandButton value="Add user" actionListener="#{myForm.addUser}" oncomplete="#{rich:component('edituser')}.show()" reRender="edituserouter"/>
(of course you can also use an a4j:commandLink) This piece of logic performs three tasks:
- Calls an action listener through an Ajax invocation so we can prepare the content of the dialog
- Makes the dialog visible
- Rerenders the body part of the dialog to react to our preparations on the server side. This is going to be especially important when we add edit capabilities
The backing beans
Alright, we want to create a dialog to add a user. You can make any kind of form that you want, I'm keeping it simple: our user is going to have a name, a username and a password. We need a backing bean for that; we are going to store this in a session scoped bean of my own design. Like this (note, don't forget to configure in the faces-config.xml):
public class EditUserForm { private String name; private String username; private String password; public void reset(){ name = null; username = null; password = null; } public String save(){ if(name.trim().length() == 0){ addErrorMessage("Please input a name."); return null; } else if(username.trim().length() == 0){ addErrorMessage("Please input a username."); return null; } else if(password.trim().length() == 0){ addErrorMessage("Please input a password."); return null; } // data is valid, call DAO method to insert user here // userDao.insertUser(name, username, password); // reset form to not let stale data linger reset(); // reload the page that opened the dialog by navigating to its identifier // note that returning null will cause the page to not reload at all return "insert_identifier_here"; } public void cancel(ActionEvent event){ reset(); } private void addErrorMessage(String msg) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, null)); } // getters and setters here }
Again some key pieces here:
- The reset() method I add to all my session scoped beans that temporarily hold data I need, like in a dialog. This allows me to start with a clean slate whenever I desire.
- I already added some validations to the save action method - I don't believe in JSF validators. For one they get in the way of Ajax events. No, I like to do the validations where the data is actually used - in the action method itself.
The example doesn't have the capacity yet to show the validation errors - we'll get to that in a moment.
- The navigation rule you return determines what happens after the save action is performed - if you return null, the dialog will close but the main page won't reload. If you need the inserted (or updated) data to actually show in your main page, you can return the navigation identifier of the page in stead; this will cause the page to reload.
We now have all the tools to make the dialog do its magic! The only thing we're missing is the action handler we want to call when we open the dialog in our main page; its very simple for now:
public class MyForm { private EditUserForm editUserForm; public void addUser(ActionEvent event){ editUserForm.reset(); } // note: use JSF dependency injection public EditUserForm getEditUserForm() { return editUserForm; } public void setEditUserForm(EditUserForm euf) { this.editUserForm = euf; } }
For our add functionality, its no more than this - we want to be sure we don't have old data, so we reset() our EditUserForm session scoped bean.
Ajax and validation errors
When the user inputs bogus data into the form (or no data at all), we will want to inform said user of this WITHOUT closing the dialog or causing any kind of page reload. This is a bit tricky, but it can be done.First of all, we print messages in the dialog:
<h:form id="edituserform"> <h:messages layout="table" errorClass="errorMessage"/> ... </h:form>
In order to know if there are validation errors, we should make the backing bean be able to tell us:
public boolean isHasErrors() { return FacesContext.getCurrentInstance().getMessages().hasNext(); } // empty setter so JSF doesn't complain about it missing on a postback public void setHasErrors(boolean f){ }
Now we can add that as an element of the form...
<h:form id="edituserform"> <h:inputHidden id="hasErrors" value="#{editUserForm.hasErrors}" /> <h:messages layout="table" errorClass="errorMessage"/> ... </h:form>
And now we can transform our add user link into the following:
<a4j:commandLink action="#{editUserForm.save}" value="Add user" oncomplete="checkErrors(); return false;" reRender="edituserouter"/>
Basically this says 'if there are no errors after the action method has run, you can close the dialog'. The rerender will make it so any error messages are actually displayed inside the dialog. If there are none it will rerender a dialog that isn't even visible - oh well.
We are missing the checkErrors() javascript function:
<script type="text/javascript"> //<![CDATA[ function checkErrors(){ if (document.getElementById('edituserform:hasErrors').value=='false') { #{rich:component('edituser')}.hide(); } } //]]> </script>
Nothing too surprising there. You could inline this piece of code into the oncomplete attribute if you want. Note that for the getElementById call to work, both the form and the hidden input elements need to have manually set IDs so you can be sure what ID JSF will generate for the input element. You could choose to add the prependId="false" attribute to the form so JSF doesn't add form IDs to its elements (might be Mojarra specific).
Now you have all the tools at your disposal to create a modal dialog in which you can add a new user, perform validations on the inputted values and have full control over when the dialog is shown and hidden. Lets take it one little step further and add edit capabilities as well.
Creating an add/edit dialog
The trick is to make the code flexible enough that you can go either way - the magic is in two parts:- OPTIONALLY you will want to prefil data into the form, from an existing object
- Come the time to save the data, you will want to either insert or update it
Anything else should be the same, except for some labels in the dialog perhaps (its a bit confusing to have an 'add user' link when you are actually modifying an existing one). That is basic JSF stuff, I'm sure you can figure out how to get that done.
The way I make editing work is by adding a setExistingXXX() method to the session scoped bean. Lets say we have a User entity, we can use that:
private User existing = null; ... public void setExistingUser(User user){ existing = user; name = user.getName(); username = user.getUsername(); password = user.getPassword(); } public User getExistingUser(){ return existing; } public void reset(){ existing = null; ... }
Okay, now we can prefill the form with information from an existing user. Now we need to be able to actually get that object into the session scoped bean. There are multiple ways to rome, I like to stick to the basics: pass an ID value as a request parameter.
In a user listing page, we might have something like this:
<h:dataTable value="#{myForm.users}" var="item"> <h:column> <f:facet name="header"> <h:outputText value="name"/> </f:facet> <h:outputText value="#{item.name}"/> </h:column> </h:dataTable>
A bit basic, but this datatable will show the names of all users in our system. I'll leave the implementation of the getUsers() method of the MyForm backing bean up to your imagination.
Now we want an edit link per user. We add another column:
<h:column> <a4j:commandLink oncomplete="#{rich:component('edituser')}.show()" reRender="edituserouter" actionListener="#{myForm.editUser}" value="edit"> <f:param value="#{item.id}" name="userId" /> </a4j:commandLink> </h:column>
So we create a commandLink that invokes the actionListener editUser and passes the ID of the user along as a request parameter in the Ajax request. Upon completing the dialog is made visible and the body panel is rerendered to show the data we have set in the session bean.
Lets see that editUser backing bean method:
public void editUser(ActionEvent event){ editUserForm.reset(); Long toEdit = Long.parseLong(getRequestParameter("userId")); // implement your own DAO logic here to fetch the user User user = userDao.getUser(toEdit); editUserForm.setExistingUser(user); } private String getRequestParameter(String name) { return FacesContext.getCurrentInstance().getExternalContext() .getRequestParameterMap().get(name); }
There, that allows us to fetch the ID we passed to the link, fetch the user using your own DAO logic and then pass it along to the EditUserForm session scoped bean. And we're set, the form is now filled with the user's data!
Now we just need to save it back. I use a very simple trick to know if I want to insert or update an entity:
public String save(){ // validations here if(existing != null){ // update existing entity } else { // insert new entity here } // problem here: see next paragraph return "insert_identifier_here"; }
Thats it. No that isn't it, you still need to DAO methods to actually perform the insert/update; you should be able to deal with that yourself though. But now you have effectively made it so you can use the exact same dialog and the exact same backing form to do either an insert or an update!
Using the same dialog in multiple views
There is one problem with the reusable dialog: it always returns the same view identifier upon saving. If you want to use the popup in multiple views, you want it to return to the view it was opened in.For now, I see only one solution: set the view identifier when you initialize the dialog.
public void editUser(ActionEvent event){ editUserForm.reset(); // setViewIdentifier() is a new method editUserForm.setViewIdentifier("edituserlist"); ... }
public String save(){ ... return getViewIdentifier(); }
This way you can set a different view identifier in each page you use the dialog in and have it redirect to the correct page when you save.
It doesn't work!
Here are some things to check:
- Did you include the dialog into the page?
- Are you including the JQuery library in the page? (don't)
- Use firefox and check the error console to see if there are javascript errors. Usually there is a typo somewhere.
- Is javascript even enabled in the browser?
- Check the server logs to see if there aren't any hidden error logs in there - ajax invoked method calls tend to not cause the error to display in the browser.
- Are you sure that the EditUserForm bean is session scoped?
If this doesn't answer your question... well good luck figuring it out then!
Conclusion
There you go. What I have presented in this article is everything I personally desire from a modal dialog. I hope with the tools and knowledge I have presented here, you can add cool popup dialogs to your webapp as well. Happy coding!A last note: this code is taken from one of my existing projects and thus should work, but of course I had to cut it down and massage it a little to make it easier to read and understand; a typo might have slipped in here and there. Sorry if it happened!
Hi, great post. However I have problem with validating the errors. I debugged the code - isHasErrors() is called before save() method. Therefore the value of hidden input is always false when tested with JS.
ReplyDeleteI tried immediate attribute on Save button in modal dialog box, but then just isHasErrors() is never called, therefore again remains false.
I checked everything twice. What is the problem?
Thank you
Uwe
I don't know. As stated I took this code from a working project, so if there is a difference in execution in your situation you must be doing something differently somewhere.
ReplyDeleteThe best I can do is work up a small runnable proof of concept you can compare against (I should have done that to begin with); I don't know when I'll have the time to do that though.
ok, for now I'm displaying the error message back on main page (which of course is not good (and also the appropriate field is not getting highlighted properly :( )).
ReplyDeleteWhen you have time in future, I will be curious for standalone project that can be tested in my environment (seam 2.2.0, richfaces 3.3.2)
Thank you Uwe
Thanks for the post. I need a help.
ReplyDeleteI have a search window(modal panel), i need to fill in few details and based on that need to retrieve value in the data table available in the main screen.
I have a problem in sending the input values to the backing bean.
My modal dialog....
........
here using action param i tried assigning to the backing bean variable but i failed coz #{beanData.dataVO.searchCurrency} is returning null. how to set the form value in the actionparam tag value attribute? kindly help.
(I really need to fix that example program :/ )
ReplyDeleteWell first question of all: are you using a session scoped backing bean? I can't see your code because blogspot swallows it unfortunately, but even then there is only one real way to debug this stuff: trial and error.
An article I used as the start of my research into the modal panel is this one:
http://java.dzone.com/articles/an-introduction-to-jboss-richf
(yes, I even adopted the idea of user management for my own article - I have no imagination). It may help until I finally get around to attaching an example program for this stuff.
I also ran into the problem of the hidden field always getting set to false.
ReplyDeleteTried immediate="true" and that didn't work.
What worked: disabled="true".
The original value in the hidden field was getting put back. Setting it to disabled basically makes it a one way field, server->client.