2

I've replaced the f:ajax tag with an homemade solution that doesn't put inline script. It works wonder for actionButton. However I cannot make it work for a listener on a panelGroup. The reason is that it is specified nowhere what the bean target method resulting from the ajax request should be. In other words with a commandButton I can specify the target bean method in action, but there is no such attribute for panelGroup; as I don't want to use f:ajax listener, I want to replace it.

 <h:commandButton data-widget="jsfajax" value="ajax" action="#{someAction}"/>
$(document).ready(function(){   
    (function(widgets){
        document.body.addEventListener("click", function(e) {
               var w = e.target.getAttribute("data-widget");
               if(w){
                   e.preventDefault();
                   widgets[w](e.target);
               }
            });
    })(new Widgets);
});
function Widgets(){
    this.jsfajax =  function jsfajax(elem){ 
            if(elem.id == ""){
            elem.id = elem.name;
        }
       mojarra.ab(elem,"click",'action','@form',0);  
     }
}

This works.

But this obviously doesn't (it does but it doesn't invoke anything) :

 <h:panelGroup>
    <f:passThroughAttribute name="data-widget" value="jsfajax"/>
    Click here
 </h:panelGroup>

But this does :

 <h:panelGroup>
    <f:ajax event="click" listener="#{someAction}"/>
    Click here
 </h:panelGroup>

Both those panelGroup result in the same HTML output, so I assume it's the jsf container which "remembers" the click on that panelGroup is linked to #{someAction}.

What I'd like to do is recreate that link without using f:ajax listener. At the moment I've to use an hidden commandButton which is less elegant. So maybe a composite component panelGroup which would save the "action link", I've no idea.

Ced
  • 15,847
  • 14
  • 87
  • 146
  • I'm curious to why you want this? Only to prevent inline js? You'd need to run all this again when you update with ajax. Do the advantages outweigh the disadvantages of inline js? – Kukeltje Apr 12 '16 at 19:17
  • @Kukeltje I don't need to run this again after update, the click event is delegated to the doc. I want this to because I'm planning to ban inline js. with the content security policy header. And honestly I prefer it this way. It's just annoying in this case with a panelGroup. – Ced Apr 12 '16 at 21:13
  • @Kukeltje I've opened a bounty, if you have additional informations it would be welcome :) – Ced Apr 15 '16 at 14:26
  • @BalusC sorry I copy pasted the whole working code then deleted what I thought was unnecessary. I will do it now. I thought it was correct. – Ced Apr 15 '16 at 16:51

1 Answers1

0

What you want to achieve is only possible on UICommand components, not on ClientBehaviorHolder components. One solution would be to create a custom component extending HtmlCommandLink which renders a <div> instead of <a> and use it like so <your:div action="#{bean.action}">.

The most ideal solution would be to replace the standard renderers. E.g. for <h:panelGorup>:

<render-kit>
    <renderer>
        <component-family>javax.faces.Panel</component-family>
        <renderer-type>javax.faces.Group</renderer-type>
        <renderer-class>com.example.YourPanelGroupRenderer</renderer-class>
    </renderer>
</render-kit>

Basically, those renderers should skip rendering <f:ajax>-related on* attributes and instead render your data-widget attribute (and preferably also other attributes representing existing <f:ajax> attributes such as execute, render, delay, etc). You should also program against the standard API, not the Mojarra-specific API. I.e. use jsf.ajax.request() directly instead of mojarra.ab() shortcut.

This way you can keep your view identical conform the JSF standards. You and future developers would this way not even need to learn/think about a "proprietary" API while writing JSF code. You just continue using <h:panelGroup><f:ajax>. You simply plug in the custom renders and script via a JAR in webapp and you're done. That JAR would even be reusable on all other existing JSF applications. It could even become popular, because inline scripts are indeed considered poor practice.

It's only quite some code and not necessarily trivial for a starter.

A different approach is to replace the standard response writer with a custom one wherein you override writeAttribute() and check if the attribute name starts with on and then handle them accordingly the way you had in mind. E.g. parsing it and writing a different attribute. Here's a kickoff example which also recognizes <h:panelGroup><f:ajax>.

public class NoInlineScriptRenderKitFactory extends RenderKitFactory {

    private RenderKitFactory wrapped;

    public NoInlineScriptRenderKitFactory(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 NoInlineScriptRenderKit(renderKit) : renderKit;
    }

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

}
public class NoInlineScriptRenderKit extends RenderKitWrapper {

    private RenderKit wrapped;

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

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

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

}
public class NoInlineScriptResponseWriter extends ResponseWriterWrapper {

    private ResponseWriter wrapped;

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

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

    @Override
    public void writeAttribute(String name, Object value, String property) throws IOException {
        if (name.startsWith("on")) {
            if (value != null && value.toString().startsWith("mojarra.ab(")) {
                super.writeAttribute("data-widget", "jsfajax", property);
            }
        }
        else {
            super.writeAttribute(name, value, property);
        }
    }

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

}

The most important part where you have your freedom is the writeAttribute() method in the last snippet. The above kickoff example just blindly checks if the on* attribute value starts with Mojarra-specific "mojarra.ab(" and then instead writes your data-widget="jsfajax". In other words, every single (naturally used!) <f:ajax> will be rewritten this way. You can continue using <h:commandLink><f:ajax> and <h:panelGroup><f:ajax> the natural way. Don't forget to deal with other <f:ajax> attributes while you're at it.

In order to get it to run, register as below in faces-config.xml:

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

You only still need to take into account existing implementation-specific details (fortunately there are only two: Mojarra and MyFaces).

See also:

Community
  • 1
  • 1
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • I upvoted while being afraid you thought it would be me lol, sorry for taking my time to accept it. – Ced Apr 18 '16 at 09:00
  • No problem, take your time. – BalusC Apr 18 '16 at 09:00
  • I didn't expect custom renderers to be a built in feature so this is better than I hoped for. I did a custom renderer extending `GroupRenderer` and in `encodeBegins` I get the `clientBehaviors`. Then I add the attribute if `click` is in the keys of the map. This renders a `data-widget` attr in html which in turn can fire a ajax req. I didn't manage yet to make it reach the listener tho, maybe you have a pointer ? It took me some further reading since early morning but I think I'm getting there but I'm tired so I will finish after. I'll git it when done since you said it could be popular – Ced Apr 18 '16 at 11:51
  • Compare your `mojarra.ab` call with the default one. The one in your current question indeed wouldn't work on ``. Basically, you should call `mojarra.ab(event.target, event, "click", 0, 0)`. – BalusC Apr 18 '16 at 12:27
  • A hint for the future bounty questions: just mark the answer accepted and let the bounty expire. When the question rises to top the last days of the bounty, there's an increased chance in interest/sympathy upvotes from users passing by the bounty list, so you can almost earn back the reputation spent to the bounty. – BalusC Apr 19 '16 at 18:45
  • Right once again, I thought you were tripping balls(didn't notice "click") and spent the day looking at RenderKitUtil not understanding why my listener wasn't invoked. I won't doubt you ever again lol. And thanks for the tip. – Ced Apr 19 '16 at 20:32
  • You're welcome :) Don't forget the recommendation to just use `jsf.ajax.request()` directly instead of the Mojarra-specific `mojarra.ab()` shortcut function. It'll then work regardless of the JSF implementation used. – BalusC Apr 19 '16 at 20:36