The goal
There is a lot of good introductory tutorials on writing JSF components based on RichFaces and using CDK. However they cover very simple cases and developers have hard times figuring out how to tackle some more advanced scenarios which are easily achievable in RichFaces.About the component
In this tutorial we will create schedule component similar to i.e. Google Calendar. Front-end, meaning JavaScript code, is already there, and it’s so cool. It’s called fullcalendar and it’s a jQuery plugin written by Adam Shaw (http://arshaw.com/fullcalendar/). Actually most of components I’ve created is based on some existing jQuery plugins.<schedule:schedule date="#{myBean.initialDate}" styleclass="slim" switchtype="server" value="#{myBean.lazyDataModel}" var="event"> <schedule:scheduleitem allday="#{event.allDay}" data="#{event.data}" enddate="#{event.endDate}" eventid="#{event.id}" startdate="#{event.startDate}" styleclass="#{event.id == 1 ? 'first' : null}" title="#{event.title}"/> </schedule:schedule>The widget is a jQuery plugin. It depends on following files:
- fullcalendar.css
- fullcalendar.js
- ui.core.js
- ui.draggable.js
- ui.resizable.js
- jquery.js
<div id="container"/> <script> jQuery("#container").fullCalendar(options); </script>Items (events in Adam's terminology) that are rendered can be provided in several ways:
- array of objects within options (JSON)
- callback function
- Google Calendar feed URL
Project setup
As for most RF projects we will use Maven. Our project will consist of 3 modules: parent, ui and demo. UI Is the actual component, demo is an application for testing the component manually and parent contains some commond configuration. The directory structure looks like this:- schedule
- demo
- pom.xml
- ui
- pom.xml
- pom.xml
- demo
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.richfaces</groupId> <artifactId>richfaces-root-parent</artifactId> <version>4.2.1-SNAPSHOT</version> </parent> <groupId>org.richfaces.sandbox.ui.schedule</groupId> <artifactId>schedule-parent</artifactId> <packaging>pom</packaging> <name>Richfaces UI Components: schedule parent</name> <modules> <module>ui</module> <module>demo</module> </modules> </project>
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.richfaces.sandbox.ui.schedule</groupId> <artifactId>schedule-parent</artifactId> <version>4.2.1-SNAPSHOT</version> </parent> <artifactId>schedule-ui</artifactId> <name>Richfaces UI Components: schedule ui</name> <dependencies> <dependency> <groupId>org.richfaces.ui.core</groupId> <artifactId>richfaces-ui-core-ui</artifactId> <version>${project.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.richfaces.core</groupId> <artifactId>richfaces-core-impl</artifactId> <version>${project.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.richfaces.cdk</groupId> <artifactId>annotations</artifactId> <version>${project.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <scope>provided</scope> <version>1.2</version> </dependency> <dependency> <groupId>javax.el</groupId> <artifactId>el-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.jboss.test-jsf</groupId> <artifactId>jsf-test-stage</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.richfaces.cdk</groupId> <artifactId>maven-cdk-plugin</artifactId> <version>${project.version}</version> <executions> <execution> <id>cdk-generate-sources</id> <phase>generate-sources</phase> <goals> <goal>generate</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.richfaces.sandbox.ui.schedule</groupId> <artifactId>schedule-parent</artifactId> <version>4.2.1-SNAPSHOT</version> </parent> <artifactId>schedule-demo</artifactId> <name>Richfaces UI Components: schedule demo</name> <packaging>war</packaging> <build> <finalName>schedule-demo</finalName> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-deploy-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.richfaces.ui</groupId> <artifactId>richfaces-components-ui</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.richfaces.core</groupId> <artifactId>richfaces-core-impl</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.richfaces.sandbox.ui.schedule</groupId> <artifactId>schedule-ui</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>com.sun.faces</groupId> <artifactId>jsf-api</artifactId> <scope>compile</scope> </dependency> <dependency> <groupId>com.sun.faces</groupId> <artifactId>jsf-impl</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.sun.el</groupId> <artifactId>el-ri</artifactId> <version>1.0</version> </dependency> </dependencies> </project>When you run mvn install in schedule directory maven will fetch about 50MB of dependencies and then will build both ui and demo. Please note that some of required artifacts are hosted on JBoss repository so you will need to add it to your maven configuration.
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <localRepository/> <interactiveMode/> <usePluginRegistry/> <offline/> <pluginGroups/> <servers/> <profiles> <profile> <id>jboss</id> <repositories> <repository> <id>jboss-public-repository-group</id> <name>JBoss Public Repository Group</name> <url>http://repository.jboss.org/nexus/content/groups/public/</url> <layout>default</layout> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> <updatePolicy>daily</updatePolicy> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>jboss-public-repository-group</id> <name>JBoss Public Repository Group</name> <url>http://repository.jboss.org/nexus/content/groups/public/</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories> </profile> </profiles> <activeProfiles> <activeProfile>jboss</activeProfile> </activeProfiles> </settings>Our demo consists of single facelet and single backing bean. I assume you are familiar with JF so there's no need to get into details.
<!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:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets"> <f:view locale="#{myBean.locale}"> <h:head> </h:head> <h:body styleClass="rich-container"> <h:form id="f"> <schedule:schedule id="schedule" widgetVar="schedule" switchType="#{mode}" value="#{myBean.lazyDataModel}" var="event" weekMode="#{myBean.weekMode}" height="#{myBean.height}" date="#{myBean.initialDate}" firstDay="#{myBean.firstDay}" showWeekends="#{myBean.showWeekends}" eventColor="#00ff00" eventBorderColor="#ffff00" eventBackgroundColor="#ff00ff" eventTextColor="#ff55ff" allDayText="#{myBean.allDayText}" allDayByDefault="#{myBean.allDayByDefault}" allDaySlot="#{myBean.allDaySlot}" aspectRatio="#{myBean.aspectRatio}" axisFormat="#{myBean.axisFormat}" contentHeight="#{myBean.contentHeight}" defaultEventMinutes="#{myBean.defaultEventMinutes}" dragOpacity="#{myBean.dragOpacity}" dragRevertDuration="#{myBean.dragRevertDuration}" editable="#{myBean.editable}" selectable="#{myBean.selectable}" selectHelper="#{myBean.selectHelper}" unselectAuto="#{myBean.unselectAuto}" unselectCancel="#{myBean.unselectCancel}" firstHour="#{myBean.firstHour}" headerCenter="#{myBean.headerCenter}" headerLeft="#{myBean.headerLeft}" headerRight="#{myBean.headerRight}" isRTL="#{myBean.isRTL}" maxTime="#{myBean.maxTime}" minTime="#{myBean.minTime}" slotMinutes="#{myBean.slotMinutes}" view="#{myBean.view}" columnFormat="#{myBean.columnFormat}" titleFormat="#{myBean.titleFormat}" timeFormat="#{myBean.timeFormat}" itemResizeListener="#{myBean.taskResized}" itemMoveListener="#{myBean.taskMoved}" itemSelectListener="#{myBean.taskSelected}" viewChangeListener="#{myBean.viewChanged}" dateRangeChangeListener="#{myBean.dateRangeChanged}" dateRangeSelectListener="#{myBean.dateRangeSelected}" dateSelectListener="#{myBean.dateSelected}" ondaterangeselect="#{rich:component('schedule')}.unselect()"> <schedule:scheduleItem eventId="#{event.id}" startDate="#{event.startDate}" title="#{event.title}" endDate="#{event.endDate}" allDay="#{event.allDay}" styleClass="#{event.id == 1 ? 'first' : null}" data="#{event.data}" color="#{event.color}"/> <schedule:scheduleMonthView weekMode="#{myBean.weekMode}" titleFormat="MM yy" timeFormat="h:m" columnFormat="dddd" dragOpacity=".1"/> <schedule:scheduleAgendaDayView titleFormat="d MMM yy" timeFormat="hh:m" columnFormat="ddd" dragOpacity=".3"/> <schedule:scheduleBasicDayView titleFormat="dd MMM yy" timeFormat="hh:mm" columnFormat="aaa ddd" dragOpacity=".5"/> <schedule:scheduleAgendaWeekView titleFormat="dd MMM yy" timeFormat="hh:mm" columnFormat="aaa ddd" dragOpacity=".7"/> <schedule:scheduleBasicWeekView titleFormat="bw dd MMM yy" timeFormat="bw hh:mm" columnFormat="bw ddd" dragOpacity=".9"/> <schedule:itemSelectedListener binding="#{myBean.additionalListener}"/> <schedule:itemMoveListener binding="#{myBean.additionalListener}"/> <schedule:itemResizeListener binding="#{myBean.additionalListener}"/> <schedule:dateRangeChangedListener binding="#{myBean.additionalListener}"/> <schedule:dateRangeSelectedListener binding="#{myBean.additionalListener}"/> <schedule:dateSelectedListener binding="#{myBean.additionalListener}"/> <schedule:viewChangedListener binding="#{myBean.additionalListener}"/> </schedule:schedule> </h:form> </h:body> </f:view> </html>
package org.richfaces.schedule; import org.ajax4jsf.model.DataVisitor; import org.ajax4jsf.model.ExtendedDataModel; import org.ajax4jsf.model.Range; import org.richfaces.component.AbstractSchedule; import org.richfaces.component.event.ScheduleDateRangeChangeEvent; import org.richfaces.component.event.ScheduleDateRangeChangeListener; import org.richfaces.component.event.ScheduleDateRangeSelectEvent; import org.richfaces.component.event.ScheduleDateRangeSelectListener; import org.richfaces.component.event.ScheduleDateSelectEvent; import org.richfaces.component.event.ScheduleDateSelectListener; import org.richfaces.component.event.ScheduleItemMoveEvent; import org.richfaces.component.event.ScheduleItemMoveListener; import org.richfaces.component.event.ScheduleItemResizeEvent; import org.richfaces.component.event.ScheduleItemResizeListener; import org.richfaces.component.event.ScheduleItemSelectEvent; import org.richfaces.component.event.ScheduleItemSelectListener; import org.richfaces.component.event.ScheduleViewChangeEvent; import org.richfaces.component.event.ScheduleViewChangeListener; import org.richfaces.component.model.DateRange; import javax.faces.application.FacesMessage; import javax.faces.application.FacesMessage.Severity; import javax.faces.context.FacesContext; import javax.faces.event.FacesEvent; import java.io.Serializable; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; public class Bean implements Serializable { // ------------------------------ FIELDS ------------------------------ private CustomScheduleListener additionalListener = new CustomScheduleListener(); private boolean allDayByDefault; private boolean allDaySlot = true; private String allDayText = "All day"; private List<ScheduleTask> allTasks = new ArrayList<ScheduleTask>(); private boolean allowTaskMoving; private boolean allowTaskResizing; private Double aspectRatio = 1.; private String axisFormat = "h(:mm)tt"; private String columnFormat = null; private Integer contentHeight = 400; private Integer defaultEventMinutes = 90; private Double dragOpacity = .2; private Integer dragRevertDuration = 2000; private Boolean editable = true; private int firstDay = Calendar.SUNDAY; private Integer firstHour = 8; private String headerCenter = "title"; private String headerLeft = "prevYear,nextYear"; private String headerRight = "basicDay,basicWeek agendaDay,agendaWeek month today prev,next"; private Integer height = 400; private Date initialDate; private boolean isRTL; private ExtendedDataModel lazyDataModel = new MyDataModel(); private String locale; private Integer maxTime = 17; private Integer minTime = 8; private Boolean selectHelper = true; private Boolean selectable = true; private String selectedEventId; private boolean showWeekends; private Integer slotMinutes = 30; private String switchType = "ajax"; private int taskIdSequence = 1; private String text; private String timeFormat = null; private String titleFormat = null; private Boolean unselectAuto = true; private String unselectCancel = ""; private String view = AbstractSchedule.VIEW_MONTH; private String weekMode = AbstractSchedule.WEEK_MODE_FIXED; // --------------------------- CONSTRUCTORS --------------------------- public Bean() { Calendar instance = Calendar.getInstance(); instance.setTime(getInitialDate()); final String[] colors = new String[]{"#00ff00", "#ff0000", "#0000ff"}; Random random = new Random(); for (int i = -30; i < 60; i++) { instance.set(Calendar.HOUR, minTime + random.nextInt(maxTime - minTime)); instance.set(Calendar.MINUTE, random.nextInt(59)); instance.add(Calendar.DAY_OF_YEAR, 1); Map<String, Object> data = new HashMap<String, Object>(); data.put("category", "category-" + (i % 3)); int taskId = taskIdSequence++; allTasks.add(new ScheduleTask("" + taskId, "Title " + taskId, instance.getTime(), instance.getTime(), data, colors[random.nextInt(3)])); } } // --------------------- GETTER / SETTER METHODS --------------------- public CustomScheduleListener getAdditionalListener() { return additionalListener; } public boolean getAllDayByDefault() { return allDayByDefault; } public void setAllDayByDefault(boolean allDayByDefault) { this.allDayByDefault = allDayByDefault; } public boolean getAllDaySlot() { return allDaySlot; } public void setAllDaySlot(boolean allDaySlot) { this.allDaySlot = allDaySlot; } public String getAllDayText() { return allDayText; } public void setAllDayText(String allDayText) { this.allDayText = allDayText; } public Double getAspectRatio() { return aspectRatio; } public void setAspectRatio(Double aspectRatio) { this.aspectRatio = aspectRatio; } public String getAxisFormat() { return axisFormat; } public void setAxisFormat(String axisFormat) { this.axisFormat = axisFormat; } public String getColumnFormat() { return columnFormat; } public void setColumnFormat(String columnFormat) { this.columnFormat = columnFormat; } public Integer getContentHeight() { return contentHeight; } public void setContentHeight(Integer contentHeight) { this.contentHeight = contentHeight; } public Integer getDefaultEventMinutes() { return defaultEventMinutes; } public void setDefaultEventMinutes(Integer defaultEventMinutes) { this.defaultEventMinutes = defaultEventMinutes; } public Double getDragOpacity() { return dragOpacity; } public void setDragOpacity(Double dragOpacity) { this.dragOpacity = dragOpacity; } public Integer getDragRevertDuration() { return dragRevertDuration; } public void setDragRevertDuration(Integer dragRevertDuration) { this.dragRevertDuration = dragRevertDuration; } public Boolean getEditable() { return editable; } public void setEditable(Boolean editable) { this.editable = editable; } public int getFirstDay() { return firstDay; } public void setFirstDay(int firstDay) { this.firstDay = firstDay; } public Integer getFirstHour() { return firstHour; } public void setFirstHour(Integer firstHour) { this.firstHour = firstHour; } public String getHeaderCenter() { return headerCenter; } public void setHeaderCenter(String headerCenter) { this.headerCenter = headerCenter; } public String getHeaderLeft() { return headerLeft; } public void setHeaderLeft(String headerLeft) { this.headerLeft = headerLeft; } public String getHeaderRight() { return headerRight; } public void setHeaderRight(String headerRight) { this.headerRight = headerRight; } public Integer getHeight() { return height; } public void setHeight(Integer height) { this.height = height; } public Date getInitialDate() { if (initialDate == null) { Calendar instance = Calendar.getInstance(); instance.set(Calendar.YEAR, 2012); instance.set(Calendar.MONTH, 7); instance.set(Calendar.DATE, 22); initialDate = instance.getTime(); } return initialDate; } public void setInitialDate(Date initialDate) { this.initialDate = initialDate; } public Boolean getIsRTL() { return isRTL; } public void setIsRTL(Boolean isRTL) { this.isRTL = isRTL; } public ExtendedDataModel getLazyDataModel() { return lazyDataModel; } public String getLocale() { if (locale == null) { locale = FacesContext.getCurrentInstance().getViewRoot().getLocale().getLanguage(); } return locale; } public void setLocale(String locale) { this.locale = locale; } public Integer getMaxTime() { return maxTime; } public void setMaxTime(Integer maxTime) { this.maxTime = maxTime; } public Integer getMinTime() { return minTime; } public void setMinTime(Integer minTime) { this.minTime = minTime; } public Boolean getSelectHelper() { return selectHelper; } public void setSelectHelper(Boolean selectHelper) { this.selectHelper = selectHelper; } public Boolean getSelectable() { return selectable; } public void setSelectable(Boolean selectable) { this.selectable = selectable; } public String getSelectedEventId() { return selectedEventId; } public Integer getSlotMinutes() { return slotMinutes; } public void setSlotMinutes(Integer slotMinutes) { this.slotMinutes = slotMinutes; } public String getSwitchType() { return switchType; } public void setSwitchType(String switchType) { this.switchType = switchType; } public String getText() { return text; } public void setText(String text) { this.text = text; } public String getTimeFormat() { return timeFormat; } public void setTimeFormat(String timeFormat) { this.timeFormat = timeFormat; } public String getTitleFormat() { return titleFormat; } public void setTitleFormat(String titleFormat) { this.titleFormat = titleFormat; } public Boolean getUnselectAuto() { return unselectAuto; } public void setUnselectAuto(Boolean unselectAuto) { this.unselectAuto = unselectAuto; } public String getUnselectCancel() { return unselectCancel; } public void setUnselectCancel(String unselectCancel) { this.unselectCancel = unselectCancel; } public String getView() { return view; } public void setView(String view) { this.view = view; } public String getWeekMode() { return weekMode; } public void setWeekMode(String weekMode) { this.weekMode = weekMode; } public boolean isAllowTaskMoving() { return allowTaskMoving; } public void setAllowTaskMoving(boolean allowTaskMoving) { this.allowTaskMoving = allowTaskMoving; } public boolean isAllowTaskResizing() { return allowTaskResizing; } public void setAllowTaskResizing(boolean allowTaskResizing) { this.allowTaskResizing = allowTaskResizing; } public boolean isShowWeekends() { return showWeekends; } public void setShowWeekends(boolean showWeekends) { this.showWeekends = showWeekends; } // -------------------------- OTHER METHODS -------------------------- public void dateRangeChanged(ScheduleDateRangeChangeEvent event) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Date range changed", event.toString())); Calendar calendar = Calendar.getInstance(); calendar.setTime(event.getStartDate()); if (AbstractSchedule.VIEW_MONTH.equals(getView())) { calendar.add(Calendar.DATE, 15); } setInitialDate(calendar.getTime()); } public void dateRangeSelected(ScheduleDateRangeSelectEvent event) { if (editable) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Date range selected", event.toString())); int taskId = taskIdSequence++; allTasks.add(new ScheduleTask("" + taskId, "Title-" + taskId, event.getStartDate(), event.getEndDate(), event.isAllDay())); } else { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Date range selected", "Cannot create item. Not in edit mode.")); } } public void dateSelected(ScheduleDateSelectEvent event) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Date selected", event.toString())); setInitialDate(event.getDate()); } public List<ScheduleTask> getAllEvents() { return allTasks; } public ScheduleTask getSelectedTask() { return getTask(getSelectedEventId()); } public Boolean taskMoved(ScheduleItemMoveEvent event) { System.out.println("taskMoved invoked " + event + " : " + isAllowTaskMoving()); if (isAllowTaskMoving()) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Item moved", event.toString())); ScheduleTask task = getTask(event.getEventId()); selectedEventId = event.getEventId(); if (task != null) { boolean endDateEqualsStartDate = task.getStartDate().equals(task.getEndDate()); Calendar calendar = Calendar.getInstance(); calendar.setTime(task.getStartDate()); calendar.add(Calendar.DAY_OF_MONTH, event.getDayDelta()); calendar.add(Calendar.MINUTE, event.getMinuteDelta()); task.setStartDate(calendar.getTime()); if (!event.isAllDay() && endDateEqualsStartDate) { calendar.setTime(task.getStartDate()); calendar.add(Calendar.MINUTE, getDefaultEventMinutes()); } else { calendar.setTime(task.getEndDate()); calendar.add(Calendar.DAY_OF_MONTH, event.getDayDelta()); calendar.add(Calendar.MINUTE, event.getMinuteDelta()); } task.setEndDate(calendar.getTime()); task.setAllDay(event.isAllDay()); } } else { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "It is not allowed to move this item", event.toString())); } return isAllowTaskMoving(); } public Boolean taskResized(ScheduleItemResizeEvent event) { System.out.println("taskResized invoked " + event + " : " + isAllowTaskResizing()); if (isAllowTaskResizing()) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Item resized", event.toString())); ScheduleTask task = getTask(event.getEventId()); selectedEventId = event.getEventId(); if (task != null) { Calendar calendar = Calendar.getInstance(); Date date = task.getEndDate() == null ? task.getStartDate() : task.getEndDate(); calendar.setTime(date); calendar.add(Calendar.DAY_OF_MONTH, event.getDayDelta()); calendar.add(Calendar.MINUTE, event.getMinuteDelta()); task.setEndDate(calendar.getTime()); } } else { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "It is not allowed to resize this item", event.toString())); } return isAllowTaskResizing(); } public void taskSelected(ScheduleItemSelectEvent event) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Task selected", event.toString())); selectedEventId = event.getEventId(); } public void viewChanged(ScheduleViewChangeEvent event) { System.out.println("viewChanged invoked " + event); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("View changed", event.toString())); setView(event.getView()); } protected ScheduleTask getTask(String id) { if (id == null) { return null; } for (ScheduleTask task : allTasks) { if (id.equals(task.getId())) { return task; } } return null; } // -------------------------- INNER CLASSES -------------------------- public static class CustomScheduleListener implements ScheduleDateRangeChangeListener, ScheduleDateSelectListener, ScheduleItemMoveListener, ScheduleItemResizeListener, ScheduleItemSelectListener, ScheduleViewChangeListener, ScheduleDateRangeSelectListener, Serializable { // ------------------------------ FIELDS ------------------------------ private static FacesEvent recentlyProcessedEvent; // ------------------------ INTERFACE METHODS ------------------------ // --------------------- Interface ScheduleDateRangeChangeListener --------------------- public void dateRangeChanged(ScheduleDateRangeChangeEvent event) { if (event != recentlyProcessedEvent) { recentlyProcessedEvent = event; addMessage(event.toString(), FacesMessage.SEVERITY_INFO); } } // --------------------- Interface ScheduleDateRangeSelectListener --------------------- public void dateRangeSelected(ScheduleDateRangeSelectEvent event) { if (event != recentlyProcessedEvent) { recentlyProcessedEvent = event; addMessage(event.toString(), FacesMessage.SEVERITY_INFO); } } // --------------------- Interface ScheduleDateSelectListener --------------------- public void dateSelected(ScheduleDateSelectEvent event) { if (event != recentlyProcessedEvent) { recentlyProcessedEvent = event; addMessage(event.toString(), FacesMessage.SEVERITY_INFO); } } // --------------------- Interface ScheduleItemMoveListener --------------------- public void itemMove(ScheduleItemMoveEvent event) { if (event != recentlyProcessedEvent) { recentlyProcessedEvent = event; addMessage("I'd like to veto moving, but nobody cares!", FacesMessage.SEVERITY_WARN); } } // --------------------- Interface ScheduleItemResizeListener --------------------- public void itemResize(ScheduleItemResizeEvent event) { if (event != recentlyProcessedEvent) { recentlyProcessedEvent = event; addMessage("I'd like to veto resizing, but nobody cares!", FacesMessage.SEVERITY_WARN); } } // --------------------- Interface ScheduleItemSelectListener --------------------- public void itemSelected(ScheduleItemSelectEvent event) { if (event != recentlyProcessedEvent) { recentlyProcessedEvent = event; addMessage(event.toString(), FacesMessage.SEVERITY_INFO); } } // --------------------- Interface ScheduleViewChangeListener --------------------- public void viewChanged(ScheduleViewChangeEvent event) { if (event != recentlyProcessedEvent) { recentlyProcessedEvent = event; addMessage(event.toString(), FacesMessage.SEVERITY_INFO); } } private void addMessage(String text, Severity severity) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(severity, "Additional listener", text)); } } private class MyDataModel extends ExtendedDataModel implements Serializable { // ------------------------------ FIELDS ------------------------------ java.util.Map indexToRowKey = new HashMap(); int rowCount = -1; int rowIndex = -1; Object rowKey; java.util.Map rowKeyToIndex = new HashMap(); java.util.Map wrappedDataMap = new HashMap(); // --------------------- GETTER / SETTER METHODS --------------------- @Override public int getRowCount() { if (rowCount == -1) { rowCount = wrappedDataMap.size(); } return rowCount; } @Override public int getRowIndex() { return rowIndex; } @Override public Object getRowKey() { return rowKey; } // -------------------------- OTHER METHODS -------------------------- @Override public Object getRowData() { if (getRowKey() == null) { return null; } else { return wrappedDataMap.get(getRowKey()); } } @Override public Object getWrappedData() { throw new UnsupportedOperationException("Not supported yet."); } @Override public boolean isRowAvailable() { if (getRowKey() == null) { return false; } else { return null != wrappedDataMap.get(getRowKey()); } } @Override public void setRowIndex(int rowIndex) { this.rowIndex = rowIndex; Object key = indexToRowKey.get(rowIndex); if ((key != null && !key.equals(getRowKey())) || (key == null && getRowKey() != null)) { setRowKey(key); } } @Override public void setRowKey(Object key) { this.rowKey = key; Integer index = (Integer) rowKeyToIndex.get(key); if (index == null) { index = -1; } if (index != getRowIndex()) { setRowIndex(rowIndex); } } @Override public void setWrappedData(Object data) { throw new UnsupportedOperationException("Not supported yet."); } @Override public void walk(FacesContext context, DataVisitor visitor, Range range, Object argument) { Date startDate = ((DateRange) range).getStartDate(); Date endDate = ((DateRange) range).getEndDate(); wrappedDataMap.clear(); indexToRowKey.clear(); rowKeyToIndex.clear(); int i = 0; for (ScheduleTask task : allTasks) { if ((startDate == null || task.getStartDate().compareTo(startDate) >= 0) && (endDate == null || task.getStartDate().compareTo(endDate) < 0)) { wrappedDataMap.put(task.getId(), task); int index = i++; indexToRowKey.put(index, task.getId()); rowKeyToIndex.put(task.getId(), index); visitor.process(context, task.getId(), argument); } } rowCount = -1; } } }
package org.richfaces.schedule; import java.io.Serializable; import java.util.Date; import java.util.Map; public class ScheduleTask implements Serializable { private String id; private String title; private Date startDate; private Date endDate; private Boolean allDay; private Map<String, Object> data; private Boolean editable; private String url; private String details; private String color; public ScheduleTask() { } public ScheduleTask(String id, String title, Date start, Date end) { this.id = id; this.title = title; this.startDate = start; this.endDate = end; } public ScheduleTask(String id, String title, Date start, Date end, boolean allDay) { this.id = id; this.title = title; this.startDate = start; this.endDate = end; this.allDay = allDay; } public ScheduleTask(String id, String title, Date start, Date end, Map<String, Object> data, String color) { this.id = id; this.title = title; this.startDate = start; this.endDate = end; this.data = data; this.color = color; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Date getStartDate() { return startDate; } public void setStartDate(Date startDate) { this.startDate = startDate; } public Date getEndDate() { return endDate; } public void setEndDate(Date endDate) { this.endDate = endDate; } public Boolean getAllDay() { return allDay; } public void setAllDay(boolean allDay) { this.allDay = allDay; } public Map<String, Object> getData() { return data; } public void setData(Map<String, Object> data) { this.data = data; } public Boolean getEditable() { return editable; } public void setEditable(Boolean editable) { this.editable = editable; } public String getURL() { return url; } public void setURL(String url) { this.url = url; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof ScheduleTask)) { return false; } ScheduleTask toCompare = (ScheduleTask) obj; return this.id != null && this.id.equals(toCompare.id); } @Override public int hashCode() { int hash = 1; if (this.id != null) { hash = hash * 31 + this.id.hashCode(); } return hash; } public String getDetails() { return details; } public void setDetails(String details) { this.details = details; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } }
Simple attributes and rendering first javascript
How does the widget work? What it requires? Show code for widget with pure HTML+JS. It’s time to start writing our component. Firstly we will try to have our component render javascript that initiates the JavaScript widget with options defined in <schedule:schedule> tag. So the output should look like this:<div class="rich-schedule " id="f:schedule"> <script type="text/javascript"> var schedule = new RichFaces.ui.Schedule("f:schedule",{"dragRevertDuration":2000,"minTime":"8","locale":"en","aspectRatio":1.0,"maxTime":"17","selectHelper":true,"date":22,"header":{"center":"title","right":"basicDay,basicWeek agendaDay,agendaWeek month today prev,next","left":"prevYear,nextYear"} ,"weekends":false,"editable":true,"ondaterangeselect":"RichFaces.$('f:schedule').unselect()","contentHeight":400,"height":400,"allDayDefault":false,"dragOpacity":{"":0.2} ,"defaultEventMinutes":90,"selectable":true,"month":7,"allDayText":"All day","year":2012,"firstHour":8} ); </script> </div>We need to write 5 files:
- main/java/org/richfaces/component/package-info.java
- main/java/org/richfaces/component/AbstractSchedule.java
- main/java/org/richfaces/renderkit/ScheduleRendererBase.java
- main/templates/org/richfaces/schedule.template.xml
- main/resources/META-INF/resources/richfaces.schedule.js
- ui.core.js
- ui.draggable.js
- ui.resizable.js
- fullcalendar.js
- fullcalendar.css
@TagLibrary(uri = "http://richfaces.org/sandbox/schedule", shortName = "schedule", prefix = "schedule", displayName = "Schedule component tags") package org.richfaces.component; import org.richfaces.cdk.annotations.TagLibrary;This file contains name of package and @TagLibrary annotation. It is important that the package is the same as the one of AbstractSchedule. As you can see above we tell CDK to include all components in org.richfaces.component package of our project into tag library which name space shall be http://richfaces.org/sandbox/schedule default prefix (i.e. suggested by IDE’s) should be “schedule” (but this can always be redefined by end user in facelet).
AbstractSchedule class consist mostly of annotated getters that represent attributes of our component. Later we will add code responsible for handling events. The class alone must be appropriately annotated and be a subclass of UIComponent.
@JsfComponent(tag = @Tag(name = "schedule", type = TagType.Facelets), renderer = @JsfRenderer(family = AbstractSchedule.COMPONENT_FAMILY, type = ScheduleRendererBase.RENDERER_TYPE) ) public abstract class AbstractSchedule extends UIComponentBase@JsfComponent tells CDK to generate component by convention named UISchedule (Abstract is stripped from AbstractSchedule and UI prefix is added). To associate this component with a renderer it is important that @JsfRenderer(family,type) pair is exactly the same as in schedule.template.xml. Next we define attributes. Note that unlike in RF 3.X we only need to specify getters. Setters will be automatically generated in UISchedule unless we set readOnly attribute of @Attribute annotation to true. Here is complete class for current stage:
@JsfComponent(tag = @Tag(name = "schedule", type = TagType.Facelets), renderer = @JsfRenderer(family = AbstractSchedule.COMPONENT_FAMILY, type = ScheduleRendererBase.RENDERER_TYPE) ) public abstract class AbstractSchedule extends UIComponentBase { // ------------------------------ FIELDS ------------------------------ public static final String COMPONENT_FAMILY = "org.richfaces.Schedule"; public static final String COMPONENT_TYPE = "org.richfaces.Schedule"; /** * Values of switchType attribute */ public static final String SWITCH_TYPE_AJAX = "ajax"; public static final String SWITCH_TYPE_CLIENT = "client"; public static final String SWITCH_TYPE_SERVER = "server"; public static final String VIEW_AGENDA_DAY = "agendaDay"; public static final String VIEW_AGENDA_WEEK = "agendaWeek"; public static final String VIEW_BASIC_DAY = "basicDay"; public static final String VIEW_BASIC_WEEK = "basicWeek"; /** * Values of view attribute. */ public static final String VIEW_MONTH = "month"; /** * Values of weekMode attribute. */ public static final String WEEK_MODE_FIXED = "fixed"; public static final String WEEK_MODE_LIQUID = "liquid"; public static final String WEEK_MODE_VARIABLE = "variable"; public static final boolean DEFAULT_ALL_DAY_DEFAULT = true; public static final boolean DEFAULT_ALL_DAY_SLOT = true; public static final double DEFAULT_ASPECT_RATIO = 1.35; public static final boolean DEFAULT_AUTO_REFRESH_ON_DATE_RANGE_SELECT = true; public static final String DEFAULT_AXIS_FORMAT = "h(:mm)tt"; public static final boolean DEFAULT_DISABLE_DRAGGING = false; public static final boolean DEFAULT_DISABLE_RESIZING = false; public static final double DEFAULT_DRAG_OPACITY = .3; public static final int DEFAULT_DRAG_REVERT_DURATION = 500; public static final boolean DEFAULT_EDITABLE = false; public static final int DEFAULT_EVENT_MINUTES = 120; public static final int DEFAULT_FIRST_DAY = Calendar.SUNDAY; public static final int DEFAULT_FIRST_HOUR = 6; public static final String DEFAULT_MAX_TIME = "24"; public static final String DEFAULT_MIN_TIME = "0"; public static final boolean DEFAULT_RTL = false; public static final boolean DEFAULT_SELECTABLE = false; public static final boolean DEFAULT_SELECT_HELPER = false; public static final boolean DEFAULT_SHOW_WEEKENDS = true; public static final int DEFAULT_SLOT_MINUTES = 30; public static final String DEFAULT_SWITCH_TYPE = SWITCH_TYPE_AJAX; public static final boolean DEFAULT_UNSELECT_AUTO = true; public static final String DEFAULT_UNSELECT_CANCEL = ""; public static final String DEFAULT_VIEW = VIEW_MONTH; public static final String DEFAULT_WEEK_MODE = WEEK_MODE_FIXED; // -------------------------- STATIC METHODS -------------------------- @Attribute(defaultValue = "" + DEFAULT_DRAG_OPACITY) public abstract Double getDragOpacity(); // -------------------------- OTHER METHODS -------------------------- @Attribute public abstract String getAllDayText(); @Attribute(defaultValue = "" + DEFAULT_ASPECT_RATIO) public abstract Double getAspectRatio(); @Attribute(defaultValue = DEFAULT_AXIS_FORMAT) public abstract String getAxisFormat(); @Attribute public abstract Integer getContentHeight(); @Attribute public abstract Date getDate(); @Attribute(defaultValue = "" + DEFAULT_EVENT_MINUTES) public abstract Integer getDefaultEventMinutes(); @Attribute(defaultValue = "" + DEFAULT_DRAG_REVERT_DURATION) public abstract Integer getDragRevertDuration(); @Attribute(defaultValue = "" + DEFAULT_FIRST_DAY, description = @Description("First day of week. 1 - sunday, 2 - monday,..,7 - saturday.")) public abstract Integer getFirstDay(); @Attribute(defaultValue = "" + DEFAULT_FIRST_HOUR) public abstract Integer getFirstHour(); @Attribute public abstract String getHeaderCenter(); @Attribute public abstract String getHeaderLeft(); @Attribute public abstract String getHeaderRight(); @Attribute public abstract Integer getHeight(); @Attribute(defaultValue = DEFAULT_MAX_TIME) public abstract String getMaxTime(); @Attribute(defaultValue = DEFAULT_MIN_TIME) public abstract String getMinTime(); @Attribute(events = @EventName("beforedaterangeselect")) public abstract String getOnbeforedaterangeselect(); @Attribute(events = @EventName("beforedateselect")) public abstract String getOnbeforedateselect(); @Attribute(events = @EventName("beforeitemdrop")) public abstract String getOnbeforeitemdrop(); @Attribute(events = @EventName("beforeitemresize")) public abstract String getOnbeforeitemresize(); @Attribute(events = @EventName("beforeitemselect")) public abstract String getOnbeforeitemselect(); @Attribute(events = @EventName("daterangechange")) public abstract String getOndaterangechange(); @Attribute(events = @EventName("daterangeselect")) public abstract String getOndaterangeselect(); @Attribute(events = @EventName(value = "dateselect", defaultEvent = true)) public abstract String getOndateselect(); @Attribute(events = @EventName("itemdragstart")) public abstract String getOnitemdragstart(); @Attribute(events = @EventName("itemdragstop")) public abstract String getOnitemdragstop(); @Attribute(events = @EventName("itemdrop")) public abstract String getOnitemdrop(); @Attribute(events = @EventName("itemmouseout")) public abstract String getOnitemmouseout(); @Attribute(events = @EventName("itemmouseover")) public abstract String getOnitemmouseover(); @Attribute(events = @EventName("itemresize")) public abstract String getOnitemresize(); @Attribute(events = @EventName("itemresizestart")) public abstract String getOnitemresizestart(); @Attribute(events = @EventName("itemresizestop")) public abstract String getOnitemresizestop(); @Attribute(events = @EventName("itemselect")) public abstract String getOnitemselect(); @Attribute(events = @EventName("viewchange")) public abstract String getOnviewchange(); @Attribute(events = @EventName("viewdisplay")) public abstract String getOnviewdisplay(); @Attribute(defaultValue = "" + DEFAULT_SLOT_MINUTES) public abstract Integer getSlotMinutes(); @Attribute public abstract String getStyleClass(); @Attribute(defaultValue = "SwitchType." + DEFAULT_SWITCH_TYPE, suggestedValue = SWITCH_TYPE_AJAX + "," + SWITCH_TYPE_SERVER + "," + SWITCH_TYPE_CLIENT) public abstract SwitchType getSwitchType(); @Attribute(defaultValue = DEFAULT_UNSELECT_CANCEL) public abstract String getUnselectCancel(); @Attribute(defaultValue = DEFAULT_VIEW, suggestedValue = VIEW_MONTH + "," + VIEW_AGENDA_DAY + "," + VIEW_AGENDA_WEEK + "," + VIEW_BASIC_DAY + "," + VIEW_BASIC_WEEK) public abstract String getView(); @Attribute(defaultValue = DEFAULT_WEEK_MODE, suggestedValue = WEEK_MODE_FIXED + "," + WEEK_MODE_LIQUID + "," + WEEK_MODE_VARIABLE) public abstract String getWeekMode(); @Attribute public abstract String getWidgetVar(); @Attribute(defaultValue = "" + DEFAULT_ALL_DAY_DEFAULT) public abstract Boolean isAllDayByDefault(); @Attribute(defaultValue = "" + DEFAULT_ALL_DAY_SLOT) public abstract Boolean isAllDaySlot(); /** * Tells if schedule should be automatically refreshed when date range is selected. * * @return true if schedule should be refreshed automaticaly; flase otherwise. */ @Attribute(defaultValue = "true") public abstract boolean isAutoRefreshOnDateRangeSelect(); @Attribute(defaultValue = "" + DEFAULT_DISABLE_DRAGGING) public abstract Boolean isDisableDragging(); @Attribute(defaultValue = "" + DEFAULT_DISABLE_RESIZING) public abstract Boolean isDisableResizing(); @Attribute(defaultValue = "" + DEFAULT_EDITABLE) public abstract Boolean isEditable(); @Attribute(defaultValue = "" + DEFAULT_RTL) public abstract Boolean isRTL(); @Attribute(defaultValue = "" + DEFAULT_SELECT_HELPER) public abstract Boolean isSelectHelper(); @Attribute(defaultValue = "" + DEFAULT_SELECTABLE) public abstract Boolean isSelectable(); @Attribute(defaultValue = "" + DEFAULT_SHOW_WEEKENDS) public abstract Boolean isShowWeekends(); @Attribute(defaultValue = "" + DEFAULT_UNSELECT_AUTO) public abstract Boolean isUnselectAuto(); }There are a few things I want to draw your attention to. Firstly, I use public static attributes to store default values for attributes. This can save us and the end users some headaches caused by typo’s. Secondly if attribute is of type String, but only particular values are allowed (it’s more enum then String, but it’s easier to write strings as tag attribute values in facelets) then it is good to specify suggestedValue attribute (i.e. getView). Thirdly take look at this:
@Attribute(events = @EventName(value = "dateselect", defaultEvent = true)) public abstract String getOndateselect();If some attribute is responsible for rendering some javascript callback that is triggered on some event then we should specify events attribute. Moreover if this event is default (most commonly used) we can specify this by defaultEvent=true. This way end user will be able to attach custom behaviours to our component.
<schedule:schedule> <a4j:ajax event="dateselect" render="someComponentId"/> </schedule:schedule>
<cdk:root xmlns:cc="http://jboss.org/schema/richfaces/cdk/jsf/composite" xmlns:cdk="http://jboss.org/schema/richfaces/cdk/core" xmlns="http://jboss.org/schema/richfaces/cdk/xhtml-el"> <cc:interface> <cdk:class>org.richfaces.renderkit.ScheduleRenderer</cdk:class> <cdk:superclass>org.richfaces.renderkit.ScheduleRendererBase</cdk:superclass> <cdk:component-family>org.richfaces.Schedule</cdk:component-family> <cdk:renderer-type>org.richfaces.ScheduleRenderer</cdk:renderer-type> </cc:interface> <cc:implementation> <div class="rich-schedule #{component.attributes['styleClass']}" id="#{clientId}"> <script type="text/javascript"> <cdk:call expression="writeInitFunction(facesContext,component);"/> </script> </div> </cc:implementation> </cdk:root>XSDs for most of namespaces usable in this file are located in org.richfaces.cdk:generator (https://github.com/richfaces/cdk/tree/develop/generator). This file is fairly simple. Based on this file CDK will generate a java class that will be responsible for rendering. In interface section we specify the name of generated class (<cdk:class>), which class should generated one extend (<cdk:superclass>), the component family and renderer type (this must match values passed to @JsfComponent in AbstractSchedule). Implementation section holds stuff that will be rendered.
Basically we will output DIV that will surround javascript that initiates our widget. Note that we nest the script within DIV. That’s necessary if end user will want to re-render the component using “render” attribute of some other ajax-enabled component. Otherwise the DIV would be re-rendered (old div with schedule would be removed and new empty DIV would be inserted) but widget would not be initiated again. Notice how we can easily, using EL expressions, access components attributes. Last thing is generating the javascript. Because there is a lot of options here and we don’t want to explicitly render options with default values (so there will be lots of conditions) we will put that code in ScheduleRendererBase.writeInitFunction. Now you know what such classes (<cdk:superclass>) are for.
ScheduleRendererBase class is responsible for rendering the javascript code (so partial encoding of component) and decoding events. We will discuss events later. For now we only need to implement writeInitFunction that will produce javascript code that initiates our widget.
protected void writeInitFunction(FacesContext context, UIComponent component) throws IOException { AbstractSchedule schedule = (AbstractSchedule) component; ResponseWriter writer = context.getResponseWriter(); String clientId = schedule.getClientId(context); Locale locale = context.getViewRoot().getLocale(); String widgetVar = schedule.getWidgetVar(); if (widgetVar != null) { writer.writeText("var " + widgetVar + " = ", null); } final Map<String, Object> options = getOptions(schedule); options.put("locale", locale.toString()); writer.writeText(new JSObject("RichFaces.ui.Schedule", clientId, options).toScript(), null); }The most important line here is the last one. It will produce
new RichFaces.ui.Schedule(“someClientId”,{options...})Parameter options is a Map that gets transformed to JSON. Check out other objects like JSObject (org.ajax4jsf.javascript) Our user may also want to store reference to widget in some global variable, that’s why we have following lines:
if (widgetVar != null) { writer.writeText("var " + widgetVar + " = ", null); }Outputting JavaScript code is not enough. We also need to attach JavaScript libraries and other resources needed by the widget. This is easily achieved in JSF2 by decorating renderer with following annotations:
@ResourceDependencies({ @ResourceDependency(library = "javax.faces", name = "jsf.js"), @ResourceDependency(name = "jquery.js", target = "head"), @ResourceDependency(name = "richfaces.js", target = "head"), @ResourceDependency(name = "richfaces-event.js", target = "head"), @ResourceDependency(name = "richfaces-base-component.js", target = "head"), @ResourceDependency(name = "ui.core.js", target = "head"), @ResourceDependency(name = "ui.draggable.js", target = "head"), @ResourceDependency(name = "ui.resizable.js", target = "head"), @ResourceDependency(name = "fullcalendar.js", target = "head"), @ResourceDependency(name = "richfaces.schedule.js", target = "head"), @ResourceDependency(name = "fullcalendar.css", target = "head")})Note that in case of JavaScript files order matters! Our richfaces.schedule.js library depends on fullcalendar.js which in turn depends on ui.resizable.js, ui.draggable.js. Those two last libraries depend on jquery.js. Our richfaces.schedule.js depends also on richfaces-base-components.js, which depends on richfaces.js which in turn depends on jquery.js and jsf.js. All mentioned resources must reside in META-INF/resources directory in final jar. So if we use Maven to build the library we need to place the files in src/main/resources/META-INF/resources. Please note that some files are provided by JSF and RichFaces so we don’t need to place them in our jar, nor source folder. Those files are:
- jsf.js
- jquery.js
- richfaces.js
- richfaces-event.js
- richfaces-base-component.js
@ResourceDependencies({ @ResourceDependency(library = "javax.faces", name = "jsf.js"), @ResourceDependency(name = "jquery.js", target = "head"), @ResourceDependency(name = "richfaces.js", target = "head"), @ResourceDependency(name = "richfaces-event.js", target = "head"), @ResourceDependency(name = "richfaces-base-component.js", target = "head"), @ResourceDependency(name = "ui.core.js", target = "head"), @ResourceDependency(name = "ui.draggable.js", target = "head"), @ResourceDependency(name = "ui.resizable.js", target = "head"), @ResourceDependency(name = "fullcalendar.js", target = "head"), @ResourceDependency(name = "richfaces.schedule.js", target = "head"), @ResourceDependency(name = "fullcalendar.css", target = "head")}) public abstract class ScheduleRendererBase extends RendererBase { // ------------------------------ FIELDS ------------------------------ public static final String RENDERER_TYPE = "org.richfaces.ScheduleRenderer"; private static final Map<String, Object> DEFAULTS; // -------------------------- STATIC METHODS -------------------------- /** * Following defaults are be used by addOptionIfSetAndNotDefault */ static { Map<String, Object> defaults = new HashMap<String, Object>(); defaults.put("styleClass", ""); defaults.put("defaultView", AbstractSchedule.DEFAULT_VIEW); defaults.put("firstDay", AbstractSchedule.DEFAULT_FIRST_DAY - 1); defaults.put("isRTL", AbstractSchedule.DEFAULT_RTL); defaults.put("weekends", AbstractSchedule.DEFAULT_SHOW_WEEKENDS); defaults.put("weekMode", AbstractSchedule.DEFAULT_WEEK_MODE); defaults.put("aspectRatio", AbstractSchedule.DEFAULT_ASPECT_RATIO); defaults.put("allDaySlot", AbstractSchedule.DEFAULT_ALL_DAY_SLOT); defaults.put("axisFormat", AbstractSchedule.DEFAULT_AXIS_FORMAT); defaults.put("slotMinutes", AbstractSchedule.DEFAULT_SLOT_MINUTES); defaults.put("defaultEventMinutes", AbstractSchedule.DEFAULT_EVENT_MINUTES); defaults.put("firstHour", AbstractSchedule.DEFAULT_FIRST_HOUR); defaults.put("minTime", AbstractSchedule.DEFAULT_MIN_TIME); defaults.put("maxTime", AbstractSchedule.DEFAULT_MAX_TIME); defaults.put("editable", AbstractSchedule.DEFAULT_EDITABLE); defaults.put("selectable", AbstractSchedule.DEFAULT_SELECTABLE); defaults.put("selectHelper", AbstractSchedule.DEFAULT_SELECT_HELPER); defaults.put("unselectAuto", AbstractSchedule.DEFAULT_UNSELECT_AUTO); defaults.put("unselectCancel", AbstractSchedule.DEFAULT_UNSELECT_CANCEL); defaults.put("disableDragging", AbstractSchedule.DEFAULT_DISABLE_DRAGGING); defaults.put("disableResizing", AbstractSchedule.DEFAULT_DISABLE_RESIZING); defaults.put("dragRevertDuration", AbstractSchedule.DEFAULT_DRAG_REVERT_DURATION); defaults.put("allDayDefault", AbstractSchedule.DEFAULT_ALL_DAY_DEFAULT); defaults.put("autoRefreshOnDateRangeSelect", AbstractSchedule.DEFAULT_AUTO_REFRESH_ON_DATE_RANGE_SELECT); defaults.put("onbeforeitemselect", ""); defaults.put("onitemselect", ""); defaults.put("onbeforeitemdrop", ""); defaults.put("onitemdrop", ""); defaults.put("onbeforeitemresize", ""); defaults.put("onitemresize", ""); defaults.put("onitemresizestart", ""); defaults.put("onitemresizestop", ""); defaults.put("onitemdragstart", ""); defaults.put("onitemdragstop", ""); defaults.put("onitemmouseover", ""); defaults.put("onitemmouseout", ""); defaults.put("onviewchange", ""); defaults.put("onviewdisplay", ""); defaults.put("onbeforedateselect", ""); defaults.put("ondateselect", ""); defaults.put("onbeforedaterangeselect", ""); defaults.put("ondaterangeselect", ""); defaults.put("ondaterangechange", ""); DEFAULTS = Collections.unmodifiableMap(defaults); } // -------------------------- OTHER METHODS -------------------------- protected void addOptionIfSetAndNotDefault(String optionName, Object value, Map<String, Object> options) { if (value != null && !"".equals(value) && !value.equals(DEFAULTS.get(optionName))) { options.put(optionName, value); } } protected Map<String, Object> getOptions(AbstractSchedule schedule) throws IOException { /** * Include only attributes that are actually set. */ Map<String, Object> options = new HashMap<String, Object>(); addOptionIfSetAndNotDefault("defaultView", schedule.getView(), options); /** * firstDayOfWeek numeration in Calendar (sunday=1,monday=2,etc.) and in widget(sunday=0,monday=1,etc.) */ Integer firstDay = schedule.getFirstDay(); if (firstDay != null) { addOptionIfSetAndNotDefault("firstDay", firstDay - 1, options); } addOptionIfSetAndNotDefault("isRTL", schedule.isRTL(), options); addOptionIfSetAndNotDefault("weekends", schedule.isShowWeekends(), options); addOptionIfSetAndNotDefault("weekMode", schedule.getWeekMode(), options); addOptionIfSetAndNotDefault("height", schedule.getHeight(), options); addOptionIfSetAndNotDefault("contentHeight", schedule.getContentHeight(), options); addOptionIfSetAndNotDefault("aspectRatio", schedule.getAspectRatio(), options); addOptionIfSetAndNotDefault("allDaySlot", schedule.isAllDaySlot(), options); addOptionIfSetAndNotDefault("allDayText", schedule.getAllDayText(), options); addOptionIfSetAndNotDefault("axisFormat", schedule.getAxisFormat(), options); addOptionIfSetAndNotDefault("slotMinutes", schedule.getSlotMinutes(), options); addOptionIfSetAndNotDefault("defaultEventMinutes", schedule.getDefaultEventMinutes(), options); addOptionIfSetAndNotDefault("firstHour", schedule.getFirstHour(), options); addOptionIfSetAndNotDefault("minTime", schedule.getMinTime(), options); addOptionIfSetAndNotDefault("maxTime", schedule.getMaxTime(), options); addOptionIfSetAndNotDefault("editable", schedule.isEditable(), options); addOptionIfSetAndNotDefault("selectable", schedule.isSelectable(), options); addOptionIfSetAndNotDefault("selectHelper", schedule.isSelectHelper(), options); addOptionIfSetAndNotDefault("unselectAuto", schedule.isUnselectAuto(), options); addOptionIfSetAndNotDefault("unselectCancel", schedule.getUnselectCancel(), options); addOptionIfSetAndNotDefault("disableDragging", schedule.isDisableDragging(), options); addOptionIfSetAndNotDefault("disableResizing", schedule.isDisableResizing(), options); addOptionIfSetAndNotDefault("dragRevertDuration", schedule.getDragRevertDuration(), options); addOptionIfSetAndNotDefault("dragOpacity", schedule.getDragOpacity(), options); Map<String, Object> headerOptions = new HashMap<String, Object>(3); addOptionIfSetAndNotDefault("left", schedule.getHeaderLeft(), headerOptions); addOptionIfSetAndNotDefault("center", schedule.getHeaderCenter(), headerOptions); addOptionIfSetAndNotDefault("right", schedule.getHeaderRight(), headerOptions); if (headerOptions.size() > 0) { options.put("header", headerOptions); } addOptionIfSetAndNotDefault("allDayDefault", schedule.isAllDayByDefault(), options); addOptionIfSetAndNotDefault("autoRefreshOnDateRangeSelect", schedule.isAutoRefreshOnDateRangeSelect(), options); addOptionIfSetAndNotDefault("onbeforeitemselect", schedule.getOnbeforeitemselect(), options); addOptionIfSetAndNotDefault("onitemselect", schedule.getOnitemselect(), options); addOptionIfSetAndNotDefault("onbeforeitemdrop", schedule.getOnbeforeitemdrop(), options); addOptionIfSetAndNotDefault("onitemdrop", schedule.getOnitemdrop(), options); addOptionIfSetAndNotDefault("onbeforeitemresize", schedule.getOnbeforeitemresize(), options); addOptionIfSetAndNotDefault("onitemresize", schedule.getOnitemresize(), options); addOptionIfSetAndNotDefault("onitemresizestart", schedule.getOnitemresizestart(), options); addOptionIfSetAndNotDefault("onitemresizestop", schedule.getOnitemresizestop(), options); addOptionIfSetAndNotDefault("onitemdragstart", schedule.getOnitemdragstart(), options); addOptionIfSetAndNotDefault("onitemdragstop", schedule.getOnitemdragstop(), options); addOptionIfSetAndNotDefault("onitemmouseover", schedule.getOnitemmouseover(), options); addOptionIfSetAndNotDefault("onitemmouseout", schedule.getOnitemmouseout(), options); addOptionIfSetAndNotDefault("onviewchange", schedule.getOnviewchange(), options); addOptionIfSetAndNotDefault("onviewdisplay", schedule.getOnviewdisplay(), options); addOptionIfSetAndNotDefault("onbeforedateselect", schedule.getOnbeforedateselect(), options); addOptionIfSetAndNotDefault("ondateselect", schedule.getOndateselect(), options); addOptionIfSetAndNotDefault("onbeforedaterangeselect", schedule.getOnbeforedaterangeselect(), options); addOptionIfSetAndNotDefault("ondaterangeselect", schedule.getOndaterangeselect(), options); addOptionIfSetAndNotDefault("ondaterangechange", schedule.getOndaterangechange(), options); if (schedule.getDate() != null) { Calendar calendar = Calendar.getInstance(); calendar.setTime(schedule.getDate()); options.put("year", calendar.get(Calendar.YEAR)); options.put("month", calendar.get(Calendar.MONTH)); options.put("date", calendar.get(Calendar.DATE)); } return options; } protected void writeInitFunction(FacesContext context, UIComponent component) throws IOException { AbstractSchedule schedule = (AbstractSchedule) component; ResponseWriter writer = context.getResponseWriter(); String clientId = schedule.getClientId(context); Locale locale = context.getViewRoot().getLocale(); String widgetVar = schedule.getWidgetVar(); if (widgetVar != null) { writer.writeText("var " + widgetVar + " = ", null); } final Map<String, Object> options = getOptions(schedule); options.put("locale", locale.toString()); writer.writeText(new JSObject("RichFaces.ui.Schedule", clientId, options).toScript(), null); } }As we’ve said before, RichFaces team wants to have stable API (also for JavaScript), so if the component uses some third part library then it needs to provide facade, so that the third part library could be substituted with something else in future if necessary (i.e. it stops being developed, or some better library appears). I encourage you to do the same. In RF 3.X there was no unified template for component’s JavaScript. This has changed in RF 4.
(function ($, rf) { // Create (for example) ui container for our component class rf.ui = rf.ui || {}; // Extending component class with new properties and methods using extendClass // $super - reference to the parent prototype, will be available inside those methods rf.ui.Schedule = rf.BaseComponent.extendClass({ // class name name:"Schedule", init: function (componentId, options) { if (!document.getElementById(componentId)) { throw "No element with id '" + componentId + "' found."; } this.options = options; // call constructor of parent class if needed $super.constructor.call(this, componentId); // attach component object to DOM element for // future cleaning and for client side API calls this.attachToDom(this.id); var _this = this; jQuery(function() { jQuery(document.getElementById(_this.id)).fullCalendar(options); }); }, // private functions definition __getDelegate : function() { return jQuery(document.getElementById(this.id)); }, // public API // destructor definition destroy: function () { // define destructor if additional cleaning is needed but // in most cases its not nessesary. // call parent’s destructor $super.destroy.call(this); this.__getDelegate().fullCalendar('destroy'); } }); // define super class reference - reference to the parent prototype var $super = rf.ui.Schedule.$super; })(jQuery, RichFaces);You can read more about how JavaScript facade should look like here: https://community.jboss.org/wiki/RichFacesClient-SideCodeBaseComponentClassesUsage
Ok, now the component is ready to output some javascript. You should be ready to use it like this:
<schedule:schedule/>
DataModel and events rendering
Schedule component is a bit similar to DataTable. They both present a range of items. DataTable displays rowd and columns, and the range is usually defined as two integers, while Schedule displays boxes on 2D grid and the range is defined as two dates. We provide items data usually as a List, DataModel, Object[] (array of objects), ResultSet, Result. AbstractSchedule has value attribute which we can bind data to on facelets. This is similar to DataTable, which also has value attribute that plays the same role. Our widget needs to be initialized with following options in order to display items:jQuery("someSelector").fullCalendar({events:[ {id:1,title:"Item 1 title"}, {id:2,title:"Item 2 title"},... ]});Basically “options” should contain attribute “event” which is an array of objects with following attributes:
- id
- title (mandatory)
- color
- backgroundColor
- borderColor
- textColor
- allDay
- start (mandatory)
- end
- url
- className
- editable
<schedule:schedule value="#{someBean.data}" var="item"> <schedule:scheduleitem color="red" enddate="#{item.end}" eventid="#{item.nr}" rendered="#{item.important}" startdate="#{item.start}" title="#{item.name}"/> <schedule:scheduleitem color="navy" enddate="#{item.end}" eventid="#{item.nr}" rendered="#{not item.important}" startdate="#{item.start}" title="#{item.name}"/> </schedule:schedule>Note that we use a bit different names for attributes. It’s just a matter of taste and doesn’t make any problem because inside renderer we can always output widget compliant names. Also notice that it may be handy to allow several “scheduleItem” tags inside “schedule” component. Like in example above each will be rendered on opposite condition, so the number of items presented will be exactly the same as in collection returned by #{someBean.data}. Unlike DataTabe, Schedule doesn’t allow it’s children (scheduleItem) to render themselves. It’s due to JavaScript widget nature. So since all we need to do is include items into JavaScript “options” parameter let’s look at the ScheduleRendererBase class:
protected Map<String, Object> getOptions(AbstractSchedule schedule) throws IOException { /** * Include only attributes that are actually set. */ Map<String, Object> options = new HashMap<String, Object>(); addOptionIfSetAndNotDefault("defaultView", schedule.getView(), options); /** * firstDayOfWeek numeration in Calendar (sunday=1,monday=2,etc.) and in widget(sunday=0,monday=1,etc.) */ Integer firstDay = schedule.getFirstDay(); if (firstDay != null) { addOptionIfSetAndNotDefault("firstDay", firstDay - 1, options); } addOptionIfSetAndNotDefault("isRTL", schedule.isRTL(), options); addOptionIfSetAndNotDefault("weekends", schedule.isShowWeekends(), options); addOptionIfSetAndNotDefault("weekMode", schedule.getWeekMode(), options); addOptionIfSetAndNotDefault("height", schedule.getHeight(), options); addOptionIfSetAndNotDefault("contentHeight", schedule.getContentHeight(), options); addOptionIfSetAndNotDefault("aspectRatio", schedule.getAspectRatio(), options); addOptionIfSetAndNotDefault("allDaySlot", schedule.isAllDaySlot(), options); addOptionIfSetAndNotDefault("allDayText", schedule.getAllDayText(), options); addOptionIfSetAndNotDefault("axisFormat", schedule.getAxisFormat(), options); addOptionIfSetAndNotDefault("slotMinutes", schedule.getSlotMinutes(), options); addOptionIfSetAndNotDefault("defaultEventMinutes", schedule.getDefaultEventMinutes(), options); addOptionIfSetAndNotDefault("firstHour", schedule.getFirstHour(), options); addOptionIfSetAndNotDefault("minTime", schedule.getMinTime(), options); addOptionIfSetAndNotDefault("maxTime", schedule.getMaxTime(), options); addOptionIfSetAndNotDefault("editable", schedule.isEditable(), options); addOptionIfSetAndNotDefault("selectable", schedule.isSelectable(), options); addOptionIfSetAndNotDefault("selectHelper", schedule.isSelectHelper(), options); addOptionIfSetAndNotDefault("unselectAuto", schedule.isUnselectAuto(), options); addOptionIfSetAndNotDefault("unselectCancel", schedule.getUnselectCancel(), options); addOptionIfSetAndNotDefault("disableDragging", schedule.isDisableDragging(), options); addOptionIfSetAndNotDefault("disableResizing", schedule.isDisableResizing(), options); addOptionIfSetAndNotDefault("dragRevertDuration", schedule.getDragRevertDuration(), options); addOptionIfSetAndNotDefault("eventColor", schedule.getEventColor(), options); addOptionIfSetAndNotDefault("eventBackgroundColor", schedule.getEventBackgroundColor(), options); addOptionIfSetAndNotDefault("eventBorderColor", schedule.getEventBorderColor(), options); addOptionIfSetAndNotDefault("eventTextColor", schedule.getEventTextColor(), options); addOptionHash("dragOpacity", schedule, options); addOptionHash("titleFormat", schedule, options); addOptionHash("timeFormat", schedule, options); addOptionHash("columnFormat", schedule, options); Map<String, Object> headerOptions = new HashMap<String, Object>(3); addOptionIfSetAndNotDefault("left", schedule.getHeaderLeft(), headerOptions); addOptionIfSetAndNotDefault("center", schedule.getHeaderCenter(), headerOptions); addOptionIfSetAndNotDefault("right", schedule.getHeaderRight(), headerOptions); if (headerOptions.size() > 0) { options.put("header", headerOptions); } addOptionIfSetAndNotDefault("allDayDefault", schedule.isAllDayByDefault(), options); addOptionIfSetAndNotDefault("autoRefreshOnDateRangeSelect", schedule.isAutoRefreshOnDateRangeSelect(), options); addOptionIfSetAndNotDefault("onbeforeitemselect", schedule.getOnbeforeitemselect(), options); addOptionIfSetAndNotDefault("onitemselect", schedule.getOnitemselect(), options); addOptionIfSetAndNotDefault("onbeforeitemdrop", schedule.getOnbeforeitemdrop(), options); addOptionIfSetAndNotDefault("onitemdrop", schedule.getOnitemdrop(), options); addOptionIfSetAndNotDefault("onbeforeitemresize", schedule.getOnbeforeitemresize(), options); addOptionIfSetAndNotDefault("onitemresize", schedule.getOnitemresize(), options); addOptionIfSetAndNotDefault("onitemresizestart", schedule.getOnitemresizestart(), options); addOptionIfSetAndNotDefault("onitemresizestop", schedule.getOnitemresizestop(), options); addOptionIfSetAndNotDefault("onitemdragstart", schedule.getOnitemdragstart(), options); addOptionIfSetAndNotDefault("onitemdragstop", schedule.getOnitemdragstop(), options); addOptionIfSetAndNotDefault("onitemmouseover", schedule.getOnitemmouseover(), options); addOptionIfSetAndNotDefault("onitemmouseout", schedule.getOnitemmouseout(), options); addOptionIfSetAndNotDefault("onviewchange", schedule.getOnviewchange(), options); addOptionIfSetAndNotDefault("onviewdisplay", schedule.getOnviewdisplay(), options); addOptionIfSetAndNotDefault("onbeforedateselect", schedule.getOnbeforedateselect(), options); addOptionIfSetAndNotDefault("ondateselect", schedule.getOndateselect(), options); addOptionIfSetAndNotDefault("onbeforedaterangeselect", schedule.getOnbeforedaterangeselect(), options); addOptionIfSetAndNotDefault("ondaterangeselect", schedule.getOndaterangeselect(), options); addOptionIfSetAndNotDefault("ondaterangechange", schedule.getOndaterangechange(), options); if (schedule.getDate() != null) { Calendar calendar = Calendar.getInstance(); calendar.setTime(schedule.getDate()); options.put("year", calendar.get(Calendar.YEAR)); options.put("month", calendar.get(Calendar.MONTH)); options.put("date", calendar.get(Calendar.DATE)); } options.put("events", schedule.getScheduleData(null, null)); return options; }Well, not much to see here. It just gets data from AbstractSchedule and puts it under “events” key. So let’s see that AbstractSchedule.getScheduleData:
public List<Map<String, Object>> getScheduleData(Date startDate, Date endDate) { /** * Locale must be US because this is the format the javascript widget supports */ DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US); format.setLenient(false); DataModel dataModel = getDataModel(); final List<Object> rowKeys = new ArrayList<Object>(); if (dataModel instanceof ExtendedDataModel) { DataVisitor visitor = new DataVisitor() { public DataVisitResult process(FacesContext context, Object rowKey, Object argument) { rowKeys.add(rowKey); return DataVisitResult.CONTINUE; } }; ((ExtendedDataModel) dataModel).walk(getFacesContext(), visitor, new DateRange(startDate, endDate), null); } ELContext elContext = (ELContext) getFacesContext().getELContext(); ValueExpression valueExpression = getFacesContext().getApplication().getExpressionFactory() .createValueExpression(elContext, "#{" + getVar() + "}", Object.class); List<Map<String, Object> data = new ArrayList<Map<String, Object>>(); if(dataModel instanceof ExtendedDataModel) { for(Object rowKey:rowKeys) { ((ExtendedDataModel) dataModel).setRowKey(rowKey); addItem(format, dataModel, elContext, valueExpression, data); } } else { for (int i = 0; i < dataModel.getRowCount(); i++) { dataModel.setRowIndex(i); addItem(format, dataModel, elContext, valueExpression, data); } } valueExpression.setValue(elContext, null); return data; }In order to iterate over items we will use dataModel returned by getDataModel method, which converts whatever is bound to “data” attribute into dataModel and caches it. In pure JSF there is only DataModel interface which assumes it iterates over something similar to List (collection indexed with integers). RichFaces provide more flexible interface called ExtendedDataModel which is very good for i.e. db pagination. The way we iterate over each one is a bit different and that’s why we introduced additional method “addItem” which we present below.
private void addItem(DateFormat format, DataModel dataModel, ELContext elContext, ValueExpression valueExpression, List<Map<String, Object> data) { valueExpression.setValue(elContext, dataModel.getRowData()); Map<String, Object> firstDataElement = new HashMap<String, Object>(); for (UIComponent child : getChildren()) { if (child instanceof AbstractScheduleItem) { AbstractScheduleItem item = (AbstractScheduleItem) child; if (!item.isRendered()) { continue; } firstDataElement.put("id", item.getEventId()); firstDataElement.put("title", item.getTitle()); if (item.isAllDay() != null) { firstDataElement.put("allDay", item.isAllDay()); } firstDataElement.put("start", format.format(item.getStartDate())); if (item.getEndDate() != null) { firstDataElement.put("end", format.format(item.getEndDate())); } if (item.getUrl() != null) { firstDataElement.put("url", item.getUrl()); } if (item.getStyleClass() != null) { firstDataElement.put("className", item.getStyleClass()); } if (item.isEditable() != null) { firstDataElement.put("editable", item.isEditable()); } if (item.getData() != null) { firstDataElement.put("data", item.getData()); } if (item.getColor() != null) { firstDataElement.put("color", item.getColor()); } if (item.getBackgroundColor() != null) { firstDataElement.put("backgroundColor", item.getBackgroundColor()); } if (item.getBorderColor() != null) { firstDataElement.put("borderColor", item.getBorderColor()); } if (item.getTextColor() != null) { firstDataElement.put("textColor", item.getTextColor()); } data.add(firstDataElement); } } }Once we’ve got DataModel we just iterate over it. Look back at what scheduleItem attribtes are bound to. It’s something defined by “var” attribute. So for each item in dataModel we need to place it in EL context under name from “var”. At the end of the loop we also must remove it, or even better, restore original value. Now we can simply iterate over child component and simply call their getters. We will represent each item as HashMap since RichFaces can easily convert it to JSON, which is an expected format. Last but not least, we need to create new component “scheduleItem”, which is a very simple values holder:
@JsfComponent(tag = @Tag(name = "scheduleItem", type = TagType.Facelets)) public abstract class AbstractScheduleItem extends UIComponentBase { public static final String COMPONENT_TYPE = "org.richfaces.ScheduleItem"; public static final String COMPONENT_FAMILY = "org.richfaces.Schedule"; public static final boolean DEFAULT_ALL_DAY = true; public static final boolean DEFAULT_EDITABLE = AbstractSchedule.DEFAULT_EDITABLE; @Attribute public abstract String getStyleClass(); @Attribute(required = true) public abstract String getTitle(); @Attribute public abstract Date getStartDate(); @Attribute public abstract Date getEndDate(); @Attribute(required = true) public abstract String getEventId(); @Attribute(defaultValue = "DEFAULT_ALL_DAY") public abstract Boolean isAllDay(); @Attribute public abstract String getUrl(); @Attribute public abstract Boolean isEditable(); @Attribute public abstract Object getData(); }ExtendedDataModel accepts Range interface which should be used to limit number of returned items. We introduce DateRange class which uses dates as boundries.
public class DateRange implements Range { private Date startDate; private Date endDate; public DateRange() { } public DateRange(Date startDate, Date endDate) { setStartDate(startDate); setEndDate(endDate); } public Date getStartDate() { return startDate; } public void setStartDate(Date startDate) { this.startDate = startDate; } public Date getEndDate() { return endDate; } public void setEndDate(Date endDate) { this.endDate = endDate; } }That’s it. Update your demo and see the results.
Events
Schedule component triggers lots of events, probably more then any other component I know. If it works in client mode then only JavaScript callbacks are executed. If it works in server or ajax mode then both JavaScript callbacks and server side listeners are involved. Server side callback is invoked simply by sending request to the server. It is of course a JSF post-back request and can be either a regular form submit with full page refresh or an Ajax request which may be tailored to re-render particular parts of the page. RichFaces provide easy to use functions to submit the form, both regularly or with Ajax. Let’s look at them:RichFaces.submitForm(form, parameters, target);Parameter “form” may be either a jQuery ID (preceeded with # sign) or reference to form element (i.e. document.getElementById(...)). Parameter “parameters” is request parameters map. It may be new parameter or it may override existing input value (when form is submitted then params are values of input fields). “target” parameter is optional and is used to override form’s “target” attribute.
RichFaces.ajax(source, event, options);Parameter “source” is client id of component triggering the event. Parameter “event” is JavaScript event that triggered the call to RichFaces.ajax. Parameter “options” is a bit complex. It may contain “queueId”, request “parameters”, “complete”, which is a callback function that will be called when request finishes, and others. Ok, so now we know how to send a request to the server. But how does it know which listener should be notified? When we submit a form, all we do is sending a list of request parameters and their values. This is what we can customize. Let’s see a simple example. A form with single input and 2 buttons, one to save data and one to restore original values. Her is a facelet code:
<h:form id="f"> <h:inputtext id="name" value="#{bean.name}"> <h:commandbutton action="#{bean.save}" id="save" value="Save"/> <h:commandbutton action="#{bean.restore}" id="restore" value="Restore"/> </h:form>When we type "Jack" into "name" input and hit save button then request parameters will look like this:
- f=f
- f:name=Jack
- f:save=f:save
I think now it’s clear how we inform our component on the server side that some event has been triggered on client side. We just send appropriate params and their values.
Dealing with events on server side is split into two steps: recognizing event and processing it. Component shouldn’t notify listeners just when it finds the right request params. It should rather create a new event object, specify in which phase it should be processed and put it into queue. It’s because i.e. action events should usually be executed when validation is OK (so it’s scheduled for InvokeApplication phase which may be skipped in case of errors in previous phases). That’s what phases are for.
So recognizing events is performed during one of earliest phases. JSF fires “decode” method on component. Methods that deal directly with generating markup or are very close to it are usually delegated to renderer.
So actually we should implement “decode” method in ScheduleRendererBase class. That method checks request params and creates appropriate event object and puts it in a queue. Then we should implement “broadcast” method on AbstractSchedule. That method will be triggered by JSF during appropriate phase and is responsible for notifying different listeners.
Let’s look at the code.
@Override public void decode(FacesContext context, UIComponent component) { if (!component.isRendered()) { return; } Map<String, Object> requestParameterMap = context.getExternalContext().getRequestParameterMap(); if (requestParameterMap.get(component.getClientId(context)) != null) { String startDateParam = requestParameterMap.get(getFieldId(context, (AbstractSchedule) component, START_DATE_PARAM)); String endDateParam = requestParameterMap.get(getFieldId(context, (AbstractSchedule) component ,END_DATE_PARAM)); String itemIdParam = requestParameterMap.get(getFieldId(context, (AbstractSchedule) component, ITEM_ID_PARAM)); String dayDeltaParam = requestParameterMap.get(getFieldId(context, (AbstractSchedule) component, DAY_DELTA_PARAM)); String minuteDeltaParam = requestParameterMap.get(getFieldId(context, (AbstractSchedule) component, MINUTE_DELTA_PARAM)); String allDayParam = requestParameterMap.get(getFieldId(context, (AbstractSchedule) component, ALL_DAY_PARAM)); String eventTypeParam = requestParameterMap.get(getFieldId(context, (AbstractSchedule) component, EVENT_TYPE_PARAM)); String viewParam = requestParameterMap.get(getFieldId(context, (AbstractSchedule) component, VIEW_PARAM)); try { if (DATE_RANGE_CHANGED_EVENT.equals(eventTypeParam)) { Date startDate = DATE_FORMAT.parse(startDateParam); Date endDate = DATE_FORMAT.parse(endDateParam); new ScheduleDateRangeChangeEvent(component, startDate, endDate).queue(); } else if (ITEM_MOVE_EVENT.equals(eventTypeParam)) { int dayDelta = Integer.parseInt(dayDeltaParam); int minuteDelta = Integer.parseInt(minuteDeltaParam); boolean allDay = Boolean.parseBoolean(allDayParam); new ScheduleItemMoveEvent(component, itemIdParam, dayDelta, minuteDelta, allDay).queue(); } else if (ITEM_RESIZE_EVENT.equals(eventTypeParam)) { int dayDelta = Integer.parseInt(dayDeltaParam); int minuteDelta = Integer.parseInt(minuteDeltaParam); new ScheduleItemResizeEvent(component, itemIdParam, dayDelta, minuteDelta).queue(); } else if (ITEM_SELECTED_EVENT.equals(eventTypeParam)) { new ScheduleItemSelectEvent(component, itemIdParam).queue(); } else if (VIEW_CHANGED_EVENT.equals(eventTypeParam)) { new ScheduleViewChangeEvent(component, viewParam).queue(); } else if (DATE_SELECTED_EVENT.equals(eventTypeParam)) { Date startDate = DATE_FORMAT.parse(startDateParam); boolean allDay = Boolean.parseBoolean(allDayParam); new ScheduleDateSelectEvent(component, startDate, allDay).queue(); } else if (DATE_RANGE_SELECTED_EVENT.equals(eventTypeParam)) { Date startDate = DATE_FORMAT.parse(startDateParam); Date endDate = DATE_FORMAT.parse(endDateParam); boolean allDay = Boolean.parseBoolean(allDayParam); new ScheduleDateRangeSelectEvent(component, startDate, endDate, allDay).queue(); } } catch (ParseException ex) { throw new FacesException("Cannot convert request parmeters", ex); } } }I think the code is self explanatory. There is a bunch of events that can be triggered by user. We check for attribute (if client id is “f:schedule”) “f:schedule:eventType” which tells us which event it really is (our JavaScript code should set appropriate value of that param when sending a request) and then we parse additional attributes, create event object of the right type and queue it. Of course we have to write all those event classes. Here is ScheduleItemResizeEvent class to give you a picture how it looks like:
public class ScheduleItemResizeEvent extends FacesEvent { private String eventId; private int dayDelta; private int minuteDelta; public ScheduleItemResizeEvent(UIComponent component, String eventId, int dayDelta, int minuteDelta) { super(component); this.eventId = eventId; this.dayDelta = dayDelta; this.minuteDelta = minuteDelta; } public boolean isAppropriateListener(FacesListener facesListener) { return facesListener instanceof ScheduleItemResizeListener; } public void processListener(FacesListener facesListener) { ((ScheduleItemResizeListener) facesListener).itemResize(this); } public String getEventId() { return eventId; } public int getDayDelta() { return dayDelta; } public int getMinuteDelta() { return minuteDelta; } @Override public String toString() { return getClass().getSimpleName() + "[eventId=" + eventId + ";dayDelta=" + dayDelta + ";minuteDelta=" + minuteDelta + "]"; } }Events should extend FacesEvent. They should also override isAppropriateListener and processListener methods. Here is the code for such listener:
public interface ScheduleItemResizeListener extends FacesListener { void itemResize(ScheduleItemResizeEvent event); }Simple, isn’t it? Well this is required if you want to be able to attach additional listeners different than the one bound to attributes.
<schedule:schedule itemResizeListener="#{someBean.itemResized}"/>
<schedule:schedule> <schedule:itemResizeListener binding="#{someBean.additionalListener}"/> </schedule:schedule>We want to be able to use both methods, so we should also modify a bit annotation of AbstractSchedule:
@JsfComponent(tag = @Tag(name = "schedule", handler = "org.richfaces.view.facelets.html.ScheduleTagHandler", generate = true, type = TagType.Facelets), renderer = @JsfRenderer(family = AbstractSchedule.COMPONENT_FAMILY, type = ScheduleRendererBase.RENDERER_TYPE), fires = { @Event(value = ScheduleDateRangeChangeEvent.class, listener = ScheduleDateRangeChangeListener.class), @Event(value = ScheduleDateRangeSelectEvent.class, listener = ScheduleDateRangeSelectListener.class), @Event(value = ScheduleDateSelectEvent.class, listener = ScheduleDateSelectListener.class), @Event(value = ScheduleItemMoveEvent.class, listener = ScheduleItemMoveListener.class), @Event(value = ScheduleItemResizeEvent.class, listener = ScheduleItemResizeListener.class), @Event(value = ScheduleItemSelectEvent.class, listener = ScheduleItemSelectListener.class), @Event(value = ScheduleViewChangeEvent.class, listener = ScheduleViewChangeListener.class) } ) public abstract class AbstractSchedule extends UIComponentBase implements ScheduleCommonViewAttributes { //... }We’ve added “fires” attribute which is an array of @Event annotations. This will generate methods for attaching the listeners. Next let’s see broadcast method.
@Override public void broadcast(FacesEvent event) throws AbortProcessingException { if (event instanceof ScheduleDateRangeChangeEvent) { super.broadcast(event); ScheduleDateRangeChangeEvent calendarAjaxEvent = (ScheduleDateRangeChangeEvent) event; FacesContext facesContext = getFacesContext(); MethodExpression expression = getDateRangeChangeListener(); if (expression != null) { expression.invoke(facesContext.getELContext(), new Object[]{event}); } setResponseData(getScheduleData(calendarAjaxEvent.getStartDate(), calendarAjaxEvent.getEndDate())); } else if (event instanceof ScheduleItemMoveEvent) { FacesContext facesContext = getFacesContext(); MethodExpression expression = getItemMoveListener(); boolean allow = true; if (expression != null) { Object result = expression.invoke(facesContext.getELContext(), new Object[]{event}); allow = (Boolean) result; } setResponseData(allow); super.broadcast(event); } else if (event instanceof ScheduleItemResizeEvent) { FacesContext facesContext = getFacesContext(); MethodExpression expression = getItemResizeListener(); boolean allow = true; if (expression != null) { Object result = expression.invoke(facesContext.getELContext(), new Object[]{event}); allow = ((Boolean) result); } setResponseData(allow); super.broadcast(event); } else if (event instanceof ScheduleItemSelectEvent) { super.broadcast(event); FacesContext facesContext = getFacesContext(); MethodExpression expression = getItemSelectListener(); if (expression != null) { expression.invoke(facesContext.getELContext(), new Object[]{event}); } } else if (event instanceof ScheduleViewChangeEvent) { super.broadcast(event); FacesContext facesContext = getFacesContext(); MethodExpression expression = getViewChangeListener(); if (expression != null) { expression.invoke(facesContext.getELContext(), new Object[]{event}); } } else if (event instanceof ScheduleDateSelectEvent) { super.broadcast(event); FacesContext facesContext = getFacesContext(); MethodExpression expression = getDateSelectListener(); if (expression != null) { expression.invoke(facesContext.getELContext(), new Object[]{event}); } } else if (event instanceof ScheduleDateRangeSelectEvent) { super.broadcast(event); FacesContext facesContext = getFacesContext(); MethodExpression expression = getDateRangeSelectListener(); if (expression != null) { expression.invoke(facesContext.getELContext(), new Object[]{event}); } } else { super.broadcast(event); } }Note that additional listeners are fired by “super.broadcast” (those attached via nested tag). Schedule component has attributes for binding method expressions that should be evaluated on different events. That we must do on our own so as you see we call i.e. getItemResizeListener, if anything is bound to this attribute then it will return non-null value. We evaluate that expression. Note the sequence, we firstly evaluate that expression and then invoke super.broadcast. This is different than for i.e. ScheduleDateRangeSelectEvent where we call those listeners first. Generally there should be no guarantee of sequence in which we fire listeners, however here we want to allow user to attach method expression that can return boolean value. This way user can decide on server side if particular action is allowed. Well, the code above is not perfect because if user binds method that returns void then result of method evaluation will be null and casting it to boolean will throw NPE. It would be better like that:
boolean allow = true; if (expression != null) { Object result = expression.invoke(facesContext.getELContext(), new Object[]{event}); if(result!=null) { allow = ((Boolean) result); } }Note the “setResponseData(allow)” method. This will attach value of “allow” to response data, so that JavaScript callbacks on client can react if server side forbids triggered action. Here is that methods body:
private void setResponseData(Object data) { ExtendedPartialViewContext.getInstance(getFacesContext()).getResponseComponentDataMap().put(getClientId(getFacesContext()), data); }Remeber that data will be converted to JSON, so it should be one of supported types (primitives, Map,List), or have appropriate toString implementation. OK, now down to JavaScript facade. We will focus here on item resize event. Our widget provides following hooks:
- eventResize
- eventResizeStart
- eventResizeStop
(function ($, rf) { // Create (for example) ui container for our component class rf.ui = rf.ui || {}; // Default options definition if needed for the component // var defaultOptions = {}; var SUBMIT_EVENT_FUNCTION = 'submitEventFunction'; // Extending component class with new properties and methods using extendClass // $super - reference to the parent prototype, will be available inside those methods rf.ui.Schedule = rf.BaseComponent.extendClass({ init: function (componentId, options) { //.... options = jQuery.extend({ eventResizeStart: function(item, event, ui, view) { _this.__itemResizeStart(item, event, ui, view) }, eventResizeStop: function(item, event, ui, view) { _this.__itemResizeStop(item, event, ui, view) }, eventResize: function(item, dayDelta, minuteDelta, revertFunc, event, ui, view) { _this.__itemResized(item, dayDelta, minuteDelta, revertFunc, event, ui, view) } }, options); var _this = this; jQuery(function() { jQuery(document.getElementById(_this.id)).fullCalendar(options); }); }, __itemResizeStart : function(item, event, ui, view){...}, __itemResizeStop : function(item, event, ui, view){...}, __itemResized : function(item, dayDelta, minuteDelta, revertFunc, event, ui, view){...}, __getResponseComponentData : function(event) {...}, __executeInlineEventHandler : function(eventName, context) {...} }); })(jQuery, RichFaces);In order to bind our callbacks (__itemResizeStart,__itemResizeStop,__itemResized) we need to extend “options” in the “init” method. It’s just a simple delegation. First two methods are simple JavaScript callbacks. User may provide some JavaScript code (bound to onitemresizestart or onitemresizestop attributes), so we need to evaluate it, since it will be string. We’ve created separate method for that called __executeInlineEventHandler. Here is the code of first two methods:
__itemResizeStart : function(item, event, ui, view) { this.__executeInlineEventHandler('onitemresizestart', { 'item':item, 'event':event, 'ui':ui, 'view':view }); }, __itemResizeStop : function(item, event, ui, view) { this.__executeInlineEventHandler('onitemresizestop', { 'item':item, 'event':event, 'ui':ui, 'view':view }); },Things get more complicated when we speak about __itemResized. Firstly this method needs to invoke JavaScript bound to “onbeforeitemresize” attribute. If that script returns “false” then we tell widget to revert changes, cause action is forbidden and we stop further execution. If no script is bound to “onbeforeitemresize”, or it doesn’t return exactly “false” then we need to send request to the serve. In case of ajax request we need to see the response, cause server side listeners may decide to forbid action. Finally we need to call JavaScript bound to “onitemresize”. Ok, how do we make request? Well we’ve talked about it earlier. We should use one of RichFaces.ajax or RichFaces.submitForm. However those functions should be generated by server side code. So when we generate initial JavaScript that bootstraps the component we need to pass appropriate function in options. We will call it “submitEventFunction” and reuse it for sending any type of event. Here is how it will look like in generated code:
new RichFaces.ui.Schedule(“f:schedule”, {"submitEventFunction": function(event,view,eventType,itemId,startDate,endDate,dayDelta,minuteDelta,allDay,callback){RichFaces.ajax("f:schedule",event,{"complete":callback,"parameters":{"f:schedule:allDay":allDay,"f:schedule:endDate":endDate,"f:schedule:minuteDelta":minuteDelta,"f:schedule:eventType":eventType,"f:schedule:startDate":startDate,"f:schedule":"f:schedule","f:schedule:dayDelta":dayDelta,"f:schedule:view":view,"f:schedule:itemId":itemId} ,"incId":"1"} )},...});When we will call that method we will want to pass following parameters:
- view (agendaWeek,month,etc.)
- eventType (so that server knows what event is triggered)
- itemId
- startDate
- endDate
- dayDelta
- minuteDelta
- allDay
- event (JavaScript event)
- callback (function that should be called when Ajax request finishes)
__itemResized : function(item, dayDelta, minuteDelta, revertFunc, event, ui, view) { var result = this.__executeInlineEventHandler('onbeforeitemresize', { 'item':item, 'dayDelta':dayDelta, 'minuteDelta':minuteDelta, 'event':event, 'ui':ui, 'view':view }); if (result === false) { revertFunc(); return; } if (this.options[SUBMIT_EVENT_FUNCTION] != null) { var _this = this; this.options[SUBMIT_EVENT_FUNCTION](event, null, 'itemResize', item.id, null, null, dayDelta, minuteDelta, null, function(event) { var decision = _this.__getResponseComponentData(event); var vetoed = false; if (decision != undefined && decision !== true) { revertFunc(); vetoed = true; } _this.__executeInlineEventHandler('onitemresize', { 'item':item, 'dayDelta':dayDelta, 'minuteDelta':minuteDelta, 'event':event, 'ui':ui, 'view':view, 'data':decision, 'vetoed':vetoed }); } ); } else { this.__executeInlineEventHandler('onitemresize', { 'item':item, 'dayDelta':dayDelta, 'minuteDelta':minuteDelta, 'event':event, 'ui':ui, 'view':view, 'data':null, 'vetoed':null }); } },What’s worth noticing here is __getResponseComponentData, our private method for retrieving custom response data for our component:
__getResponseComponentData : function(event) { return event.componentData[this.id]; },Other thing to note is we prefix methods with two underscores, this is a convention for private methods, that may be subject for change and the end user should not rely on them. OK, now lets see how we create that submitEventFunction. We need to modify ScheduleRendererBase.writeInitFunction
protected void writeInitFunction(FacesContext context, UIComponent component) throws IOException { AbstractSchedule schedule = (AbstractSchedule) component; ResponseWriter writer = context.getResponseWriter(); String clientId = schedule.getClientId(context); Locale locale = context.getViewRoot().getLocale(); String widgetVar = schedule.getWidgetVar(); if (widgetVar != null) { writer.writeText("var " + widgetVar + " = ", null); } final Map<String, Object> options = getOptions(schedule); options.put("locale", locale.toString()); options.put("submitEventFunction", createSubmitEventFunction(context, schedule)); writer.writeText(new JSObject("RichFaces.ui.Schedule", clientId, options).toScript(), null); }We simply add another entry to options.
protected Object createSubmitEventFunction(FacesContext context, AbstractSchedule component) { ScriptString jsFunction; Map<String, Object> params = new HashMap<String, Object>(); params.put(getFieldId(context, component, START_DATE_PARAM), new JSReference(START_DATE_PARAM)); params.put(getFieldId(context, component, END_DATE_PARAM), new JSReference(END_DATE_PARAM)); params.put(getFieldId(context, component, ITEM_ID_PARAM), new JSReference(ITEM_ID_PARAM)); params.put(getFieldId(context, component, DAY_DELTA_PARAM), new JSReference(DAY_DELTA_PARAM)); params.put(getFieldId(context, component, MINUTE_DELTA_PARAM), new JSReference(MINUTE_DELTA_PARAM)); params.put(getFieldId(context, component, ALL_DAY_PARAM), new JSReference(ALL_DAY_PARAM)); params.put(getFieldId(context, component, EVENT_TYPE_PARAM), new JSReference(EVENT_TYPE_PARAM)); params.put(getFieldId(context, component, VIEW_PARAM), new JSReference(VIEW_PARAM)); String clientId = component.getClientId(); params.put(clientId, clientId); if (isAjaxMode(component)) { AjaxFunction ajaxFunction = AjaxRendererUtils.buildAjaxFunction(context, component); ajaxFunction.getOptions().getParameters().putAll(params); ajaxFunction.getOptions().set("complete", new JSReference(CALLBACK)); jsFunction = ajaxFunction; } else if (SwitchType.server.equals(component.getSwitchType())) { jsFunction = new JSFunction("RichFaces.submitForm", "#" + RendererUtils.getInstance().getNestingForm(context, component).getClientId(context), params, ""); } else { return null; } return new JSFunctionDefinition("event", VIEW_PARAM, EVENT_TYPE_PARAM, ITEM_ID_PARAM, START_DATE_PARAM, END_DATE_PARAM, DAY_DELTA_PARAM, MINUTE_DELTA_PARAM, ALL_DAY_PARAM, CALLBACK).addToBody(jsFunction); }Depending on mode (ajax,client,server) the component operates in we return different result. For client mode we return null, because we don’t want client to do any requests. In server mode we build JSFunction manually. For ajax mode we should use special function from AjaxRendererUtils. Both JSFunction and JSFunctionDefinition are special RichFaces classes that help output proper JavaScript code. You can play with those classes to see how they work (see their toString() results). I think that’s if for events. You can try to implement the rest of events yourself for practice.
Pagination in Ajax and server modes
Schedule component displays ranges of items. Smallest range covers single day and widest covers one month. The widget however can navigate back and forth. It has also been so well designed to allow different ways of providing items data. One option that suits us best is registering callback function that accepts start and end dates as params and, when called, should return array of items for that range. Doesn’t it sound familiar to events? In case of client mode we have no option but to load all the items at bootstrap. In server mode we rerender the whole component so we still may just pass items as init options. However for ajax mode we can use the callback mentioned before.(function ($, rf) { // Create (for example) ui container for our component class rf.ui = rf.ui || {}; // Default options definition if needed for the component // var defaultOptions = {}; var SUBMIT_EVENT_FUNCTION = 'submitEventFunction'; // Extending component class with new properties and methods using extendClass // $super - reference to the parent prototype, will be available inside those methods rf.ui.Schedule = rf.BaseComponent.extendClass({ // class name name:"Schedule", init: function (componentId, options) { if (!document.getElementById(componentId)) { throw "No element with id '" + componentId + "' found."; } this.options = options; // call constructor of parent class if needed $super.constructor.call(this, componentId); // attach component object to DOM element for // future cleaning and for client side API calls this.attachToDom(this.id); // ... /** * Message bundle & event handlers setup. */ options = jQuery.extend({ events: function(startDate, endDate, callback) { _this.__dateRangeChange(startDate, endDate, callback) }, …}); //… }, __dateRangeChange : function(startDate, endDate, callback) { var firstInvocation = this.options.initialItems != null; if (firstInvocation) { var startDateData = this.options.initialItems.startDate; var endDateData = this.options.initialItems.endDate; var initialStartDate = new Date(startDateData['year'], startDateData.month, startDateData.date); var initialEndDate = new Date(endDateData['year'], endDateData.month, endDateData.date); var items = this.options.initialItems.items; /** * After initial load this should be cleaned so items are not cached. */ this.options.initialItems = null; /** * In case the JSF component made a mistake in calculating initial * date range we don't use initial items and just continue. */ if (this.__isSameDay(startDate, initialStartDate) && this.__isSameDay(endDate, initialEndDate)) { callback(items); return; } } if (this.options[SUBMIT_EVENT_FUNCTION] != null) { var _this = this; this.options[SUBMIT_EVENT_FUNCTION]({} /* stub event */, null, 'dateRangeChange', null, this.__formatDateParam(startDate), this.__formatDateParam(endDate), null, null, null, function(event) { var data = _this.__getResponseComponentData(event); if (data != undefined) { callback(data); } _this.__executeInlineEventHandler('ondaterangechange', { 'startDate':startDate, 'endDate':endDate, 'event':event, 'items':data }); } ); } else if (!firstInvocation) { this.__executeInlineEventHandler('ondaterangechange', { 'startDate':startDate, 'endDate':endDate, 'event':null, 'items':null }); } }, //... }); rf.ui.Schedule.prototype.messages = []; // define super class reference - reference to the parent prototype var $super = rf.ui.Schedule.$super; })(jQuery, RichFaces);The only oddity here is that firstInvocation variable. It’s because when widget initializes it fires the dateRangeChange event and we want to avoid additional request on first component rendering because we’ve already provided items for initial date range in init options. Well, we did if we’d modified ScheduleRendererBase and AbstractSchedule before.
protected Map<String, Object> getOptions(AbstractSchedule schedule) throws IOException { //.... if (!isClientMode(schedule)) { Map<String, Object> initialItems = new HashMap<String, Object>(); Date startDate = AbstractSchedule.getFirstDisplayedDay(schedule); Date endDate = AbstractSchedule.getLastDisplayedDate(schedule); initialItems.put("items", schedule.getScheduleData(startDate, endDate)); Map<String, Object> date = new HashMap<String, Object>(); Calendar calendar = Calendar.getInstance(); calendar.setTime(startDate); date.put("year", calendar.get(Calendar.YEAR)); date.put("month", calendar.get(Calendar.MONTH)); date.put("date", calendar.get(Calendar.DATE)); initialItems.put("startDate", date); date = new HashMap<String, Object>(); calendar.setTime(endDate); date.put("year", calendar.get(Calendar.YEAR)); date.put("month", calendar.get(Calendar.MONTH)); date.put("date", calendar.get(Calendar.DATE)); initialItems.put("endDate", date); options.put("initialItems", initialItems); } else { options.put("events", schedule.getScheduleData(null, null)); } return options; }We need that twisted logic with AbstractSchedule.getFirstDisplayedDay(schedule); and AbstractSchedule.getLastDisplayedDate(schedule); in order to calculate appropriate date range for initial rendering, which depends on view and other schedule settings. Processing of ScheduleDateRangeChangeEvent is shown here:
@Override public void broadcast(FacesEvent event) throws AbortProcessingException { if (event instanceof ScheduleDateRangeChangeEvent) { super.broadcast(event); ScheduleDateRangeChangeEvent calendarAjaxEvent = (ScheduleDateRangeChangeEvent) event; FacesContext facesContext = getFacesContext(); MethodExpression expression = getDateRangeChangeListener(); if (expression != null) { expression.invoke(facesContext.getELContext(), new Object[]{event}); } setResponseData(getScheduleData(calendarAjaxEvent.getStartDate(), calendarAjaxEvent.getEndDate())); } //...It’s exactly the same as with events. It is an event actually. So we just fill the response data with list of items for new date range.
Internationalization
Schedule component is full of various labels like names of days of week, months, button labels and so on. It also presents dates and time, which are displayed differently depending on a country or region. Our widget allows us to customize both date formats and labels. All we need to do is provide them in options during component initialization. Simple, but if we’d like to include all those labels in each request we need to render the component then we will add quite a lot of data, which will slow down our application. I’ve decided to dynamically generate JavaScript file that will contain all the translations. Thanks to this the browser is able to cache it. Here is part of that file:RichFaces.ui.Schedule.prototype.messages=jQuery.extend(RichFaces.ui.Schedule.prototype.messages,{'en':{allDayText:'All day',...}});Then in our JavaScript facade’s init method we do this:
options = jQuery.extend({ events: function(startDate, endDate, callback) { _this.__dateRangeChange(startDate, endDate, callback) }, ... }, this.messages[options['locale']], options);Generating such file is very simple. We need to create class that extends AbstractChacheableResource and annotate it with @DynamicResousrce:
@DynamicResource public class ScheduleMessages extends AbstractCacheableResource { public static final String BUNDLE_NAME = "org.richfaces.component.UIScheduleMessages"; private static final String MESSAGE_KEY_BASE = "org.richfaces.component.UISchedule."; private static final Logger LOG = LogFactory.getLogger(ScheduleMessages.class); public ScheduleMessages() { setContentType(HtmlConstants.JAVASCRIPT_TYPE); } @Override public InputStream getInputStream() throws IOException { ClassLoader loader = getClassLoader(); FacesContext facesContext = FacesContext.getCurrentInstance(); Application application = facesContext.getApplication(); StringBuilder out = new StringBuilder(); out.append("RichFaces.ui.Schedule.prototype.messages=jQuery.extend(RichFaces.ui.Schedule.prototype.messages,{"); Iterator<locale> supportedLocales = application.getSupportedLocales(); int localeCount = 0; while (supportedLocales.hasNext()) { localeCount++; Locale locale = supportedLocales.next(); ResourceBundle applicationBundle = ResourceBundle.getBundle(application.getMessageBundle(), locale, loader); ResourceBundle stockBundle = ResourceBundle.getBundle(BUNDLE_NAME, locale, loader); String[] months = new String[]{"JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER"}; String[] days = new String[]{"SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"}; out.append("'").append(locale.toString()).append("':{"); out.append("allDayText:'").append(escape(getMessageFromBundle(MESSAGE_KEY_BASE + "allDay", applicationBundle, stockBundle))).append("',"); appendArray(out, applicationBundle, stockBundle, "monthNames", "monthNames", months); out.append(","); appendArray(out, applicationBundle, stockBundle, "monthNamesShort", "monthNamesShort", months); out.append(","); appendArray(out, applicationBundle, stockBundle, "dayNames", "dayNames", days); out.append(","); appendArray(out, applicationBundle, stockBundle, "dayNamesShort", "dayNamesShort", days); out.append(","); appendMap(out, applicationBundle, stockBundle, "buttonText", "buttonTexts", new String[]{"prev", "next", "prevYear", "nextYear", "today", "month", "day", "week"}); out.append("},"); } if (localeCount > 0) { out.delete(out.length() - 1, out.length()); } out.append("})"); try { return new ByteArrayInputStream(out.toString().getBytes(application.getViewHandler() .calculateCharacterEncoding(facesContext))); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private void appendArray(StringBuilder out, ResourceBundle applicationBundle, ResourceBundle stockBundle, String jsPropertyName, String prefix, String[] keys) { String key; out.append(jsPropertyName).append(":["); for (int i = 0; i < keys.length; i++) { key = MESSAGE_KEY_BASE + prefix + "." + keys[i]; out.append("'").append(escape(getMessageFromBundle(key, applicationBundle, stockBundle))).append("'"); if (i + 1 < keys.length) { out.append(","); } } out.append("]"); } private void appendMap(StringBuilder out, ResourceBundle applicationBundle, ResourceBundle stockBundle, String jsPropertyName, String prefix, String[] keys) { String key; out.append(jsPropertyName).append(":{"); for (int i = 0; i < keys.length; i++) { key = MESSAGE_KEY_BASE + prefix + "." + keys[i]; out.append(keys[i]).append(":").append("'").append(escape( getMessageFromBundle(key, applicationBundle, stockBundle)) ).append("'"); if (i + 1 < keys.length) { out.append(","); } } out.append("}"); } private String getMessageFromBundle(String key, ResourceBundle applicationBundle, ResourceBundle stockBundle) { try { return applicationBundle.getString(key); } catch (MissingResourceException e) { try { return stockBundle.getString(key); } catch (MissingResourceException e1) { if (LOG.isWarnEnabled()) { LOG.warn("Cannot find resource " + e1.getKey() + " in bundle " + e1.getClassName()); } return ""; } } } private String escape(String message) { return message.replaceAll("'", "\\\\'"); } }We also need to add this resource to our components dependencies:
@ResourceDependencies({ @ResourceDependency(library = "javax.faces", name = "jsf.js"), @ResourceDependency(name = "jquery.js", target = "head"), @ResourceDependency(name = "richfaces.js", target = "head"), @ResourceDependency(name = "richfaces-event.js", target = "head"), @ResourceDependency(name = "richfaces-base-component.js", target = "head"), @ResourceDependency(name = "ui.core.js", target = "head"), @ResourceDependency(name = "ui.draggable.js", target = "head"), @ResourceDependency(name = "ui.resizable.js", target = "head"), @ResourceDependency(name = "fullcalendar.js", target = "head"), @ResourceDependency(name = "richfaces.schedule.js", target = "head"), @ResourceDependency(name = "org.richfaces.renderkit.html.scripts.ScheduleMessages", target = "head"), @ResourceDependency(name = "fullcalendar.css", target = "head")}) public abstract class ScheduleRendererBase extends RendererBase {...}Default messages are taken from file UIScheduleMessages.properties located in directory org/richfaces/component on classpath (this is of course bundled in our components JAR). Of course first we try to get messages from bundles provided by user, so the application bundles and if there is no entry in there for particular message then we look for message in file shipped with component.
Customization per view
Fullcalendar widget has got lots of options which end user may set to tune the look’n’feel. However those options may be different for different views. There are 5 views: month, agendaWeek, agendaDay, basicWeek, basicDay. We would like to be able to customize our component like this:<schedule:schedule> <schedule:schedulemonthview columnformat="ddd" dragopacity=".1" timeformat="h:mt" titleformat="MMM/yyyy" weekmode="#{myBean.weekMode}"/> <schedule:scheduleagendadayview columnformat="dd/MMM" dragopacity=".3" timeformat="hh:m:s{ - hh:m:s}" titleformat="dd/MM/yyyy"/> <schedule:schedulebasicdayview columnformat="d/MM" dragopacity=".5" timeformat="h:mm:ss" titleformat="d/M/yy"/> <schedule:scheduleagendaweekview columnformat="dd/MM" dragopacity=".7" timeformat="HH:mm{ - HH:mm}" titleformat="MMM d[ yyyy]{ '—'[ MMM] d[ yyyy]}"/> <schedule:schedulebasicweekview columnformat="d/M" dragopacity=".9" timeformat="h(:mm)tt" titleformat="d/M/yy{ '—'d/M/yy}"/> </schedule:schedule>So view specific tags duplicate (actually override) attributes from our main component, the schedule. So firstly we need to create new components for each view specific attribute set:
@JsfComponent(tag = @Tag(name = "scheduleAgendaDayView", type = TagType.Facelets)) public abstract class AbstractScheduleAgendaDayView extends UIComponentBase implements ScheduleCommonViewAttributes { public static final String COMPONENT_TYPE = "org.richfaces.ScheduleAgendaDayView"; public static final String COMPONENT_FAMILY = "org.richfaces.Schedule"; }
@JsfComponent(tag = @Tag(name = "scheduleAgendaWeekView", type = TagType.Facelets)) public abstract class AbstractScheduleAgendaWeekView extends UIComponentBase implements ScheduleCommonViewAttributes { public static final String COMPONENT_TYPE = "org.richfaces.ScheduleAgendaWeekView"; public static final String COMPONENT_FAMILY = "org.richfaces.Schedule"; }
@JsfComponent(tag = @Tag(name = "scheduleBasicDayView", type = TagType.Facelets)) public abstract class AbstractScheduleBasicDayView extends UIComponentBase implements ScheduleCommonViewAttributes { public static final String COMPONENT_TYPE = "org.richfaces.ScheduleBasicDayView"; public static final String COMPONENT_FAMILY = "org.richfaces.Schedule"; }
@JsfComponent(tag = @Tag(name = "scheduleBasicWeekView", type = TagType.Facelets)) public abstract class AbstractScheduleBasicWeekView extends UIComponentBase implements ScheduleCommonViewAttributes { public static final String COMPONENT_TYPE = "org.richfaces.ScheduleBasicWeekView"; public static final String COMPONENT_FAMILY = "org.richfaces.Schedule"; }
public interface ScheduleCommonViewAttributes { @Attribute String getTimeFormat(); @Attribute String getColumnFormat(); @Attribute String getTitleFormat(); @Attribute Double getDragOpacity(); }Now we need to update our renderer logic to override attributes set on schedule level with those set on view specific components.
protected Map<String, Object> getOptions(AbstractSchedule schedule) throws IOException { /** * Copy attributes from child view components */ for (UIComponent child : schedule.getChildren()) { if (!child.isRendered()) { continue; } String suffix = ""; if (child instanceof AbstractScheduleMonthView) { copyAttribute("weekMode", "", child, schedule); suffix = "Month"; } else if (child instanceof AbstractScheduleAgendaDayView) { suffix = "AgendaDay"; } else if (child instanceof AbstractScheduleAgendaWeekView) { suffix = "AgendaWeek"; } else if (child instanceof AbstractScheduleBasicDayView) { suffix = "BasicDay"; } else if (child instanceof AbstractScheduleBasicWeekView) { suffix = "BasicWeek"; } if (child instanceof ScheduleCommonViewAttributes) { copyAttribute("timeFormat", suffix, child, schedule); copyAttribute("columnFormat", suffix, child, schedule); copyAttribute("titleFormat", suffix, child, schedule); copyAttribute("dragOpacity", suffix, child, schedule); } } //... } private static void copyAttribute(String attribute, String suffix, UIComponent source, UIComponent target) { Object value = source.getAttributes().get(attribute); if (value != null) { target.getAttributes().put(attribute + suffix, value); } } private void addOptionHash(String attribute, UIComponent source, Map<String, Object> options) { Map<String, Object> hash = new HashMap<String, Object>(3); Map<String, Object> attributes = source.getAttributes(); addOptionIfSetAndNotDefault("month", attributes.get(attribute + "Month"), hash); addOptionIfSetAndNotDefault("basicWeek", attributes.get(attribute + "BasicWeek"), hash); addOptionIfSetAndNotDefault("agendaWeek", attributes.get(attribute + "AgendaWeek"), hash); addOptionIfSetAndNotDefault("basicDay", attributes.get(attribute + "BasicDay"), hash); addOptionIfSetAndNotDefault("agendaDay", attributes.get(attribute + "AgendaDay"), hash); addOptionIfSetAndNotDefault("", attributes.get(attribute), hash); if (hash.size() > 0) { options.put(attribute, hash); } }When we copy attributes from child components we append view name to attribute name. Finally for those "dragOpacity","timeFormat","titleFormat",etc. we use addOptionHash menthod which generates map of attribute values where key is view name.
It's likely the most complete and concrete study case on CDK that I never read: Thank you Bernard, you're a rock!
ReplyDeleteAlso, I have one question/demand : can you put the entire code project in ZIP file format or on Github.
thank you.
++
Antoine schellenberger
Antoine, the code is part of RichFaces sandbox:
Deletehttps://github.com/richfaces/sandbox
My fork:
https://github.com/blabno/sandbox
so, it's all right! thank you again for this job.
Delete