-
Notifications
You must be signed in to change notification settings - Fork 95
Combine hardcoded PrimeFaces resources using CombinedResourceHandler
PrimeFaces has a few CSS/JS resources which are not declared via @ResourceDependency
annotation and therefore cannot be combined by CombinedResourceHandler
. Those resources are hardcoded in PrimeFaces own HeadRenderer
class.
The PrimeFaces theme.css
file has a dynamic library name (representing the theme name) and therefore cannot be declared as @ResourceDependency
because the library/resource name must be compiletime constants.
The since PrimeFaces 4.0 introduced validation.js
and beanvalidation.js
files needs to be conditionally included (depending on whether <p:clientValidator>
is present in the view and whether JSR303 bean validation is supported) and therefore cannot be declared as @ResourceDependency
because it cannot be conditionally disabled.
Additionally, it also renders some PrimeFaces.settings
constants in a plain vanilla <script>
element.
In order to properly combine those hardcoded PrimeFaces CSS/JS resources with CombinedResourceHandler
, follow the steps below.
First you need to disable the PrimeFaces HeadRenderer
by putting back the default HeadRenderer
of your JSF implementation in faces-config.xml
. Below example assumes that you're using Mojarra.
<render-kit>
<renderer>
<component-family>javax.faces.Output</component-family>
<renderer-type>javax.faces.Head</renderer-type>
<renderer-class>com.sun.faces.renderkit.html_basic.HeadRenderer</renderer-class>
</renderer>
</render-kit>
In case you're using MyFaces, use org.apache.myfaces.renderkit.html.HtmlHeadRenderer
as <renderer-class>
.
Then, create a custom PhaseListener
for RENDER_RESPONSE
phase which will during beforePhase()
dynamically add those PrimeFaces resources via UIViewRoot#addComponentResource()
. This will run far before those @ResourceDependency
annotations are processed. This satisfies PrimeFaces' intent of having those hardcoded resources to be rendered before of the dependencies of their components.
The below beforePhase()
logic is based on PrimeFaces 6.1 source.
public class PrimeFacesResourceProcessor implements PhaseListener {
private static final long serialVersionUID = 1L;
@Override
public PhaseId getPhaseId() {
return PhaseId.RENDER_RESPONSE;
}
@Override
public void beforePhase(PhaseEvent event) {
FacesContext context = event.getFacesContext();
PrimeConfiguration primeConfiguration = RequestContext.getCurrentInstance().getApplicationContext().getConfig();
String theme = "aristo";
String themeParamValue = primeConfiguration.getTheme();
if (themeParamValue != null) {
ELContext elContext = context.getELContext();
ExpressionFactory expressionFactory = context.getApplication().getExpressionFactory();
ValueExpression ve = expressionFactory.createValueExpression(elContext, themeParamValue, String.class);
theme = (String) ve.getValue(elContext);
}
if (theme != null && !theme.equals("none")) {
addCSS(context, "primefaces-" + theme, "theme.css");
}
if (primeConfiguration.isFontAwesomeEnabled()) {
addCSS(context, "primefaces", "fa/font-awesome.css");
}
if (primeConfiguration.isClientSideValidationEnabled()) {
addJS(context, "primefaces", "validation/validation.js");
if (primeConfiguration.isBeanValidationAvailable()) {
addJS(context, "primefaces", "validation/beanvalidation.js");
}
}
}
@Override
public void afterPhase(PhaseEvent event) {
// NOOP.
}
private void addCSS(FacesContext context, String library, String name) {
UIOutput css = new UIOutput();
css.setRendererType("javax.faces.resource.Stylesheet");
css.getAttributes().put("library", library);
css.getAttributes().put("name", name);
context.getViewRoot().addComponentResource(context, css, "head");
}
private void addJS(FacesContext context, String library, String name) {
UIOutput js = new UIOutput();
js.setRendererType("javax.faces.resource.Script");
js.getAttributes().put("library", library);
js.getAttributes().put("name", name);
context.getViewRoot().addComponentResource(context, js, "head");
}
}
Register it as below in faces-config.xml
:
<lifecycle>
<phase-listener>com.example.PrimeFacesResourceProcessor</phase-listener>
</lifecycle>
Finally, you need to create a custom SystemEventListener
for PostAddToViewEvent
on UIViewRoot
. This will run after all those @ResourceDependency
annotations of PrimeFaces components have been processed. This is thus an ideal moment to add the PrimeFaces.settings
script as a component resource, as intented by PrimeFaces.
The below processEvent()
logic is based on PrimeFaces 6.1 source.
public class PrimeFacesScriptProcessor implements SystemEventListener {
@Override
public boolean isListenerForSource(Object source) {
return source instanceof UIViewRoot;
}
@Override
public void processEvent(SystemEvent event) throws AbortProcessingException {
FacesContext context = event.getFacesContext(); // Or FacesContext.getCurrentInstance() if not JSF 2.3 yet.
ProjectStage projectStage = context.getApplication().getProjectStage();
PrimeConfiguration primeConfiguration = RequestContext.getCurrentInstance().getApplicationContext().getConfig();
StringBuilder script = new StringBuilder();
script.append("if(window.PrimeFaces){");
script.append("PrimeFaces.settings.locale='").append(context.getViewRoot().getLocale()).append("';");
if (primeConfiguration.isClientSideValidationEnabled()) {
script.append("PrimeFaces.settings.validateEmptyFields=").append(primeConfiguration.isValidateEmptyFields()).append(";");
script.append("PrimeFaces.settings.considerEmptyStringNull=").append(primeConfiguration.isInterpretEmptyStringAsNull()).append(";");
}
if (primeConfiguration.isLegacyWidgetNamespace()) {
script.append("PrimeFaces.settings.legacyWidgetNamespace=true;");
}
if (primeConfiguration.isEarlyPostParamEvaluation()) {
script.append("PrimeFaces.settings.earlyPostParamEvaluation=true;");
}
if (!projectStage.equals(ProjectStage.Production)) {
script.append("PrimeFaces.settings.projectStage='").append(projectStage.toString()).append("';");
}
script.append("}");
addJS(context, script.toString());
}
private void addJS(FacesContext context, String script) {
UIOutput js = new UIOutput();
js.setRendererType("javax.faces.resource.Script");
UIOutput content = new UIOutput();
content.setValue(script);
js.getChildren().add(content);
context.getViewRoot().addComponentResource(context, js);
}
}
Register it as below in faces-config.xml
:
<application>
<system-event-listener>
<system-event-listener-class>com.example.PrimeFacesScriptProcessor</system-event-listener-class>
<system-event-class>javax.faces.event.PostAddToViewEvent</system-event-class>
<source-class>javax.faces.component.UIViewRoot</source-class>
</system-event-listener>
</application>
Note that you can this way not make use of the PrimeFaces specific <facet name="first|middle|last">
facets in <h:head>
to render resources at a specific location in the HTML <head>
. But this PrimeFaces-specific feature is already not recognized by CombinedResourceHandler
anyway.
Starting with 10.0.0
PrimeFaces Extensions now includes these classes so you don't have to keep them up to date with every PF version change.
See ticket: https://github.com/primefaces-extensions/primefaces-extensions/issues/293