The phase listener is working fairly well, it's handling the situation with the AJAX request with update="@form" quite well.
I wrote the listener in java and wired it up in the extensions /META-INF/faces-config.xml.
Code: Select all
package com.otbo.faces.render;
import java.util.List;
import java.util.Map;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
public class OtboRenderPhaseListener implements PhaseListener {
private static final long serialVersionUID = -52626426246421L;
private final PhaseId phaseId = PhaseId.ANY_PHASE;
private final BeforePhaseResourceComponentRenderer beforePhaseResourceComponentRenderer = new BeforePhaseResourceComponentRenderer();
private final AfterPhaseResourceComponentRenderer afterPhaseResourceComponentRenderer = new AfterPhaseResourceComponentRenderer();
@Override
public void beforePhase( PhaseEvent event ) {
beforePhaseResourceComponentRenderer.render( event );
}
@Override
public void afterPhase( PhaseEvent event ) {
afterPhaseResourceComponentRenderer.render( event );
}
class BeforePhaseResourceComponentRenderer extends ResourceComponentChangeRenderer {
@Override
protected void apply( PhaseEvent event, List<UIComponent> componentResources ) {
detectChanges( event, componentResources );
}
}
class AfterPhaseResourceComponentRenderer extends ResourceComponentChangeRenderer {
protected void apply( PhaseEvent event, List<UIComponent> componentResources ) {
storeInitialState( event, componentResources );
detectChanges( event, componentResources );
}
private void storeInitialState( PhaseEvent event, List<UIComponent> componentResources ) {
FacesContext facesContext = event.getFacesContext();
Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
// creates a list of the previously loaded component resources.
if ( PhaseId.RESTORE_VIEW == event.getPhaseId() ) {
requestMap.put( COMPONENT_RESOURCE_SET, loadedResources( componentResources ) );
}
}
}
@Override
public PhaseId getPhaseId() {
return phaseId;
}
}
Code: Select all
package com.otbo.faces.render;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.faces.FacesException;
import javax.faces.application.Resource;
import javax.faces.component.UIComponent;
import javax.faces.component.UIOutput;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.primefaces.context.RequestContext;
public abstract class ResourceComponentChangeRenderer {
public static final String COMPONENT_RESOURCE_SET = OtboRenderPhaseListener.class.getSimpleName() + "_ComponentResourceSet";
public static final PhaseId[] DETECT_PHASES = { PhaseId.APPLY_REQUEST_VALUES, PhaseId.PROCESS_VALIDATIONS, PhaseId.UPDATE_MODEL_VALUES, PhaseId.INVOKE_APPLICATION, PhaseId.RENDER_RESPONSE };
public void render( PhaseEvent event ) {
FacesContext facesContext = event.getFacesContext();
UIViewRoot viewRoot = facesContext.getViewRoot();
if ( viewRoot != null ) {
apply( event, viewRoot.getComponentResources( facesContext, "head" ) );
}
}
protected abstract void apply( PhaseEvent event, List<UIComponent> componentResources );
protected void detectChanges( PhaseEvent event, List<UIComponent> componentResources ) {
if ( ArrayUtils.contains( DETECT_PHASES, event.getPhaseId() ) ) {
renderChanges( event.getFacesContext(), componentResources );
}
}
protected Set<String> loadedResources( List<UIComponent> componentResources ) {
Set<String> loaded = new HashSet<String>();
for ( UIComponent c : componentResources ) {
if ( c instanceof UIOutput ) {
String library = (String) c.getAttributes().get( "library" );
String name = (String) c.getAttributes().get( "name" );
if ( library != null && name != null ) {
loaded.add( getResourceComponentKey( library, name ) );
}
}
}
return loaded;
}
protected void renderChanges( FacesContext facesContext, List<UIComponent> componentResources ) {
RequestContext requestContext = RequestContext.getCurrentInstance();
Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
Set<String> loaded = (Set<String>) requestMap.get( COMPONENT_RESOURCE_SET );
for ( UIComponent c : componentResources ) {
if ( c instanceof UIOutput ) {
String library = (String) c.getAttributes().get( "library" );
String name = (String) c.getAttributes().get( "name" );
if ( library != null && name != null ) {
// only load this file if it hasn't already been transmitted to the client.
String key = getResourceComponentKey( library, name );
if ( ! loaded.contains( key ) ) {
Resource resource = facesContext.getApplication().getResourceHandler().createResource(name, library);
String js = "(function(){var c=\"cache\";var v=$.ajaxSetup()[c];$.ajaxSetup()[c]=true;try{%s}finally{$.ajaxSetup()[c]=v;}}())";
if ( isStylesheet( c ) ) {
String styleInclude = String.format( "$('head').append('<link type=\"text/css\" rel=\"stylesheet\" href=\"%s\" />')", StringEscapeUtils.escapeEcmaScript( resource.getRequestPath() ) );
requestContext.execute( String.format( js, styleInclude ) );
loaded.add( key );
} else if ( isScript( c ) ) {
String scriptInclude = String.format( "$('head').append('<script type=\"text/javascript\" src=\"%s\"></script>')", StringEscapeUtils.escapeEcmaScript( resource.getRequestPath() ) );
requestContext.execute( String.format( js, scriptInclude ) );
loaded.add( key );
} else {
throw new FacesException( "Unknown component resource type." );
}
}
}
}
}
}
protected String getResourceComponentKey( String library, String name ) {
return library + ":" + name;
}
protected boolean isScript( UIComponent component ) {
return "javax.faces.resource.Script".equals( component.getRendererType() );
}
protected boolean isStylesheet( UIComponent component ) {
return "javax.faces.resource.Stylesheet".equals( component.getRendererType() );
}
}
Code: Select all
<lifecycle>
<phase-listener>com.otbo.faces.render.OtboRenderPhaseListener</phase-listener>
</lifecycle>
This is now working great when there are new resource components added to the header of the HTML. The only problem I face now, is an execution order problem on the client-side. It seems that the code in the request above the injected script is executed first - so the libraries that I need are still not loaded at the time it attempts to construct the component.
i.e. In this example XHR request, the <eval> section at the end is being processed after the <update id="j_idt9"> section. For these particular parts of the script, I need it to be done the other way around.
Code: Select all
<?xml version='1.0' encoding='UTF-8'?>
<partial-response><changes><update id="j_idt9"><![CDATA[
<form id="j_idt9" name="j_idt9" method="post" action="/index2.xhtml" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="j_idt9" value="j_idt9" />
<div id="container">
<div id="row">
<div id="cell">
<h1>Date Entry</h1>
</div>
</div>
<div id="row">
<div id="cell">
<span>They will appear here:</span>
</div>
<div id="cell"><div id="j_idt9:datecontainer" class="ui-panel ui-widget ui-widget-content ui-corner-all" data-widget="widget_j_idt9_datecontainer"><div id="j_idt9:datecontainer_content" class="ui-panel-content ui-widget-content"><div id="j_idt9:j_id11" name="j_idt9:j_id11" date="15" month="8" year="2011" hour="20" minute="12" datepicker="datepicker" format="datetime" fieldorder="date,month,year,datepicker,hour,minute" monthnames="Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec"></div><script id="j_idt9:j_id11_s" type="text/javascript">$(function(){PrimeFaces.cw("DateEntry","widget_j_idt9_j_id11",{id:"j_idt9:j_id11"},"dateentry");});</script></div></div><script id="j_idt9:datecontainer_s" type="text/javascript">PrimeFaces.cw("Panel","widget_j_idt9_datecontainer",{id:"j_idt9:datecontainer"});</script>
</div>
</div>
<div id="row">
<div id="cell">
<div><button id="j_idt9:j_idt12" name="j_idt9:j_idt12" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only" onclick="" style="visibility: hidden;" type="submit"><span class="ui-button-text ui-c">Non-AJAX [@form,@(:head)]</span></button><script id="j_idt9:j_idt12_s" type="text/javascript">PrimeFaces.cw("CommandButton","widget_j_idt9_j_idt12",{id:"j_idt9:j_idt12"});</script></div>
<div><button id="j_idt9:j_idt14" name="j_idt9:j_idt14" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only" onclick="PrimeFaces.ab({s:"j_idt9:j_idt14",u:"j_idt9 @(:head)"});return false;" type="submit"><span class="ui-button-text ui-c">AJAX [@form,@(:head)] - ignores ajax=true!</span></button><script id="j_idt9:j_idt14_s" type="text/javascript">PrimeFaces.cw("CommandButton","widget_j_idt9_j_idt14",{id:"j_idt9:j_idt14"});</script></div>
<div><button id="j_idt9:j_idt16" name="j_idt9:j_idt16" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only" onclick="PrimeFaces.ab({s:"j_idt9:j_idt16",u:"j_idt9"});return false;" type="submit"><span class="ui-button-text ui-c">AJAX [@form]</span></button><script id="j_idt9:j_idt16_s" type="text/javascript">PrimeFaces.cw("CommandButton","widget_j_idt9_j_idt16",{id:"j_idt9:j_idt16"});</script></div>
<div><button id="j_idt9:j_idt18" name="j_idt9:j_idt18" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only" onclick="PrimeFaces.ab({s:"j_idt9:j_idt18",u:"@all"});return false;" type="submit"><span class="ui-button-text ui-c">AJAX [@all]</span></button><script id="j_idt9:j_idt18_s" type="text/javascript">PrimeFaces.cw("CommandButton","widget_j_idt9_j_idt18",{id:"j_idt9:j_idt18"});</script></div>
</div>
</div>
</div>
</form>]]></update><update id="javax.faces.ViewState"><![CDATA[8334561928340862055:4065791384651051508]]></update><eval><![CDATA[(function(){var c="cache";var v=$.ajaxSetup()[c];$.ajaxSetup()[c]=true;try{$('head').append('<script type="text/javascript" src="\/javax.faces.resource\/dateentry\/jquery.autotab.js.xhtml?ln=otbofaces&v=20160127164129"></script>')}finally{$.ajaxSetup()[c]=v;}}());(function(){var c="cache";var v=$.ajaxSetup()[c];$.ajaxSetup()[c]=true;try{$('head').append('<script type="text/javascript" src="\/javax.faces.resource\/dateentry\/jquery.caret.js.xhtml?ln=otbofaces&v=20160127164129"></script>')}finally{$.ajaxSetup()[c]=v;}}());(function(){var c="cache";var v=$.ajaxSetup()[c];$.ajaxSetup()[c]=true;try{$('head').append('<script type="text/javascript" src="\/javax.faces.resource\/dateentry\/jquery.date-entry.js.xhtml?ln=otbofaces&v=20160127164129"></script>')}finally{$.ajaxSetup()[c]=v;}}());(function(){var c="cache";var v=$.ajaxSetup()[c];$.ajaxSetup()[c]=true;try{$('head').append('<script type="text/javascript" src="\/javax.faces.resource\/dateentry\/dateentry.js.xhtml?ln=otbofaces&v=20160127164129"></script>')}finally{$.ajaxSetup()[c]=v;}}());(function(){var c="cache";var v=$.ajaxSetup()[c];$.ajaxSetup()[c]=true;try{$('head').append('<link type="text/css" rel="stylesheet" href="\/javax.faces.resource\/dateentry\/dateentry.css.xhtml?ln=otbofaces&v=20160127164129" />')}finally{$.ajaxSetup()[c]=v;}}());]]></eval></changes></partial-response>
Not quite sure where to go from here.
Is there a way to prioritise the script to run before PF updates the view in the browser? I suppose I could move some of this logic into the component renderer and try to track the currently included resources before sending the PF.createWidget() command.