Extending PrimeFaces components

 

The purpose of this post is to show how we can use PrimeFaces base classes to create new custom components. In a nutshell we will see how to extend PrimeFaces. We will do that by developing a simplified version of the PrimeFaces Extensions Analogclock.

Let’s start with the component class:

@FacesComponent(value = AnalogClock.COMPONENT_TYPE)
@ResourceDependencies({
		@ResourceDependency(library = "moment", name = "moment.min.js"),
		@ResourceDependency(library = "primefaces", name = "jquery/jquery.js"),
		@ResourceDependency(library = "raphael", name = "raphael-min.js"),
		@ResourceDependency(library = "primefaces", name = "primefaces.js"),
		@ResourceDependency(library = "strazzfaces", name = "analog-clock.js") })
public class AnalogClock extends UIComponentBase implements Widget {


	public static final String COMPONENT_TYPE = "it.strazz.faces.AnalogClock";
	public static final String COMPONENT_FAMILY = "it.strazz.faces.components";


	public String getFamily() {
		return COMPONENT_FAMILY;
	}


	public void setStartTime(Date _pattern) {
		getStateHelper().put(PropertyKeys.startTime, _pattern);
	}


	public Date getStartTime() {
		return (Date) getStateHelper().eval(PropertyKeys.startTime, new Date());
	}


	public String getMode() {
		return (java.lang.String) getStateHelper().eval(PropertyKeys.mode,"client");
	}


	public void setMode(String _mode) {
		getStateHelper().put(PropertyKeys.mode, _mode);
	}


	public Integer getWidth(){
		return (Integer) this.getStateHelper().eval(PropertyKeys.width,null);
	}


	public void setWidth(Integer width){
		this.getStateHelper().put(PropertyKeys.width, width);
	}


	public String getWidgetVar() {
		return (String) getStateHelper().eval(PropertyKeys.widgetVar, null);
	}


	public void setWidgetVar(String _widgetVar) {
		getStateHelper().put(PropertyKeys.widgetVar, _widgetVar);
	}


	public String resolveWidgetVar() {
		FacesContext context = getFacesContext();
		String userWidgetVar = (String) getAttributes().get("widgetVar");


		if (userWidgetVar != null)
			return userWidgetVar;
		else
			return "widget_"
					+ getClientId(context).replaceAll(
							"-|" + UINamingContainer.getSeparatorChar(context),
							"_");
	}


	protected static enum PropertyKeys {
		width, widgetVar, startTime, mode;
	}
}

The most interesting thing in this component is to be a Widget. All the components that implements this interface will have a JavaScript counterpart. Basically for every AnalogClock instance on the server, a mirror object will exists on the client browser. The only method we need to implement to use this interface is solveWidgetVar. Its purpose is to return the widgetVar of the component, namely the name of the JavaScript variable assigned to our component.

Analyzing the code above you can notice that if you did not manually set a widgetVar to the component (invoking the setWidgetVar method), the value is automatically generated using the clientId. Some of the others attribute of the AnalogClock are:

Name

Description

width

width of the clock in pixel, set ‘auto’ to adjust to the width of the container. The default value is ‘auto’.

mode

‘server’ for using server time, use the client time otherwise.

startTime

time to display when the mode is €˜server€™, the default value is current time.

BaseWidget

One of the ResourceDependencies of AnalogClock it’s the file analog-clock.js. This is a resource file and it must be placed in the resources folder of our project, just like in the next picture.

The file must contain the JavaScript definition of the AnalogClock widget. On the client every widget will be instantiated through a constructor function that extends PrimeFaces.widget.BaseWidget.

PrimeFaces.widget.BaseWidget = Class.extend({ 


        init: function(cfg) {
            this.cfg = cfg;
            this.id = cfg.id;
            this.jqId = PrimeFaces.escapeClientId(this.id);
            this.jq = $(this.jqId);


            .....
        },


        ......
    });

The heart of this function is the init method, here a configuration obect and a reference of the main DOM element of the component are memorized. The Class function its created by Jonh Resig author of the book Secrets of the JavaScript Ninja. In this post theres a detailed explanation of the logic behind this pattern.

Lets create PrimeFaces.widget.AnalogClockthat will extend the BaseWidget. We just have to recall to invoke the superinit manually.

PrimeFaces.widget.AnalogClock = PrimeFaces.widget.BaseWidget.extend({
			init : function(cfg) {


				this._super(cfg);


				this.startTime = cfg.time && cfg.mode !== 'client' ? moment(cfg.time) : moment();


				this.colorScheme = cfg.colorScheme || 'standard';


				this.dimensions = new PrimeFaces.widget.AnalogClock.Dimensions(this.cfg.width || this.jq.width());


				this.interval = setInterval((function(self) {
					return function() {
						self.update();
					}
				})(this), 1000);


				this.draw();
			},


			draw : function() {


				this.canvas = Raphael(this.id, this.dimensions.size, this.dimensions.size);


				this.clock = this.canvas.circle(this.dimensions.half, this.dimensions.half, this.dimensions.clock_width);
				this.clock.attr({
					"fill" : PrimeFaces.widget.AnalogClock.colorSchemes[this.colorScheme].face,
					"stroke" :PrimeFaces.widget.AnalogClock.colorSchemes[this.colorScheme].border,
					"stroke-width" : "5"
				})


				this.draw_hour_signs();


				this.draw_hands();


				var pin = this.canvas.circle(this.dimensions.half, this.dimensions.half, this.dimensions.pin_width);
				pin.attr("fill", PrimeFaces.widget.AnalogClock.colorSchemes[this.colorScheme].pin);


				this.update();
			},


			draw_hour_signs: function(){
				var hour_sign;


				for (i = 0; i < 12; i++) {


					var start_x = this.dimensions.half + Math.round(this.dimensions.hour_sign_min_size * Math.cos(30 * i	* Math.PI / 180));
					var start_y = this.dimensions.half + Math.round(this.dimensions.hour_sign_min_size * Math.sin(30 * i * Math.PI / 180));
					var end_x = this.dimensions.half + Math.round(this.dimensions.hour_sign_max_size * Math.cos(30 * i * Math.PI	/ 180));
					var end_y = this.dimensions.half + Math.round(this.dimensions.hour_sign_max_size * Math.sin(30 * i * Math.PI	/ 180));


					hour_sign = this.canvas.path("M" + start_x + " " + start_y	+ "L" + end_x + " " + end_y);
					hour_sign.attr({
						"stroke":PrimeFaces.widget.AnalogClock.colorSchemes[this.colorScheme].hour_signs,
						"stroke-width" : this.dimensions.hour_sign_stroke_width
					});
				}
			},


			draw_hands: function(){


				this.hour_hand = this.canvas.path("M" + this.dimensions.half + " " + this.dimensions.half	+ "L" + this.dimensions.half + " " + this.dimensions.hour_hand_start_position);
				this.hour_hand.attr({
					stroke : PrimeFaces.widget.AnalogClock.colorSchemes[this.colorScheme].hour_hand,
					"stroke-width" : this.dimensions.hour_hand_stroke_width
				});


				this.minute_hand = this.canvas.path("M" + this.dimensions.half + " " + this.dimensions.half	+ "L" + this.dimensions.half + " " + this.dimensions.minute_hand_start_position);
				this.minute_hand.attr({
					stroke : PrimeFaces.widget.AnalogClock.colorSchemes[this.colorScheme].minute_hand,
					"stroke-width" : this.dimensions.minute_hand_stroke_width
				});


				this.second_hand = this.canvas.path("M" + this.dimensions.half + " " + this.dimensions.half	+ "L" + this.dimensions.half + " " + this.dimensions.second_hand_start_position);
				this.second_hand.attr({
					stroke : PrimeFaces.widget.AnalogClock.colorSchemes[this.colorScheme].second_hand,
					"stroke-width" : this.dimensions.second_hand_stroke_width
				});
			},


			update : function() {
				var now = this.startTime;


				this.hour_hand.rotate(30 * now.hours() + (this.startTime.minutes() / 2.5), this.dimensions.half, this.dimensions.half);
				this.minute_hand.rotate(6 * this.startTime.minutes(), this.dimensions.half, this.dimensions.half);
				this.second_hand.rotate(6 * this.startTime.seconds(), this.dimensions.half, this.dimensions.half);


				this.startTime.add('s', 1);
			}


		});

We will omit the draw function because its not the focus of this article. The only thing that we need to know its that it uses the JavaScript library Raphal to draw a clock via HTML5 canvas. This function is a patchwork of various works Ive found on the internet. Its not very complex, you only need to know a little trigonometry. The cfg parameter holds all the parameters of our AnalogClock component, this attributes are used during the draw phase.

Renderer

The purpose of the Renderer is to link the AnalogClock instance on the server with the JavaScript brother on the client. PrimeFaces API make this operation very easy using the WidgetBuilder. This class handles to build and configure a new widget.

@FacesRenderer(componentFamily = AnalogClock.COMPONENT_FAMILY, rendererType = AnalogClockRenderer.RENDERER_TYPE)
public class AnalogClockRenderer extends CoreRenderer {


	public static final String RENDERER_TYPE = "it.strazz.faces.AnalogClockRenderer";


	public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
		AnalogClock analogClock = (AnalogClock) component;


		encodeMarkup(context, analogClock);
		encodeScript(context, analogClock);
	}


	protected void encodeMarkup(FacesContext context, AnalogClock clock) throws IOException {
		ResponseWriter writer = context.getResponseWriter();


		writer.startElement("div", clock);
		writer.writeAttribute("id", clock.getClientId(), null);
		writer.endElement("div");
	}


	protected void encodeScript(FacesContext context, AnalogClock analogClock) throws IOException {


		String clientId = analogClock.getClientId();
		String widgetVar = analogClock.resolveWidgetVar();


		WidgetBuilder wb = getWidgetBuilder(context);


		wb.init("AnalogClock", widgetVar, clientId);
		wb.attr("mode", analogClock.getMode());
		wb.attr("time",	analogClock.getStartTime() != null ? analogClock.getStartTime().getTime() : null);


		if(analogClock.getWidth() != null){
			wb.attr("width", analogClock.getWidth());
		}


		wb.finish();
	}
}

As you can see our Renderer extends the PrimeFacess CoreRenderer, so that writing a component to the response its enormously simplified task. In this way our code will remain extremely maintainabile.

Example

We can now try our component. In the next example we will create four AnalogClocks: three of the them will display a server time, the last one the client time.

XHTML Page:

<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:ui="http://java.sun.com/jsf/facelets"
	xmlns:p="http://primefaces.org/ui"
	xmlns:s="http://it.strazz.faces/ui">
<h:head>
</h:head>
<h:body>
	<h1>StrazzFaces Clock</h1>
	<h:form>
		<p:panelGrid columns="4">
			<h:panelGroup>
				<h1>Rome</h1>
				<s:analogClock
					startTime="#{clockBean.romeTime}"
					width="250"
					mode="server"/>
			</h:panelGroup>
			<h:panelGroup>
				<h1>London</h1>
				<s:analogClock
					startTime="#{clockBean.londonTime}"
					width="250"
					mode="server"/>
			</h:panelGroup>
			<h:panelGroup>
				<h1>New York</h1>
				<s:analogClock
					startTime="#{clockBean.newYorkTime}"
					width="250"
					mode="server"/>
			</h:panelGroup>
			<h:panelGroup>
				<h1>You</h1>
				<s:analogClock width="250" />
			</h:panelGroup>
		</p:panelGrid>
	</h:form>
</h:body>
</html>

Managed Bean:

@ManagedBean
@RequestScoped
public class ClockBean implements Serializable {


	private static final long serialVersionUID = 1L;


	private Date romeTime;
	private Date londonTime;
	private Date newYorkTime;


	@PostConstruct
	public void loadTimes(){
		romeTime = new Date();


		Calendar c = Calendar.getInstance();


		c.setTime(romeTime);
		c.add(Calendar.HOUR, -1);
		londonTime = c.getTime();


		c.setTime(romeTime);
		c.add(Calendar.HOUR, -5);
		newYorkTime = c.getTime();
	}


	public Date getRomeTime() {
		return romeTime;
	}


	public Date getLondonTime(){
		return londonTime;
	}


	public Date getNewYorkTime(){
		return newYorkTime;
	}


}

extend primefaces

 

Author:

Francesco Strazzullo – software architect and one of the committer in the Primefaces Extensions project.

Found the article helpful? if so please follow us on Socials