Knowledge brings fear ... Prosthetic Conscious

Friday May 08, 2009

Suppose we have list of things, or a table of things, and a search box. The list should update based on what the user types. Additionally, the search should be live, in that the table updates as the user is typing; they are not required to hit enter, click a button, or otherwise take some action.

For the first attempt at this, let's add a valueChangeListener to your inputText component.

<ice:inputText
  value="#{aBean.searchFilter}"
  valueChangeListener=#{aHandler.search}/>

The problem is that this is not triggered until the form submits. You're going to need to add some Javascript to make this happen. For the next attempt, let's hook some Javascript into the component's keyup event,

<ice:inputText
  value="#{aBean.searchFilter}"
  onkeyup="doSubmit(this)"/>

 and,

function doSubmit(element) {
  iceSubmitPartial(
    document.getElementById("form"),
    element,
    MouseEvent.CLICK
  );
  setFocus(element);
}

Note that I'm using Icefaces, and making use of their utility Javascript function icePartialSubmit() to do a "partial submit". In a nutshell, this submits only this element and ignores the rest of the form. The call to setFocus() avoids losing focus in the input after the submission (another Icefaces utility).

This solution works fine, as long as the user is a slow typer. To understand what's wrong, suppose the user (quickly) types "java".

  1. user types "j"
  2. keyup occurs for "j"
  3. form is submitted
  4. bean is updated with value "j"
  5. user types "a"
  6. response is return from #3
  7. input is updated with value from backing bean: "j"

The response overwrites the "a" that the user just typed. What we want to do is add a delay in the form submission. If the keyup occurs, queue a form submission. If another keyup occurs, unqueue the first form submission and queue another, etc. It's set/cancelTimeout() to the rescure for this,

function doSubmit(element) {
        iceSubmitPartial(
            document.getElementById("form"),
            element,
            MouseEvent.CLICK
        );
        submitTimeout = null;
        setFocus(element);
}

function submitNow(element) {
    if (submitTimeout != null) {
        clearTimeout(submitTimeout);
    }
    submitTimeout = setTimeout(function(){doSubmit(element)}, 1000);
} 

The component binds keyup to submitNow(),

<ice:inputText
  value="#{aBean.searchFilter}"
  valueChangeListener=#{aHandler.search}
  onkeyup="submitNow(this)"/>

We use a delay of 1s. This still isn't perfect, because there's still a chance that the user will type something between a form submission and response. It is however much less likely considering average typing patterns.


Thursday May 07, 2009

In a recent entry, I described a mechanism for passing a JSF view ID as a GET parameter to force JSF to a particular view. This entry will improve on that by allowing a JSF action to be specified a a GET parameter.

After writing the previous entry, I started thinking ... JSF already has a mapping from action to view ID, why not make use of that to simplify the URL? Instead of:

/faces/home.xhtml?viewId=/faces/other.xhtml

we have this,

/faces/home/xhtml?jsf.action=success

So, from the current view of /faces/home.xhtml, and action success, go to the view defined in faces-config.xml's navigation rules. For example,

     <navigation-rule>
        <from-view-id>/faces/summary.xhtml
        <navigation-case>
            <from-outcome>success</from-outcome>
            <to-view-id>/admin/facelet/finished.xhtml</to-view-id>
        </navigation-case>
        ...
     ...

If our current view is /faces/summary.xhtml, and the jsf.action=success parameter is present, then move to view ID /facelet/finished.xhtml. This is rather slick as it makes use of existing rules; no additional configuration is required. Anyway, here's the code:

public class ActionPhaseListener implements PhaseListener {

    public RedirectPhaseListener() {
    }

    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    public void beforePhase(PhaseEvent phaseEvent) {
    }

    public void afterPhase(PhaseEvent phaseEvent) {
        if (navigationRules == null) {
            navigationRules = new NavigationRules();
        }

        FacesContext ctx = phaseEvent.getFacesContext();
        HttpServletRequest request =
                (HttpServletRequest) ctx.getExternalContext().getRequest();

        String action = request.getParameter("jsf.action");

        if (action != null) {
            String currentViewId = ctx.getViewRoot().getViewId();
            NavigationRule nr = navigationRules.getNavigationRules().get(currentViewId);
            if (nr == null) {
                nr = navigationRules.getNavigationRules().get(null);
            }
            NavigationCase nc = nr.getNavigationCases().get(action);
            if (nc != null) {
                String newViewId = nc.getToViewId();
                UIViewRoot page = ctx.getApplication().getViewHandler().createView(ctx, newViewId);
                ctx.setViewRoot(page);
                ctx.renderResponse();
            }
        }
    }

    private NavigationRules navigationRules = null;
} 

in the interest of not boring you, NavigationRules.java is linked. It reads faces-config.xml by using ServletContext.getResource() and parses it into bean-like objects using JDOM.

Alain commented on my previous post that the solution there was problematic as it allowed access to arbitrary view which may depend on objects being set up in other views (think of a work flow / wizard). This solution has no such problem, as you can only navigate according to the rules you've already defined in faces-config.xml.



Tuesday May 05, 2009

Every wanted to access a JSF view by directly entering the view onto URL? This is a common request. In my case, I needed to link to specific places in my JSF app from a legacy non-JSF application. This is the type of thing you'd think would be straightforward, but JSF falls flat.

I scoured the web for solutions to this, and this is the simplest: implement a phase listener. In the interest of getting right to it, here it is:

public class RedirectPhaseListener implements PhaseListener {

    public RedirectPhaseListener() {
    }

    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    public void afterPhase(PhaseEvent phaseEvent) {
    }

    public void beforePhase(PhaseEvent phaseEvent) {
        FacesContext ctx = phaseEvent.getFacesContext();
        HttpServletRequest request =
                (HttpServletRequest) ctx.getExternalContext().getRequest();

        String viewId = request.getParameter("viewId");

        if (viewId != null) {
            UIViewRoot page = ctx.getApplication().getViewHandler().createView(ctx, viewId);
            ctx.setViewRoot(page);
            ctx.renderResponse();

        }
    }
} 

The phase listener gets the request, and checks for a parameter. The parameter is the path to the view ID you want to visit. For example: /faces/someview.xhtml. With Facelets, these goto URLs end up looking funny, because the real view ID is in the URL also,

 /faces/home.xhtml?viewId=/faces/other.xhtml

Not nice, but it works. Other solutions I've seen try to get fancy by keeping a mapping a view+action result=new view. That's cleaner, and it's easy to do once you understand what's going on above.