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 posite ponent 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"
".dtd">
<html xmlns=""
xmlns:h=""
xmlns:f=""
xmlns:posite="">
<posite:interface>
<posite:attribute name="start" />
</posite:interface>
<posite: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 < 1) {
minutes = "00";
} else if (minutes < 10) {
minutes = "0" + minutes;
}
if (seconds < 1) {
seconds = "00";
} else if (seconds < 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>
</posite: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 posite?
Many thanks!
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 posite ponent 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/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3/1999/xhtml"
xmlns:h="http://java.sun./jsf/html"
xmlns:f="http://java.sun./jsf/core"
xmlns:posite="http://java.sun./jsf/posite">
<posite:interface>
<posite:attribute name="start" />
</posite:interface>
<posite: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 < 1) {
minutes = "00";
} else if (minutes < 10) {
minutes = "0" + minutes;
}
if (seconds < 1) {
seconds = "00";
} else if (seconds < 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>
</posite: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 posite?
Many thanks!
Share Improve this question asked Apr 9, 2017 at 8:40 ToastorToastor 8,9904 gold badges51 silver badges82 bronze badges 2- 1 First of all, this is not 'posite' 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' es frome. And you know this: timeago.yarp. (no need to build it yourself) – Kukeltje Commented Apr 10, 2017 at 8:38
- @Kukeltje: Thanks! My problem was that the value based on which the passed time should be calculated is brought into the ponent via an attribute. Because I was unable to read its value directly, I bound the outputText ponent 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, Toastor – Toastor Commented Apr 10, 2017 at 9:09
3 Answers
Reset to default 6You can avoid JS function collision by only rendering the function the first time the ponent 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 acplish this using the ponentType
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 posite
<html xmlns="http://www.w3/1999/xhtml"
xmlns:ui="http://java.sun./jsf/facelets"
xmlns:h="http://java.sun./jsf/html"
xmlns:f="http://java.sun./jsf/core"
xmlns:cc="http://java.sun./jsf/posite">
<cc:interface ponentType="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);
}
}
As ForguesR already pointed out, you could just use the timeago jQuery plugin.
I took the plugin and wrapped it into a JSF ponent. Simply add this dependency:
<dependency>
<groupId>.github.jepsar</groupId>
<artifactId>timeago-jsf</artifactId>
<version>1.0</version>
</dependency>
This namespace:
xmlns:ta="http://jepsar/timeago"
Now you can use the ponent by passing a java.util.Date
using the value
attribute:
<ta:timeAgo value="#{bean.date}" />
jQuery is loaded from PrimeFaces, BootsFaces or the timeago ponent.
Localization script is automatically loaded based on on the JSF UIViewRoot#getLocale()
.
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./. 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>