Friday, 5 October 2012

JSF Component must not be script

Recently we needed form to create JSF composite component that would bind custom Ajax event handler to a form. We simply made it output <script id="xxx">...</script>. And it was mistake!

Here is the code of the composite component:
<cc:interface componentType="com.yartista.web.UIFormChangeListener">
    <cc:attribute name="listener" method-signature="void action()"/>
    <cc:attribute name="render" type="java.lang.String"/>
    <h:outputScript library="javax.faces" name="jsf.js" target="head"/>
    <h:outputScript library="org/richfaces" name="formChangeListener.js" target="head"/>
    <script type="text/javascript" id="#{cc.clientId}">new RichFaces.ui.FormChangeListener("#{cc.clientId}");</script>
So the output was i.e.:
<script type="text/javascript" id="formId:changeListener">new RichFaces.ui.FormChangeListener("formId:changeListener");</script>
I'll also post the definition of that magic JavaScript method:
 rf.ui.FormChangeListener = function (componentId)
        var jform = jQuery(document.getElementById(componentId)).parents("form").eq(0);
The function simply looks for element specified by id given in param and looks for surrounding form and binds some event handler.
Does it work? Sure, on initial request and regular form submits it does. The problem is if we want to re-render the script or any surrounding component. It's due to implementation of jsf.js, which IMHO is wrong here. It does not attach to page exactly the same markup (from request) in case of scripts! It extracts it's contents and creates new tags. This is why after Ajax re-rendering of the form the "jform" is null. It's because no "script" tag with given id is attached to the page.

Can we do anything about it? Yes! We need to surround our script tag with some other tag and move id attribute to the latter. Here is fixed implementation of our composite component:
    <h:outputScript library="javax.faces" name="jsf.js" target="head"/>
    <h:outputScript library="org/richfaces" name="formChangeListener.js" target="head"/>

    <div id="#{cc.clientId}">
        <script type="text/javascript">new RichFaces.ui.FormChangeListener("#{cc.clientId}");</script>