Custom JSF component renderer: how to render HTML around another element
Asked Answered
S

2

6

I have 2 elements, banana and an outputText, where banana is a custom JSF component and in banana renderer, I would like to generate HTML enclosing the specified element.

xhtml:

<h:outputText id="theApple" value="This is an Apple" />
.
.
.
<my:banana for="theApple" link="http://www.banana.com" />

bananaRenderer(encloses the target element in anchor links):

@Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
    ResponseWriter responseWriter = context.getResponseWriter();
    Banana banana = Banana.class.cast(component);
    responseWriter.startElement("a", null);
    responseWriter.writeAttribute("id", banana.getClientId(context), null);
    responseWriter.writeAttribute("href", banana.getLink(), null);
}

@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
    ResponseWriter responseWriter = context.getResponseWriter();
    Banana banana = Banana.class.cast(component);
    responseWriter.endElement("a");
}

What I want to achieve:

<a href="http://www.banana.com">This is an Apple</a>
.
.
.

As you can see I want to encode not on the banana component but on the element it targets using "for" attribute. The closest example I saw is PrimeFaces tooltip, but I can't quite grasp how it utilizes the "for" attribute. http://www.primefaces.org/showcase/ui/overlay/tooltip/tooltipOptions.xhtml#

If someone can point me to the right direction, it would really be appreciated. Thank you.

Spherulite answered 5/7, 2016 at 7:21 Comment(0)
T
5

You can manipulate the component tree during PreRenderViewEvent. During that moment, all components are guaranteed to be present in the tree, and it's guaranteed safe to manipulate the component tree by adding/moving/removing components in the tree. You can find other components in the tree by UIComponent#findComponent(). You can traverse the component tree using UIComponent#getParent() and UIComponent#getChildren(). The getChildren() returns a mutable list which is guaranteed safe to manipulate during PreRenderViewEvent.

Given that you'd like to wrap a given component with a hyperlink, and there exist already a JSF hyperlink component, the <h:outputLink>, it's easier to extend and reuse it to keep the code simple and DRY. Here's a basic kickoff example:

@FacesComponent(createTag=true)
public class Banana extends HtmlOutputLink implements SystemEventListener {

    public Banana() {
        getFacesContext().getViewRoot().subscribeToViewEvent(PreRenderViewEvent.class, this);
    }

    @Override
    public boolean isListenerForSource(Object source) {
        return source instanceof UIViewRoot;
    }

    @Override
    public void processEvent(SystemEvent event) throws AbortProcessingException {
        String forClientId = (String) getAttributes().get("for");

        if (forClientId != null) {
            UIComponent forComponent = findComponent(forClientId);
            List<UIComponent> forComponentSiblings = forComponent.getParent().getChildren();
            int originalPosition = forComponentSiblings.indexOf(forComponent);
            forComponentSiblings.add(originalPosition, this);
            getChildren().add(forComponent);
        }
    }

}

<!DOCTYPE html>
<html lang="en"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:my="http://xmlns.jcp.org/jsf/component"
>
    <h:head>
        <title>SO question 38197494</title>
    </h:head>
    <h:body>
        <h:outputText id="theApple" value="This is an Apple" />
        <my:banana for="theApple" value="http://www.banana.com" />
    </h:body>
</html>

Do note that explicit children.remove() call is not necessary when you already perform a children.add(). It will automatically take care about the child's parent.

Turkey answered 5/7, 2016 at 8:42 Comment(2)
Your solution works well as long as theApple comes first. Nonetheless, thank you.Spherulite
I've fixed the code in answer to cover the other case as well. Nonetheless, thank you.Turkey
B
0

If I correctly understand the problem, you want to define 2 separate JSF components which produce common HTML code. One of possible solutions (as presented @BalusC) is component tree manipulation, but this is almost the same as to define in xhtml in a more natural way:

<my:banana link="http://www.banana.com">
    <h:outputText value="This is an Apple" />
</my:banana>

If for some needs you want to (re)render components referenced via for-attribute, I suggest to use javascript for DOM manipulations on the client side. You just need to define script tag in your banana renderer with appropriate javascript code:

@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
    ResponseWriter writer = context.getResponseWriter();
    writer.startElement("script", null);
    writer.writeAttribute("type", "text/javascript", null);
    // find DOM-element by 'for'-attribute, and wrap it with a hyperlink 
    writer.endElement("script");
}

This is nearly how Primefaces tooltip built.

The benefit is that banana renderer does not aware of how <h:outputText> (or any other) will be rendered.

Blasting answered 5/7, 2016 at 9:48 Comment(1)
From the question: "As you can see I want to encode not on the banana component but on the element it targets using "for" attribute. " so part one of your answer is not 'right'. And if you look at the code in the answer by @BalusC, you'll see that it is not aware either of how the outputText is rendered. So the second part of your second answer is not an 'enhancement'Ankara

© 2022 - 2024 — McMap. All rights reserved.