I found a Bootstrap reusable field custom control and have been using it in my latest project. It works well but the validation is done by SSJS within the Xpage going forward I am moving as much programming as I can out of the Xpage and into java beans.
I have validation working in my bean,but it puts the errors in the display errors control at the top of the form. I want my Java validation to use the Bootstrap style error.
I found a very good discussion of doing this on the BootstrapForXpages site.
Another question in StackOverflow addressed this same issue but I was not able to figure out how to get this to work.
How to use XPages Java code to set valid method of input control inside a custom control?
Here is the Field CC
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
<xp:div>
<xp:this.styleClass><![CDATA[#{javascript:"form-group" + ( getComponent("inputText1").isValid() ? "" : " has-error" )}]]></xp:this.styleClass>
<xp:label styleClass="col-sm-2 control-label" for="inputText1"
value="${compositeData.fieldLabel}" />
<div class="col-sm-10">
<xp:inputText type="text" id="inputText1"
loaded="${!empty compositeData.placeholder}" value="#{compositeData.dataSource[compositeData.fieldName]}"
required="${compositeData.required}">
<xp:this.attrs>
<xp:attr name="placeholder" value="${compositeData.placeholder}" />
</xp:this.attrs>
<xp:this.validators>
<xp:validateRequired
message="#{javascript:compositeData.fieldLabel + ' is required'}" />
</xp:this.validators>
</xp:inputText>
<xp:text escape="true" id="computedField1" styleClass="help-block"
value="${compositeData.helpText}">
<xp:this.rendered><![CDATA[#{javascript:getComponent("inputText1").isValid() && compositeData.helpText != null}]]></xp:this.rendered>
</xp:text>
<xp:message id="message1" for="inputText1" styleClass="help-block" />
</div>
</xp:div>
</xp:view>
and the code for a simple Xpage with the control on it:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core"
xmlns:xc="http://www.ibm.com/xsp/custom"
xmlns:xe="http://www.ibm.com/xsp/coreex">
<xc:ccCustomField placeholder="Enter your email address"
fieldLabel="Email" helpText="Guess what you have to enter here..."
fieldName="model">
<xc:this.dataSource>
<xe:objectData var="PCModel">
<xe:this.createObject><![CDATA[#{javascript:var pc = new com.scoular.data.PC().create();
return pc;}]]></xe:this.createObject>
</xe:objectData>
</xc:this.dataSource>
</xc:ccCustomField>
</xp:view>
Define objectData outside the custom control include and
set property dataSource to objectData's variable PCModel: dataSource="#{PCModel}"
<?xml version="1.0" encoding="UTF-8"?>
<xp:view
xmlns:xp="http://www.ibm.com/xsp/core"
xmlns:xc="http://www.ibm.com/xsp/custom"
xmlns:xe="http://www.ibm.com/xsp/coreex">
<xp:this.data>
<xe:objectData
var="PCModel">
<xe:this.createObject><![CDATA[#{javascript:
var pc = new com.scoular.data.PC();
pc.create();
return pc;
}]]></xe:this.createObject>
</xe:objectData>
</xp:this.data>
<xc:ccCustomField
placeholder="Enter your email address"
fieldLabel="Email"
helpText="Guess what you have to enter here..."
fieldName="model"
dataSource="#{PCModel}">
</xc:ccCustomField>
</xp:view>
So, it's not an issue with the bean but with setting dataSource property for custom control.
Also, be careful with bean's method create(). It won't probably return bean's instance. If that's the case then your code line
var pc = new com.scoular.data.PC().create();
won't set pc to an PC instance and dataSource won't be initialized correctly.
To pass a error message to specific field I have Utility method, to find the field and then add the error to the FacesContext referring to the clientId, which is found with the findComponent method
public static void addMessageToSpecificField(String fieldId, String message) {
String clientId = JSFUtil.findComponent(fieldId).getClientId(
JSFUtil.getFacesContext());
JSFUtil.getFacesContext().addMessage(clientId,
new javax.faces.application.FacesMessage(message));
}
/**
* Finds an UIComponent by its component identifier in the current
* component tree.
*
* #param compId the component identifier to search for
* #return found UIComponent or null
*
* #throws NullPointerException if <code>compId</code> is null
*/
public static UIComponent findComponent(String compId) {
return findComponent(FacesContext.getCurrentInstance().getViewRoot(), compId);
}
/**
* Finds an UIComponent by its component identifier in the component tree
* below the specified <code>topComponent</code> top component.
*
* #param topComponent first component to be checked
* #param compId the component identifier to search for
* #return found UIComponent or null
*
* #throws NullPointerException if <code>compId</code> is null
*/
#SuppressWarnings("unchecked")
public static UIComponent findComponent(UIComponent topComponent, String compId) {
if (compId==null)
throw new NullPointerException("Component identifier cannot be null");
if (compId.equals(topComponent.getId()))
return topComponent;
if (topComponent.getChildCount()>0) {
List<UIComponent> childComponents= topComponent.getChildren();
for (UIComponent currChildComponent : childComponents) {
UIComponent foundComponent=findComponent(currChildComponent, compId);
if (foundComponent!=null)
return foundComponent;
}
}
return null;
}
Related
Want to check if rest service control does not honour context.getSubmittedValue() for viewName property?
REST Service view name is not being computed when partialRefresh is done on parent panel.
I am trying to load different views (DataTable used for display) and need to work with JSON object. However, when I click on accordion entry which updates a panel in another custom control, I can see submitted value changing using computed field but my REST service control will still return old data. Here is the code should you want to have a look:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" xmlns:xe="http://www.ibm.com/xsp/coreex">
<xp:panel id="panelMiddle">
<xp:text escape="true" id="computedField1" value="#{javascript:context.getSubmittedValue()}"></xp:text>
<xe:restService id="restService1" pathInfo="data" preventDojoStore="true" state="false">
<xe:this.service>
<xe:viewItemFileService defaultColumns="true" contentType="application/json" compact="true" systemColumns="0">
<xe:this.viewName><![CDATA[#{javascript:var v = context.getSubmittedValue();
if (null == v){
v = application.getString("defaultView");
}
}]]></xe:this.viewName>
<xe:this.count><![CDATA[#{javascript:
var v = context.getSubmittedValue();
if (null == v){
v = application.getString("defaultView");
}
database.getView(v).getEntryCount();
}]]></xe:this.count>
</xe:viewItemFileService>
</xe:this.service>
</xe:restService>
<div id="demo"></div>
<xp:eventHandler event="onClientLoad" submit="false">
<xp:this.script><![CDATA[$(document).ready(function() {
$.ajax({
url:"New.xsp/data",
dataType:"json",
success:
function(data){
var colHeaders = [];
var tarr = [];
$.each(data.items[0], function (key, val) {
if(key.indexOf("#")==-1){
colHeaders.push({"title":key});
//colHeaders.push({"data":key});
}
});
$.each(data.items,function(v,i){
var temp = [];
$.each(i,function(k,vv){
if(k.indexOf("#")==-1){temp.push(vv)};
})
tarr.push(temp);
})
$('#demo').html( '<table cellpadding="0" cellspacing="0" border="0" class="table table-striped table-bordered table-hover" id="example"></table>' );
$('#example').DataTable( {
"data": tarr,
"columns": colHeaders
} );
}
});
});]]></xp:this.script>
</xp:eventHandler>
</xp:panel>
</xp:view>
After much trial with scope variables and computed fields, found out that only sessionScope works in this scenario. Not sure if this is an ideal solution as people may open more than one tab in a browser and it may cause conflicts.
Ideas are welcome.
I have to do some calculations which many of my controls needed, and i just want to do it once on every request and store it. (e.g. beforeRenderResponse)
I can not figure out a clean way to achieve this in a custom-control.
I do not want to use the dataContext, because i read some creepy article regarding its performance (Multiple calculations on a singe JSF-cycle).
See: http://hasselba.ch/blog/?p=1112
So i went to objectData, just to figure out, that it has the same problem than the compositeData - It is not available in any custom-control events but beforePageLoad and afterPageLoad.
See: http://hasselba.ch/blog/?p=1099
Current solutions
I got two (unsatisfying) solutions for that problem.
I marked the solutions with <!-- Solutions --> in the markup below.
Solution #1:
Abuses the property "value" of a xp:text to update the objectData.
Solution #2:
Requires to call the update() method manually.
Test-setup
XPage
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" xmlns:xc="http://www.ibm.com/xsp/custom">
<xc:dataControl />
</xp:view>
Custom-Control (dataControl)
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" xmlns:xe="http://www.ibm.com/xsp/coreex" xmlns:xc="http://www.ibm.com/xsp/custom"
beforePageLoad="#{javascript:object1.sayHello('beforePageLoad')}"
afterPageLoad="#{javascript:object1.sayHello('afterPageLoad')}"
afterRestoreView="#{javascript:object1.sayHello('afterRestoreView')}"
beforeRenderResponse="#{javascript:object1.sayHello('beforeRenderResponse')}"
afterRenderResponse="#{javascript:object1.sayHello('afterRenderResponse')}">
<xp:this.data>
<xe:objectData var="object1">
<xe:this.createObject><![CDATA[#{javascript://
print("object1 is created");
return new ds.DataTest(); }]]>
</xe:this.createObject>
</xe:objectData>
</xp:this.data>
<!-- Solutions -->
<!-- Solution #1 -->
<xp:text id="computedField1" value="#{javascript: object1.update(); return ''; }" />
<!-- Solution #2 -->
<xp:button value="dataObject update" id="updateButton">
<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
<xp:this.action><![CDATA[#{javascript:object1.update()}]]></xp:this.action>
</xp:eventHandler>
</xp:button>
<!-- /Solutions -->
<div>
Object1 current value:
<xp:text escape="true" id="output" value="#{object1.value}" />
</div>
<xp:button value="Update" id="updateButton2">
<xp:eventHandler event="onclick" submit="true" refreshMode="complete" />
</xp:button>
</xp:view>
Java-class (ds.DataTest)
package ds;
import java.io.Serializable;
public class DataTest implements Serializable {
private static final long serialVersionUID = 1L;
private String value = "";
public DataTest() { /* Constructor */ }
public void sayHello(String text){
System.out.println("Hello from: " + text);
}
public void update() {
value = Double.toString( Math.random() );
}
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
What i like to do is
<xp:view beforeRenderResponse="#{javascript:object1.update()}">
but that seems not possible, see what happens with the sayHello() calls.
First request
Followup requests
I appreciate every idea or hint :)
An important design principle for any custom control is encapsulation. If you drag it onto a canvas (page, customControl) it should work without any dependency on existing conditions.
So parameters a control uses should be, well parameters. Define them as custom parameters in your control definition.
Then you have 2 options: use a SsJS object you keep in the viewScope or use a managed bean in the viewScope.
Let's say your controls need a parameter called color, then the control would be called like this:
<xc:myControl color="#{pageBean.color}"></xc:myControl>
In your bean you could do all the computations in the constructor which is called exactly once per page (that's what you want isn't it?) or lazy loaded when needed:
public synchronized String getColor() {
if(this.color==null) {
this.initColor();
}
return this.color;
}
Lazy loading only makes sense if the values don't interdepend.
Let us know how it goes.
I’d like to have a (body) containing the attributes data-target, data-spy and data-twwttr-rendered and finally look like this:
<body data-target=".bs-docs-sidebar" data-spy="scroll" data-twttr-rendered="true">
Can you help me out?
This is not possible server side by default. You have to override the existing ViewRootRenderer.
1.
Create a new Java class which extends the existing ViewRootRenderer of XPages. This class has to override the method encodeHtmlBodyStart which generates the HTML code of the body attribute:
package ch.hasselba.jsf;
import com.ibm.commons.util.StringUtil;
import com.ibm.xsp.component.UIViewRootEx;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import java.io.IOException;
public class ViewRootRenderer extends
com.ibm.xsp.renderkit.html_basic.ViewRootRendererEx2 {
#Override
protected void encodeHtmlBodyStart(FacesContext fc, UIViewRootEx uiRoot,
ResponseWriter rw) throws IOException {
String str = null;
// begin with body element
rw.startElement("body", uiRoot);
// add style attribute
str = uiRoot.getStyle();
if (StringUtil.isNotEmpty(str)) {
rw.writeAttribute("style", str, "style");
}
// add class attribute
str = uiRoot.getStyleClass();
if (StringUtil.isNotEmpty(str)) {
rw.writeAttribute("class", str, "styleClass");
}
// add your own attributes here
rw.writeAttribute("data-target", ".bs-docs-sidebar", "data-target");
rw.writeAttribute("data-spy", "scroll", "data-spy");
rw.writeAttribute("data-twttr-rendered", "true", "data-twttr-rendered");
// add new line
writeln(rw);
}
}
As you can see, the attributes you want to add are hardcoded above. The code before the hardcoded part (style & class attribute) is required because this is the default code.
2.
Register this ViewRootRenderer in the faces-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
<render-kit>
<renderer>
<component-family>javax.faces.ViewRoot</component-family>
<renderer-type>ch.hasselba.jsf.ViewRootRenderer</renderer-type>
<renderer-class>ch.hasselba.jsf.ViewRootRenderer</renderer-class>
</renderer>
</render-kit>
</faces-config>
3.
Use the renderedType property of your XPage to add this Renderer instead of the default one:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view
xmlns:xp="http://www.ibm.com/xsp/core"
rendererType="ch.hasselba.jsf.ViewRootRenderer">
</xp:view>
This is the way to add the renderer to a specific XPage only. If you want to override it in the whole application, you have to change the existing renderer-class (Step 2).
<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
<render-kit>
<renderer>
<component-family>javax.faces.ViewRoot</component-family>
<renderer-type>com.ibm.xsp.ViewRootEx</renderer-type>
<renderer-class>ch.hasselba.jsf.ViewRootRenderer</renderer-class>
</renderer>
</render-kit>
</faces-config>
You can skip Step 3 now, but be aware that this affects every XPage in your application.
You can set the body attributes on onClientLoad event:
<xp:eventHandler
event="onClientLoad"
submit="false">
<xp:this.script><![CDATA[
document.body.setAttribute('data-target', '.bs-docs-sidebar');
document.body.setAttribute('data-spy', 'scroll');
document.body.setAttribute('data-twttr-rendered', 'true');
]]></xp:this.script>
</xp:eventHandler>
The rendered page body has then the attributes you wanted:
I have an outer repeat control that gets it's collection as a hashmap(variable name is hmOuterCollection). Inside this repeat control there is another repeat control which gets it's collection as a hashmap (variable name is hmInnerCollection) as well. The inner repeat controls collection is based on the outer repeat entry's key. What happens is, the inner repeat control entries seems to overwrite the previous entries as I click through the outer repeat entries.
For example, consider the following
Barcelona (outer entry, clicked first)
Messi
Xavi
Puyol
Manchester United (outer entry, clicked second)
Rooney,
xxx
Real Madrid (outer entry, clicked third)
Ronaldo
Kaka
After I expanded all these soccer teams, I go back and click the player named Messi. It prints the name Ronaldo on the server console. If I click the name Xavi, it prints Kaka.
I just cant figure what's going on here. I tried the "repeatControls" and "removeRepeat" properties also. No luck. Is this a java hashmap caching or something wrong with the repeat control behavior.
Please let me know if anyone has any idea.
Here is the XPage source
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
<xp:this.resources>
<xp:script src="/xpTestCacheIssue.jss" clientSide="false"></xp:script>
</xp:this.resources>
<xp:repeat id="repeat1" rows="30" value="#{javascript:getTeams()}"
var="entryTeam">
<xp:panel>
<xp:link escape="true"
text="#{javascript:entryTeam.getValue()}" id="lnkTeam">
<xp:eventHandler event="onclick" submit="true"
refreshMode="partial" refreshId="panelPlayers"
action="#{javascript:viewScope.teamID=entryTeam.getKey()}">
</xp:eventHandler>
</xp:link>
</xp:panel>
<xp:panel id="panelPlayers">
<xp:repeat id="repeat2" rows="30"
value="#{javascript:getPlayers(viewScope.teamID)}"
var="entryPlayer">
<xp:panel style="margin-left:20px;padding:10px">
<xp:link escape="true"
text="#{javascript:entryPlayer.getValue()}" id="lnkPlayer">
<xp:eventHandler event="onclick" submit="true"
refreshMode="partial" refreshId="selectedPlayer"
execMode="partial" execId="lnkPlayer">
<xp:this.action><![CDATA[#{javascript:viewScope.PlayerName=entryPlayer.getValue();}]]></xp:this.action>
</xp:eventHandler>
</xp:link>
</xp:panel>
</xp:repeat>
</xp:panel>
</xp:repeat>
<xp:panel id="selectedPlayer" style="border:1px solid green;padding:20px;background-color:yellow;font-weight:bold">
<xp:text escape="true" id="computedField1"
value="#{javascript:viewScope.PlayerName}">
</xp:text>
</xp:panel>
Here is the java code that gets the hashmaps for these repeats. There is a SSJS function that calls the java methods.
public Map<String,String> getSoccerTeams() {
Map<String,String> hmTeams=new HashMap<String,String>();
try {
ViewEntryCollection vec=vwTeams.getAllEntries();
ViewEntry ve=vec.getFirstEntry();
while (ve!=null) {
hmTeams.put(ve.getUniversalID(), ve.getDocument().getItemValueString("TeamName"));
ve=vec.getNextEntry(ve);
}
} catch (Exception e) {
e.printStackTrace();
}
return hmTeams;
}
public Map<String,String> getPlayers(String idPlayer) {
HashMap<String,String> hmPlayers=new HashMap<String,String>();
try {
View vwPlayers=this.dbCur.getView("playersview");
DocumentCollection dc=vwPlayers.getAllDocumentsByKey(idPlayer, true);
Document doc=dc.getFirstDocument();
while (doc!=null) {
hmPlayers.put(doc.getUniversalID(), doc.getItemValueString("PlayerName"));
doc=dc.getNextDocument(doc);
}
} catch (Exception e) {
e.printStackTrace();
}
return hmPlayers;
}
Here is the SSJS code that calls the java methods.
function getTeams() {
var Teams=new LoadTeams();
var hmTeams=Teams.getSoccerTeams();
return hmTeams.entrySet();
}
function getPlayers(playerID) {
var Teams=new LoadTeams();
var hmPlayers=Teams.getPlayers(playerID);
return hmPlayers.entrySet();
}
Your problem is here:
value="#{javascript:getPlayers(viewScope.teamID)}"
It should rather read like:
value="#{javascript:getPlayers(entryTeam.getKey())}"
the outer variable is available in the inner repeat. If you stuff code into a scope object it will refer to the last value in there.
And please recycle your Notes objects in the Java code. You also would want to go and cache the values in your bean, so you don't need to read through the view every time. Lastly you can eliminate the SSJS from the code and use just EL -> even faster
On EL:
Let's presume your Java Class is registered as the ManagedBean "Soccer" you can write instead of:
<xp:repeat id="repeat1" rows="30" value="#{javascript:getTeams()}"
var="entryTeam">
this one:
<xp:repeat id="repeat1" rows="30" value="#{Soccer.teams}"
var="entryTeam">
and instead of:
<xp:repeat id="repeat2" rows="30"
value="#{javascript:getPlayers(entryTeam.getKey())}"
var="entryPlayer">
you write:
<xp:repeat id="repeat2" rows="30"
value="#{Soccer.teams[entryTeam.key]}"
var="entryPlayer">
Since EL can deal with Maps, you actually only need the getTeams() function, no getPlayers() function required. You want to adjust your Java class a little to avoid reading the view over and over again:
public class SoccerHandler {
private Map<String,Collection<String>> allTheTeams = null;
public Map<String,String> getTeams() {
if (allTheTeams == null) {
this.loadTheTeams();
}
return allTheTeams;
}
private void loadTheTeams() {
// Add your own try/catch
this.allTheTeams = new HashMap<String,Collection<String>>();
View vwPlayers=this.getPlayersView(); // <-- Don't keep Notes objects in a BEAN
ViewEntryCollection vec=vwPlayers.getAllEntries();
String lastTeamName = "";
Collection<String> curTeam = null;
ViewEntry ve=vec.getFirstEntry();
while (ve!=null) {
Vector colVals = ve.getColumnValues();
String curTeamName = colVals.get(0); // 1st column = team
String curPlayerName = colVals.get(1); // 2nd column = player
if (!curTeamName.equals(lastTeamName)) { // New team found
if (curTeam != null) {
this.allTheTeams.put(lastTeamName, curTeam);
}
curTeam = new ArrayList<String>();
lastTeamName = curTeamName;
}
curTeam.put(curPlayerName);
ve=vec.getNextEntry(ve);
}
// Make sure we don't miss the last team
if (curTeam != null) {
this.allTheTeams.put(lastTeamName, curTeam);
}
ve.recycle();
vec.recyle();
}
// If you want to be able to force a re-read
public void reset() {
this.allTheTeams = null;
}
// more code here like getPlayerView that uses the resolver
}
Hope that helps
I want to create custom component with attribute "title" that can have expression but I get this error:
Unable to convert string "#{myBean.text}" to class "javax.el.ValueExpression" for attribute "title": Property Editor not registered with the PropertyEditorManager
Caused by:
org.apache.jasper.JasperException - Unable to convert string "#{myBean.text}" to class "javax.el.ValueExpression" for attribute "title": Property Editor not registered with the PropertyEditorManager
My classes:
<d:ticker title="#{myBean.text}">
<f:verbatim>Hello JSF Custom Component</f:verbatim>
</d:ticker>
MyBean.java
public class MyBean {
private String text = "TITLE!!!!";
public String getText() {
return text;
}
}
TickerTag.java
private ValueExpression title = null;
public void setTitle(ValueExpression title)
{
this.title = title;
}
protected void setProperties(UIComponent component) {
super.setProperties(component);
if (title != null) {
if (!title.isLiteralText()) {
component.setValueExpression("title", title);
} else {
component.getAttributes().put("title",title.getExpressionString());
}
}
taglib.tld
<taglib version="2.1" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee web-jsptaglibrary_2_1.xsd">
<tlib-version>1.0</tlib-version>
<jsp-version>1.2</jsp-version>
<short-name>d</short-name>
<uri>http://jsftutorials.com/</uri>
<tag>
<name>ticker</name>
<tag-class>ticker.TickerTag</tag-class>
<body-content>JSP</body-content>
<attribute>
<name>title</name>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
</taglib>
anybody see the problem?
I encountered the same problem, and was able to solve it by including the deferred-value tag in my taglib.tld file. It's required when the component has an attribute that can be set with an EL expression. The 'type' tag is the type that the EL expression should evaluate to.
taglib.tld:
<tag>
<name>CustomComponent</name>
<tag-class>com.test.components.CustomComponent</tag-class>
<attribute>
<name>someAttribute</name>
<description>The custom attribute</description>
<deferred-value>
<type>java.lang.String</type>
</deferred-value>
</attribute>
</tag>