6

In trying to implement the simple html5 attribute 'autofocus' in my JSF/Primefaces web-application, I was alerted to the fact that the components do not pass all unknown attributes on to the final markup. I can understand reasonings for this, as components can be complex combinations of html markup and it would not be clear where to place the attributes if they are not already well-defined by the component.

But the best solution for me is to have support for autofocus (and any other possible types of attributes I may want to support in my application that primefaces has not defined).

I have seen Adding custom attribute (HTML5) support to JSF 2.0 UIInput component, but that seems to apply for the basic JSF components and does not work for PrimeFaces components.

How do I extend the component/rendering of Primefaces to support this?

Community
  • 1
  • 1
Rich
  • 2,805
  • 8
  • 42
  • 53

3 Answers3

10

Instead of homegrowing a custom renderer for every single individual component, you could also just create a single RenderKit wherein you provide a custom ResponseWriter wherein the startElement() method is overriden to check the element name and/or component instance and then write additional attributes accordingly.

Here's a kickoff example of the HTML5 render kit:

public class Html5RenderKit extends RenderKitWrapper {

    private RenderKit wrapped;

    public Html5RenderKit(RenderKit wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public ResponseWriter createResponseWriter(Writer writer, String contentTypeList, String characterEncoding) {
        return new Html5ResponseWriter(super.createResponseWriter(writer, contentTypeList, characterEncoding));
    }

    @Override
    public RenderKit getWrapped() {
        return wrapped;
    }

}

The HTML5 response writer:

public class Html5ResponseWriter extends ResponseWriterWrapper {

    private static final String[] HTML5_INPUT_ATTRIBUTES = { "autofocus" };

    private ResponseWriter wrapped;

    public Html5ResponseWriter(ResponseWriter wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public ResponseWriter cloneWithWriter(Writer writer) {
        return new Html5ResponseWriter(super.cloneWithWriter(writer));
    }

    @Override
    public void startElement(String name, UIComponent component) throws IOException {
        super.startElement(name, component);

        if ("input".equals(name)) {
            for (String attributeName : HTML5_INPUT_ATTRIBUTES) {
                String attributeValue = component.getAttributes().get(attributeName);

                if (attributeValue != null) {
                    super.writeAttribute(attributeName, attributeValue, null);
                }
            }
        }
    }

    @Override
    public ResponseWriter getWrapped() {
        return wrapped;
    }

}

To get it to run, create this HTML5 render kit factory:

public class Html5RenderKitFactory extends RenderKitFactory {

    private RenderKitFactory wrapped;

    public Html5RenderKitFactory(RenderKitFactory wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public void addRenderKit(String renderKitId, RenderKit renderKit) {
        wrapped.addRenderKit(renderKitId, renderKit);
    }

    @Override
    public RenderKit getRenderKit(FacesContext context, String renderKitId) {
        RenderKit renderKit = wrapped.getRenderKit(context, renderKitId);
        return (HTML_BASIC_RENDER_KIT.equals(renderKitId)) ? new Html5RenderKit(renderKit) : renderKit;
    }

    @Override
    public Iterator<String> getRenderKitIds() {
        return wrapped.getRenderKitIds();
    }

}

And register it as follows in faces-config.xml:

<factory>
    <render-kit-factory>com.example.Html5RenderKitFactory</render-kit-factory>
</factory>

The JSF utility library OmniFaces has also such a render kit, the Html5RenderKit (source code here) which should theoretically also work fine on PrimeFaces components. However, this question forced me to take a second look again and I was embarrassed to see that the component argument in ResponseWriter#startElement() is null in <p:inputText> (see line 74 of InputTextRenderer, it should have been writer.startElement("input", inputText) instead). I'm not sure if this is intentional or an oversight in the design of the PrimeFaces renderer or not, but you could use UIComponent#getCurrentComponent() instead to get it.


Update: this is fixed in OmniFaces 1.5.


Noted should be that the upcoming JSF 2.2 will support defining custom attributes in the view via the new passthrough namespace or the <f:passThroughAttribute> tag. See also What's new in JSF 2.2? - HTML5 Pass-through attributes.

Thus, so:

<html ... xmlns:p="http://java.sun.com/jsf/passthrough">
...
<h:inputText ... p:autofocus="true" />

(you may want to use a instead of p as namespace prefix to avoid clash with PrimeFaces' default namespace)

Or:

<h:inputText ...>
    <f:passThroughAttribute name="autofocus" value="true" />
</h:inputText>
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Great, this looks much nicer than the per component solution. I'm sure specific use cases could make arguments either way. If I get time I'll try this out as it seems it would be much quicker than homegrowing all the components. – Rich Mar 22 '13 at 20:01
  • The UIComponent#getCurrentComponent(FacesContext) seems to be the signature, so in the context of the startElement method that is not available. I saw in your Omnifaces commit you used Components.getCurrentCompnent() which doesn't required the context, although that is an OmniFaces class that is not available just in PrimeFaces. At that point It'd be more dangerous for me to have to also override any component that might be passing null to startElement. Do you suggest I pull in OmniFaces or is there some other way? – Rich Mar 22 '13 at 21:40
  • The `Components#getCurrentComponent()` form OmniFaces just delegates to `UIComponent#getCurrentComponent()`. You're not clear on if you've tested it or not, but it works for me. – BalusC Mar 22 '13 at 21:41
  • Do I have a different version of JSF then? The only getCurrentComponent method I'm seeing on javax.faces.component.UIComponent requires a FacesContext parameter. – Rich Mar 22 '13 at 21:43
  • No, I actually mean this one. Sorry, I've a habit of omitting the arguments in `Foo#bar()` if there's only one method anyway. – BalusC Mar 22 '13 at 21:45
  • Alright, peeked at the Components source and figured it out. So no null pointers now on retesting. It worked for me on the inputText but I just tested an inputMask and with the focus being set right away the mask doesn't appear unless I blur->focus the element. I need to confirm if this is an effect of this implementation or just of using autofocus – Rich Mar 22 '13 at 21:54
  • inputMask problem seems to be related to the autofocus and not the implementation. Great, thanks for the better solution – Rich Mar 22 '13 at 21:58
  • Please check the updated answer as to the new JSF 2.2 feature of supporting passthrough attributes. – BalusC Mar 25 '13 at 14:20
3

The solution that I found was to extend and re-implement the encodeMarkup methods for the input renderers. I wanted a more general solution, but after looking at the Primefaces source code, I did not see any generic hooks for the component renderers to add custom attributes. The markup is written out in the encodeMarkup(FacesContext context, InputText inputText) methods of the renderers. It calls up the class-hierarchy to renderPassThruAttributes(FacesContext context, UIComponent component, String[] attributes) but it only feeds in static final String[] arrays from org.primefaces.util.HTML.

In my case I wanted support for the 'autofocus' attribute on InputMask, InputText, InputTextarea, and Password components. Furthermore, the implementation is the same for each component, so I will walk through implementing 'autofocus' on the InputText component, but it should be obvious how it can be extended to support more attributes and more components.

To extend/override a renderer, you will need to have the Primefaces source available and find the encodeMarkup method and copy its contents. Here is the example for InputTextRenderer:

protected void encodeMarkup(FacesContext context, InputText inputText) throws IOException {
    ResponseWriter writer = context.getResponseWriter();
    String clientId = inputText.getClientId(context);

    writer.startElement("input", null);
    writer.writeAttribute("id", clientId, null);
    writer.writeAttribute("name", clientId, null);
    writer.writeAttribute("type", inputText.getType(), null);

    String valueToRender = ComponentUtils.getValueToRender(context, inputText);
    if(valueToRender != null) {
        writer.writeAttribute("value", valueToRender , null);
    }

    renderPassThruAttributes(context, inputText, HTML.INPUT_TEXT_ATTRS);

    if(inputText.isDisabled()) writer.writeAttribute("disabled", "disabled", null);
    if(inputText.isReadonly()) writer.writeAttribute("readonly", "readonly", null);
    if(inputText.getStyle() != null) writer.writeAttribute("style", inputText.getStyle(), null);

    writer.writeAttribute("class", createStyleClass(inputText), "styleClass");

    writer.endElement("input");
}

Extending/Overriding the renderer with your own (see comments for important code):

public class HTML5InputTextRenderer extends InputTextRenderer {

    Logger log = Logger.getLogger(HTML5InputTextRenderer.class);

    //Define your attributes to support here
    private static final String[] html5_attributes = { "autofocus" };

    protected void encodeMarkup(FacesContext context, InputText inputText) throws IOException {
    ResponseWriter writer = context.getResponseWriter();
    String clientId = inputText.getClientId(context);

    writer.startElement("input", null);
    writer.writeAttribute("id", clientId, null);
    writer.writeAttribute("name", clientId, null);
    writer.writeAttribute("type", inputText.getType(), null);

    String valueToRender = ComponentUtils.getValueToRender(context, inputText);
    if (valueToRender != null) {
        writer.writeAttribute("value", valueToRender, null);
    }

    renderPassThruAttributes(context, inputText, HTML.INPUT_TEXT_ATTRS);

    //Make an extra call to renderPassThruAttributes with your own attributes array
    renderPassThruAttributes(context, inputText, html5_attributes);

    if (inputText.isDisabled())
        writer.writeAttribute("disabled", "disabled", null);
    if (inputText.isReadonly())
        writer.writeAttribute("readonly", "readonly", null);
    if (inputText.getStyle() != null)
        writer.writeAttribute("style", inputText.getStyle(), null);

    writer.writeAttribute("class", createStyleClass(inputText), "styleClass");

    writer.endElement("input");
    }
}

Configuring the rendering override in faces-config.xml

<?xml version='1.0' encoding='UTF-8'?>
<faces-config 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 http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
        version="2.0">

    <!-- snip... -->

    <render-kit>
        <renderer>
            <component-family>org.primefaces.component</component-family>
            <renderer-type>org.primefaces.component.InputTextRenderer</renderer-type>
            <renderer-class>com.mycompany.HTML5InputTextRenderer</renderer-class>
        </renderer>
    </render-kit>

    <!-- snip... -->
</faces-config>

and just-in-case if you didn't have the faces-config configured in your web.xml add:

<context-param>
        <param-name>javax.faces.CONFIG_FILES</param-name>
        <param-value>
            /WEB-INF/faces-config.xml, /faces-config.xml
        </param-value>
    </context-param>

Then to use this in some markup:

<p:inputText id="activateUserName" value="${someBean.userName}" 
  autofocus="on">
</p:inputText> 

Note: JSF is not happy with attributes that do not have values. While autofocus in HTML5 does not use a value, JSF will throw an error if one is not given, so make sure to define some throw-away value when adding such attributes.

Rich
  • 2,805
  • 8
  • 42
  • 53
2

JSF 2.2 also provides pass through attributes feature designed for HTML5 and beyond so when PrimeFaces officially supports JSF 2.2, you can pass any attribute from components to html elements.

Cagatay Civici
  • 6,406
  • 1
  • 29
  • 34