Ferry Kranenburg created a nice hack to solve the AMD loader problem with XPages and Dojo, and because of the missing ability to add a resource to the bottom of an XPage by a property, I have created a new JavaScriptRenderer which allows to control where a CSJS script will be rendered.
The renderer has multiple options:
- NORMAL – handles the CSJS resource as always
- ASYNC – loads the script in an asynchronous way (with an own script tag)
- NOAMD – adds the no amd scripts around the resource
- NORMAL_BOTTOM – adds the script at the bottom of the <body> tag
- ASYNC_BOTTOM – async, but at the end of the generated HTML page
- NOAMD_BOTTOM – at the end, with the surrounding no amd scripts
To use the normal mode, you don’t have to change you resource definition. If you want to use the other modes, you have to change the content type of the resource with one of the entries in the list above. This for example would add a script block to the end of the page, including the non amd script blocks around it:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
<xp:this.resources>
<xp:script clientSide="true" type="NOAMD_BOTTOM">
<xp:this.contents><![CDATA[alert("Hello World!");]]></xp:this.contents>
</xp:script>
</xp:this.resources>
</xp:view>

Here is the code for the resource renderer:
package ch.hasselba.xpages;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import com.ibm.xsp.component.UIViewRootEx;
import com.ibm.xsp.renderkit.html_basic.ScriptResourceRenderer;
import com.ibm.xsp.resource.Resource;
import com.ibm.xsp.resource.ScriptResource;
import com.ibm.xsp.util.JSUtil;
public class OptimizedScriptResourceRenderer extends ScriptResourceRenderer {
private static final String TYPE = "type";
private static final String SCRIPT = "script";
private static final String CSJSTYPE = "text/javascript";
private boolean isBottom = false;
private static enum Mode {
NORMAL, ASYNC, NOAMD, ASYNC_BOTTOM, NOAMD_BOTTOM, NORMAL_BOTTOM
}
public void encodeResourceAtBottom(FacesContext fc,
UIComponent uiComponent, Resource resource) throws IOException {
isBottom = true;
encodeResource(fc, uiComponent, resource);
isBottom = false;
}
@Override
public void encodeResource(FacesContext fc, UIComponent uiComponent,
Resource resource) throws IOException {
ScriptResource scriptResource = (ScriptResource) resource;
ResponseWriter rw = fc.getResponseWriter();
String type = scriptResource.getType();
String charset = scriptResource.getCharset();
String src = scriptResource.getSrc();
Mode mode = Mode.NORMAL;
try{
mode = Mode.valueOf( type );
}catch(Exception e){};
if (mode == Mode.NORMAL || mode == Mode.NORMAL_BOTTOM ) {
normalBottomJSRenderer( fc, uiComponent, scriptResource, (mode == Mode.NORMAL_BOTTOM), type );
} else {
if (mode == Mode.ASYNC || mode == Mode.ASYNC_BOTTOM) {
asyncJSRenderer(fc, uiComponent, scriptResource, (mode == Mode.ASYNC_BOTTOM), rw, type,
charset, src );
}else if (mode == Mode.NOAMD || mode == Mode.NOAMD_BOTTOM ) {
noAMDJSRenderer(fc, uiComponent, scriptResource, (mode == Mode.NOAMD_BOTTOM) , rw,
type, charset, src);
}
}
}
private void normalBottomJSRenderer(FacesContext fc,UIComponent uiComponent,
ScriptResource scriptResource, final boolean addToBottom, final String type ) throws IOException {
if( addToBottom && !isBottom )
return;
scriptResource.setType(null);
super.encodeResource(fc, uiComponent, scriptResource);
scriptResource.setType(type);
}
private void asyncJSRenderer(FacesContext fc,
UIComponent uiComponent, ScriptResource scriptResource,
final boolean addToBottom, ResponseWriter rw, final String type, final String charset,
final String src) throws IOException {
if( addToBottom && !isBottom )
return;
Map<String, String> attrs = null;
String key = null;
String value = null;
String id = "";
if (scriptResource.getContents() == null) {
attrs = scriptResource.getAttributes();
if (!attrs.isEmpty()) {
StringBuilder strBuilder = new StringBuilder(124);
for (Iterator<String> it = attrs.keySet().iterator(); it
.hasNext();) {
key = it.next();
value = attrs.get(key);
strBuilder.append(key).append('(').append(value)
.append(')');
}
id = strBuilder.toString();
}
// check if already added
UIViewRootEx view = (UIViewRootEx) fc.getViewRoot();
String resId = "resource_" + ScriptResource.class.getName() + src
+ '|' + type + '|' + charset + id;
if (view.hasEncodeProperty(resId)) {
return;
}
view.putEncodeProperty(resId, Boolean.TRUE);
}
if (!scriptResource.isClientSide()) {
return;
}
rw.startElement(SCRIPT, uiComponent);
JSUtil.writeln(rw);
rw.write("var s = document.createElement('" + SCRIPT + "');");
JSUtil.writeln(rw);
rw.write("s.src = '" + src + "';");
JSUtil.writeln(rw);
rw.write("s.async = true;");
JSUtil.writeln(rw);
rw.write("document.getElementsByTagName('head')[0].appendChild(s);");
JSUtil.writeln(rw);
rw.endElement(SCRIPT);
JSUtil.writeln(rw);
}
private void noAMDJSRenderer(FacesContext fc,
UIComponent uiComponent,ScriptResource scriptResource,
final boolean addToBottom, ResponseWriter rw, final String type, final String charset,
final String src ) throws IOException {
if( addToBottom && !isBottom )
return;
// write the "disable AMD" script
rw.startElement(SCRIPT, uiComponent);
rw.writeAttribute(TYPE, CSJSTYPE, TYPE);
rw.writeText(
"'function'==typeof define&&define.amd&&'dojotoolkit.org'==define.amd.vendor&&(define._amd=define.amd,delete define.amd);",
null);
rw.endElement(SCRIPT);
JSUtil.writeln(rw);
// write the normal CSJS
scriptResource.setType(null);
super.encodeResource(fc, uiComponent, scriptResource);
scriptResource.setType(type);
// write the "reenable AMD" script
rw.startElement(SCRIPT, uiComponent);
rw.writeAttribute(TYPE, CSJSTYPE, TYPE);
rw
.writeText(
"'function'==typeof define&&define._amd&&(define.amd=define._amd,delete define._amd);",
null);
rw.endElement(SCRIPT);
JSUtil.writeln(rw);
}
}
The ViewRenderer must also be modified, otherwise it is not possible to add the resources at the bottom of the <body> tag:
package ch.hasselba.xpages;
import java.io.IOException;
import java.util.List;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;
import com.ibm.xsp.component.UIViewRootEx;
import com.ibm.xsp.render.ResourceRenderer;
import com.ibm.xsp.renderkit.html_basic.ViewRootRendererEx2;
import com.ibm.xsp.resource.Resource;
import com.ibm.xsp.resource.ScriptResource;
import com.ibm.xsp.util.FacesUtil;
public class ViewRootRendererEx3 extends ViewRootRendererEx2 {
protected void encodeHtmlEnd(UIViewRootEx uiRoot, ResponseWriter rw)
throws IOException {
FacesContext fc = FacesContext.getCurrentInstance();
List<Resource> resources = uiRoot.getResources();
for (Resource r : resources) {
if (r instanceof ScriptResource) {
ScriptResource scriptRes = (ScriptResource) r;
if (scriptRes.isRendered()) {
Renderer renderer = FacesUtil.getRenderer(fc, scriptRes.getFamily(), scriptRes.getRendererType());
ResourceRenderer resRenderer = (ResourceRenderer) FacesUtil.getRendererAs(renderer, ResourceRenderer.class);
if( resRenderer instanceof OptimizedScriptResourceRenderer ){
((OptimizedScriptResourceRenderer) resRenderer).encodeResourceAtBottom(fc, uiRoot, r);
}
}
}
}
rw.endElement("body");
writeln(rw);
rw.endElement("html");
}
}
To activate the new renderes, you have to add them to the faces-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
<render-kit>
<renderer>
<component-family>com.ibm.xsp.resource.Resource</component-family>
<renderer-type>com.ibm.xsp.resource.Script</renderer-type>
<renderer-class>ch.hasselba.xpages.OptimizedScriptResourceRenderer</renderer-class>
</renderer>
<renderer>
<component-family>javax.faces.ViewRoot</component-family>
<renderer-type>com.ibm.xsp.ViewRootEx</renderer-type>
<renderer-class>ch.hasselba.xpages.ViewRootRendererEx3</renderer-class>
</renderer>
</render-kit>
</faces-config>
Needless to say that this works in Themes too.