2
votes

For one of my JSF / primefaces projects, I want to display the elapsed time since a given date - think "this entry has been last edited 3h 5m ago".

At first, I calculated the time interval in the backing bean and let the view poll for it. That meant one ajax call per second and would also break easily - not good.

So I made my first simple JSF composite component for the task. Basically it is just a wrapper around h:outputText: it takes the start date as an attribute and then in Javascript it calculates the time interval to the present date every second and updates the outputText accordingly.

Here's the code:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:composite="http://java.sun.com/jsf/composite">

    <composite:interface>

        <composite:attribute name="start" />

    </composite:interface>

    <composite:implementation>

        <div id="#{cc.clientId}" class="elapsedTime">
            <h:outputText id="outTxt" value="#{cc.attrs.start}" />
        </div>

        <script type="text/javascript">
            var outTxt = document.getElementById("#{cc.clientId}:outTxt");
            var a = outTxt.innerHTML.split(/[^0-9]/);
            var baseDate = new Date(a[0], a[1] - 1, a[2], a[3], a[4], a[5]);

            function timeStringFromSeconds(s)
            {
                var hours = Math.floor((s / 86400) * 24);
                var minutes = Math.floor(((s / 3600) % 1) * 60);
                var seconds = Math.round(((s / 60) % 1) * 60);

                if (minutes &lt; 1) {
                    minutes = "00";
                } else if (minutes &lt; 10) {
                    minutes = "0" + minutes;
                }

                if (seconds &lt; 1) {
                    seconds = "00";
                } else if (seconds &lt; 10) {
                    seconds = "0" + minutes;
                }

                return(hours + ":" + minutes + ":" + seconds);
            }

            function update() {
                var currentDate = new Date();
                var elapsed = (currentDate - baseDate) / 1000;
                outTxt.innerHTML = timeStringFromSeconds(elapsed);
            }

            update();
            setInterval(update, 1000);
        </script>

    </composite:implementation>

</html>

This is working as expected. However, since I was unable to retrieve the start attribute value directly from JS, I let the h:outputText display the date value first and then JS will retrieve it from the rendered HTML and replace it with the elapsed time.

Therefore, although I update the value right away, on some browsers / devices the original date value is briefly visible. The whole thing feels like an ugly workaround to me. And if for some reason I would like to use a second attribute, I wouldn't be able to use it at all, so the approach is clearly limited / broken.

So my question is: Is there a cleaner way to do this, for example by directly accessing attributes (if possible)?

Or ist this simply something you can't do in a composite?

Many thanks!

3
First of all, this is not 'composite' related. Secondly, what do you mean by "However, since I was unable to retrieve the start attribute value directly from JS," The solution you have is not wrong, it is how it is normally done. Try to find out where the 'delay' comes frome. And you know this: timeago.yarp.com (no need to build it yourself)Kukeltje
@Kukeltje: Thanks! My problem was that the value based on which the passed time should be calculated is brought into the component via an attribute. Because I was unable to read its value directly, I bound the outputText component to the attribute and then picked the value from HTML. In the meantime I found out that you can just use EL within Javascript as well - which is the direct access I wanted, see my answer. Thanks for mentioning timeago, I'll have a look at it! Best regards, ToastorToastor

3 Answers

4
votes

You can avoid JS function collision by only rendering the function the first time the component is encountered. If you're willing to externalize your JS you can contain everything in your xhtml. See this post by BalusC

<cc:interface/>

<cc:implementation>
    <h:outputScript target="head" name="js/myJs.js"/>
    <div id="#{cc.clientId}">
        <h:outputText id="output" value="#{cc.attrs.value}" />
        <script>
            update("#{cc.clientId}:output");
            setInterval(update, 1000, "#{cc.clientId}:output");
        </script>
    </div>
</cc:implementation>

Alternatively you can accomplish this using the componentType attribute of <cc:interface/> and then annotating a Java Class with @FacesComponent(name=... annotation. In your Java Class you can then override encodeBegin to write your JS to the response if you haven't yet. Here's the composite

<html 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:cc="http://java.sun.com/jsf/composite">

<cc:interface componentType="jsTestComp" />

<cc:implementation>
    <div id="#{cc.clientId}">
        <h:outputText id="output" value="#{cc.attrs.value}" />
        <script>
            update("#{cc.clientId}:output");
            setInterval(update, 1000, "#{cc.clientId}:output");
        </script>
    </div>
</cc:implementation>
</html>

And the Java code

@FacesComponent(value="jsTestComp")
public class JSTestComp extends UINamingContainer {
    private static final String JS_RENDERED_KEY = "JSTestComp.jsRendered";
    private static final String js = 
        "function update(elementId){\n" +
        "    var outTxt = document.getElementById(elementId);\n" +                  
        "    outTxt.innerHTML = outTxt.innerHTML + \"more\";\n" +
        "}\n";

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        Map<String, Object> reqMap = FacesContext.getCurrentInstance()
                .getExternalContext().getRequestMap();

        if(reqMap.get(JS_RENDERED_KEY) == null){
            reqMap.put(JS_RENDERED_KEY, Boolean.TRUE);
            ResponseWriter out = FacesContext.getCurrentInstance().getResponseWriter();
            out.startElement("script", null);
            out.write(js);
            out.endElement("script");
        }
        super.encodeBegin(context);
    }
}
2
votes

As ForguesR already pointed out, you could just use the timeago jQuery plugin.

I took the plugin and wrapped it into a JSF component. Simply add this dependency:

<dependency>
  <groupId>com.github.jepsar</groupId>
  <artifactId>timeago-jsf</artifactId>
  <version>1.0</version>
</dependency>

This namespace:

xmlns:ta="http://jepsar.org/timeago"

Now you can use the component by passing a java.util.Date using the value attribute:

<ta:timeAgo value="#{bean.date}" />

jQuery is loaded from PrimeFaces, BootsFaces or the timeago component.

Localization script is automatically loaded based on on the JSF UIViewRoot#getLocale().

1
votes

In fact this is not a JSF problem. You just need to retrieve the initial value with JSF and pass it on to the script. Instead of reinventing the wheel have a look at http://timeago.yarp.com/. Here is a simple example from the demo page :

<time class="timeago" datetime="2008-07-17T09:24:17Z">July 17, 2008</time>

This will output : 9 years ago.

Like axemoi suggested, pass the value as a parameter. Something like :

Last modified : <time class="loaded timeago" datetime="#{YourBean.lastEdited}">#{YourBean.lastEdited}</time>