Skip to main content

Another JSF 2.0 Ajax Component: Editable Text

Posted by driscoll on November 23, 2008 at 5:45 PM PST

I was sitting in at a talk on Ajax components the other day, and they mentioned the Flickr style editable text. For those who've never experienced the Ajax joy that is Flickr, it's a web based photography site. When viewing your own pictures, text such as the titles of your photos appears just as plain text, just as it does for other people's photos. The difference is that when you hover the mouse pointer over the text, the background turns yellow, and "Click to Edit" appears as a tooltip. When you click, the text turns into an input field of type text, with a submit and cancel button appearing to the right.


I thought - I wonder how hard that would be as a component in JSF 2.0? The answer: Not too bad, about 150 lines of code. Here it is. (Note: if you haven't yet read my previous examples, you may want to do that first - I'm assuming that you know a little bit about how this all fits together.)


We'll create a component with some Ajax wiring and a little css. It will have two panels, which we'll switch back and forth between, and a couple buttons. First, here's how it'll appear in the using page:



<ez:editText id="editText1" value="#{stringholder.str}"/>


Simple enough - a simple tag that takes a valueexpression in EL.


(Update: Acting on a reader's suggestion, I've put some images of the component in action at the bottom of the post.)


Now, we'll look at the component.



<composite:interface name="editText"
                     displayName="Editable Text Component"
                     shortDescription="Editable Text Component">
    <composite:attribute name="value" required="true" type="String"/>
</composite:interface>

<composite:implementation>
    <h:outputScript name="jsf.js" library="javax.faces" target="head"/>
    <h:outputScript name="editText/editText.js" target="head" />
    <script type="text/javascript">
        init("#{cc.clientId}", "#{cc.attrs.value}");
    </script>
    <h:outputStylesheet name="editText/styles.css"/>
    <h:panelGroup id="edit1">
        <h:outputLink id="editLink" title="Click to edit" styleClass="editLink"
                      onclick="return linkClick('#{cc.clientId}');"
                >#{cc.attrs.value}</h:outputLink>
    </h:panelGroup>
    <h:panelGroup id="edit2" style="display:none;">
        <h:inputText id="editInput" value="#{cc.attrs.value}" styleClass="editInput"/>
        <h:commandButton value="Submit" id="submit"
                         onclick="return submitButton('#{cc.clientId}', event);"/>
        <h:commandButton value="Cancel" id="cancel"
                         onclick="return cancelButton('#{cc.clientId}');"/>
    </h:panelGroup>
</composite:implementation>


There's a lot to look at here, but it's acutally not anything we haven't done before, we'll go through it line by line, looking at the bolded parts, which are the most important bits.


In the composite:interface section, we're defining a required attribute, called "value", which we already saw in the using page. We use it later in the places where we reference it as #{cc.attrs.value}. This, at least, should be straightforward by now.


We use three external files - the JSF 2.0 Ajax library, our own JavaScript file, and a CSS file. The three lines with outputScript and outputStyleSheet take care of that for us.


We also need to record what our client ID is, as we saw with my final Spinner example. We use the JavaScript init method to record that (we'll look at the JavaScript next, be patient).


As I mentioned above, we're using two panels (edit1 and edit2) - and we'll switch between them. Edit2 starts off hidden (display: none;), so at first, this just looks like text. Edit1, however, is a link - we'll leverage the hover capabilities of links to generate our change in background color and to get the tooltip I mentioned at the start of the example.


For all the active components, we're wiring an onlick for them, which calls out to releavant JavaScript - they'll also return false, since that's required for us to *not* actually submit the entire form, but just the contents of the value expression instead.


So, having gone through all this, we'll look at the JavaScript - it's longer than our previous examples, but it's really not doing anything we haven't covered before.



if (!window["edittextdemo"]) {
    var edittextdemo = {};
}
function init(componentID, initialValue) {
    edittextdemo[componentID] = initialValue;
}
function toggle(idOn, idOff) {
    try {
        var elementon = document.getElementById(idOn);
        var elementoff = document.getElementById(idOff);
        elementon.style.display = "inline";
        elementoff.style.display = "none";
    } catch (ex) {
        alert(ex);
    }
}
function submitButton(componentID, event) {
    try {
        var edit1 = componentID + ':edit1';
        var edit2 = componentID + ':edit2';
        toggle(edit1, edit2);

        var link = componentID + ':editLink';
        var input = componentID + ':editInput';
        var subButton = componentID + ':submit';
        var exec = subButton + ' ' + input;
        var rend = input + ' ' + link;
        jsf.ajax.request(document.getElementById(subButton), event, {execute: exec, render: rend});
        edittextdemo[componentID] = document.getElementById(input).value;
    } catch (ex) {
        alert(ex);
    }
    return false;
}
function cancelButton(componentID) {
    try {
        var edit1 = componentID + ':edit1';
        var edit2 = componentID + ':edit2';
        toggle(edit1, edit2);
        var input = componentID + ':editInput';
        document.getElementById(input).value = edittextdemo[componentID];
    } catch (ex) {
        alert(ex);
    }
    return false;
}
function linkClick(componentID) {
    try {
        var edit1 = componentID + ':edit1';
        var edit2 = componentID + ':edit2';
        var editInput = componentID + ':editInput';
        toggle(edit2, edit1);
        document.getElementById(editInput).focus();
    } catch (ex) {
        alert(ex);
    }
    return false;
}


57 lines of code: Let's go through it by function.


We start with creating an object if it doesn't already exist. Why do this? To allow multiple components in a page. We're going to use that object to save the component ID, which will change with every use.


init just saves the component ID, and associates it with the value that was set as the initial value. We're saving that value for the cancelButton function, below.


toggle flips back in forth between our two panels, so you only see one at a time.


Next, we have the three functions that are called when user action happens. linkClick toggles the panels, and sets the focus on the newly revealed input text field.


The submitButton function toggles the panels, showing the link again, and then submits an ajaxRequest, executing the submit button (which is actually a no-op), and the input field, and rendering the input field and the link, which saves the new value to the bean. We also save the new value into a string in JavaScript, for use in restoring in case we choose cancel.


The cancelButton function again toggles the panels, showing the link, and restores the input field's value to the last saved value, effectively syncing it up with the value on the server, without doing a request.


So that's the JavaScript. Hopefully, you're with me so far. Now we'll take a peek at the CSS markup that makes it look like editable text.



.editClass {
    font-size: medium;
    font-style: normal;
    font-family: Arial, sans-serif;
}
.editLink {
    font-size: medium;
    font-style: normal;
    font-family: Arial, sans-serif;
}
a.editLink:link {text-decoration: none}
a.editLink:visited {text-decoration: none}
a.editLink:active {text-decoration: none}
a.editLink:hover {text-decoration: none; background-color: #eeee00;}


Not much going on here: Mostly, we're just changing the look of the link.


So, that's editable text. Is this a finished component? No, not really - we would still need to add css markup to give a constant width to the component. We need to add some JavaScript to allow displaying a link with no characters (the current implementation gets into an uneditable state if you change the string length to zero). But all that's not specific to JSF 2.0. There's also the need to add a waiting indicator - the traditional spinning wheel in case there's an update pause after you hit submit. And oh, does this ever need error handling. Unfortunately, error handling and event processing are still undefined in JSF 2.0 - so that's going to have to be a subject for another post.


To see the full source for this example, you can look under the Mojarra sourcebase. You can run it using GlassFish v3 Prelude, by updating to the JSF 2.0 API using the Update Center (make sure you're up to date with Build 5 - this is cutting edge stuff).


As always, leave your questions below, and I'll answer if I'm able. Next up - we'll wire buttons to method actions in a composite component, for both Ajax and non-ajax use cases.


Update:
Per Request, here's some images which show this tag in action.


Inital state:

editableText1.gif


On mouseover:

editableText2.gif


On onclick:

editableText3.gif


Data Entry:

editableText4.gif


Submit:

editableText5.gif


Hopefully, that makes it easier to visualize what's happening. I'll include pictures from now on, thanks for the suggestion.


Update: I've updated this example to be more clear, and to conform with the latest JSF 2.0 PR release.
Update 2: I've again updated this example to conform to the JSF 2.0 FCS Spec.

Related Topics >>

Comments

The selector stuff in jQuery, along with all the other *client side* functionality, should work fine. But if you're doing Ajax requests back to the server, you'll probably want to use the JSF supplied functions, since those keep track of the page state correctly for JSF, while the jQuery (and Dojo, and Prototype) ajax functions do not. It will be possible to hand wrap all the JSF stuff, but it's probably more trouble than it's worth. But I'm hoping to get to examples using stuff like jQuery in the next month or two.

If only because I've been working a good deal with jQuery, my first impression is that a good deal of this could be simplified using something like jQuery. Removing the click handlers would make the XHTML much cleaner, and the JS would be greatly simplified. I'd be curious what your thoughts would be. Again, this is first impression only, I haven't spent a good deal of time thinking about the repercussions.

Updated as requested... The images are hastily captured, I'll try to make them prettier and more succinct next time. A live example would require us to run a publicly available server - that's more bother than we can currently bear, since we're busy writing code on weekends right now. That said, we will have a live example page at sometime in the near future, probably after beta, when we have weekends again.

I've been following your last few posts on JSF 2.0 components. It would be nice if you could include a screenshot of what the component looks like or a link to a live example. Thanks