Extending PrimeFaces components

User Rating: 4 / 5

Star ActiveStar ActiveStar ActiveStar ActiveStar Inactive
 

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 “resolveWidgetVar”. 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 it’s created by Jonh Resig author of the book “Secrets of the JavaScript Ninja”. In this post there’s a detailed explanation of the logic behind this pattern.

Let’s create “PrimeFaces.widget.AnalogClock” that will extend the BaseWidget. We just have to recall to invoke the “super” init 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 it’s not the focus of this article. The only thing that we need to know it’s that it uses the JavaScript library Raphaël to draw a clock via HTML5 canvas. This function is a patchwork of various works I’ve found on the internet. It’s 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 PrimeFaces’s CoreRenderer, so that writing a component to the response it’s 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

 

Conclusions

I think that this example will prove that PrimeFaces it’s not just a very advanced component library, but also an API to build your custom components. This is the first one on a series of posts on how to create a complete custom library with PrimeFaces. The second “episode” talks about input components and it’s available here on my personal blog.

You can see the source code of this example on my GitHub. If you want to take a peek at the complete version of the AnalogClock you can go here on the PrimeFaces Extension repository.

Author:

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


Advertisement