Dynamic resource dependency loading

Community Driven Extensions Project
User avatar
mrswadge
Posts: 36
Joined: 10 Sep 2012, 11:11

26 Jan 2016, 19:13

Yes and no :)

Code: Select all

<p:commandButton value="Add Date: Non-AJAX" actionListener="#{buttonView.buttonAction2}" update="@all" ajax="false" />
<p:commandButton value="Add Date: AJAX" actionListener="#{buttonView.buttonAction2}" update="@all" ajax="true" />
Both of the above appear to work fine in my test system. Confidently I integrated it into our actual application, but it's not happening there still. :(

I need to investigate that further.
PrimeFaces 6.0 | Mojarra 2.0.11 | WebLogic 10.3.6

tandraschko
PrimeFaces Core Developer
Posts: 3979
Joined: 03 Dec 2010, 14:11
Location: Bavaria, DE
Contact:

26 Jan 2016, 21:57

And are you 100% that it's not available during the initial request?
Can you replace @all e.g. with @form?
Thomas Andraschko

PrimeFaces | PrimeFaces Extensions

Apache Member | OpenWebBeans, DeltaSpike, MyFaces, BVal, TomEE

Sponsor me: https://github.com/sponsors/tandraschko
Blog: http://tandraschko.blogspot.de/
Twitter: https://twitter.com/TAndraschko

User avatar
mrswadge
Posts: 36
Joined: 10 Sep 2012, 11:11

27 Jan 2016, 12:40

tandraschko wrote:And are you 100% that it's not available during the initial request?
Can you replace @all e.g. with @form?
I've tested it now with mixed results. The non-AJAX button does work with update="@form", but the AJAX calls do not. I need to try to find a way of asking it to update the "h:head" area through AJAX, but it may not be simple it seems: http://forum.primefaces.org/viewtopic.php?f=3&t=12117

There is a hacky solution, but it's not ideal. I'd prefer to go a better route.

Code: Select all

<p:outputPanel id="headhack">
	<h:head>
	<!-- includes etc -->
	</h:head>
</p:outputPanel>

Code: Select all

<p:commandButton value="Add Date: AJAX" actionListener="#{buttonView.buttonAction2}" update="@form,headhack" ajax="true" />
Infact IE11 is complaining (warnings) because the HTML produced is invalid. It considers there to be two body tags and doesn't take kindly to the head tag being within a div.
PrimeFaces 6.0 | Mojarra 2.0.11 | WebLogic 10.3.6

User avatar
mrswadge
Posts: 36
Joined: 10 Sep 2012, 11:11

27 Jan 2016, 13:26

I thought I found a solution by using JQuery selectors, but this appears to be disabling the AJAX nature of the request.

Code: Select all

<!-- even though ajax="true" it's not sending an XHR request -->
<p:commandButton value="AJAX [@form,@(:head)]" actionListener="#{buttonView.buttonAction2}" update="@form,@(:head)" ajax="true" />

Code: Select all

<!-- ajax="true" and so it is sending an XHR request -->
<p:commandButton value="AJAX [@form]" actionListener="#{buttonView.buttonAction2}" update="@form" ajax="true" />
The only difference here is the update attribute value.
PrimeFaces 6.0 | Mojarra 2.0.11 | WebLogic 10.3.6

tandraschko
PrimeFaces Core Developer
Posts: 3979
Joined: 03 Dec 2010, 14:11
Location: Bavaria, DE
Contact:

27 Jan 2016, 17:08

Yeah, so you found the case why we implemented the dynamic resource loading.

Is there any exception or js error when using @(head)? Dont forget to add a id to the head.
Thomas Andraschko

PrimeFaces | PrimeFaces Extensions

Apache Member | OpenWebBeans, DeltaSpike, MyFaces, BVal, TomEE

Sponsor me: https://github.com/sponsors/tandraschko
Blog: http://tandraschko.blogspot.de/
Twitter: https://twitter.com/TAndraschko

User avatar
mrswadge
Posts: 36
Joined: 10 Sep 2012, 11:11

27 Jan 2016, 17:12

tandraschko wrote:Yeah, so you found the case why we implemented the dynamic resource loading.

Is there any exception or js error when using @(head)? Dont forget to add a id to the head.
Unfortunately the head tag can't take an id attribute.

I have a plan using a phase listener. I'm working on it right now and will update you if and when it works :)
PrimeFaces 6.0 | Mojarra 2.0.11 | WebLogic 10.3.6

tandraschko
PrimeFaces Core Developer
Posts: 3979
Joined: 03 Dec 2010, 14:11
Location: Bavaria, DE
Contact:

27 Jan 2016, 17:57

Why not? Every component can take an id. Or is it not rendered?
Thomas Andraschko

PrimeFaces | PrimeFaces Extensions

Apache Member | OpenWebBeans, DeltaSpike, MyFaces, BVal, TomEE

Sponsor me: https://github.com/sponsors/tandraschko
Blog: http://tandraschko.blogspot.de/
Twitter: https://twitter.com/TAndraschko

User avatar
mrswadge
Posts: 36
Joined: 10 Sep 2012, 11:11

27 Jan 2016, 18:35

Not sure why, but the http://java.sun.com/jsf/html taglib doesn't accept id as an attribute. It's not provided as an option. I only see the attributes binding, dir and lang. I gave it a go by adding id="xxx", but it wasn't rendered.
PrimeFaces 6.0 | Mojarra 2.0.11 | WebLogic 10.3.6

User avatar
mrswadge
Posts: 36
Joined: 10 Sep 2012, 11:11

27 Jan 2016, 19:04

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.
PrimeFaces 6.0 | Mojarra 2.0.11 | WebLogic 10.3.6

tandraschko
PrimeFaces Core Developer
Posts: 3979
Joined: 03 Dec 2010, 14:11
Location: Bavaria, DE
Contact:

27 Jan 2016, 20:24

Adding single scripts might never work for 100% because some scripts might be rendered, some not. So you can not guarantee the correct order.

What might work is that if changes are found, that you dynamically update the head tag via RequestContext#update.
I think the id of the head must be rendered in plain JSF but in PF, the HeadRenderer currently does not render it.
Thomas Andraschko

PrimeFaces | PrimeFaces Extensions

Apache Member | OpenWebBeans, DeltaSpike, MyFaces, BVal, TomEE

Sponsor me: https://github.com/sponsors/tandraschko
Blog: http://tandraschko.blogspot.de/
Twitter: https://twitter.com/TAndraschko

Post Reply

Return to “Extensions”

  • Information
  • Who is online

    Users browsing this forum: No registered users and 5 guests