TabSet Dynamic TabPanes

Table of Contents

TabSet Dynamic TabPanes

  Name Size Creator (Last Modifier) Creation Date Last Mod Date Comment  
File tabSet-dynamic-tutorial.zip 88 kB Mark Collette Apr 23, 2012 Apr 23, 2012  

The ACE TabSet component uses TabPane child components to represent each tab. For supporting a configurable or dynamic set of tabs, the designers of ace:tabSet had two options: UIData / UIRepeat iteration, or JSTL ForEach replication. The existing ICE component PanelTabSet uses the UIData paradigm, which is perfect for similarly structured tabs, but when tabs hold disparate types of data, via a ui:include tag, then c:forEach of the tabs is really the only option. Building on this experience, ACE TabSet was designed to use c:forEach to accomplish configurable and dynamic tabs.

With ICEfaces Direct-To-DOM technology, rendered updates are automatically pruned down so that only actual differences need be transmitted to the browser. Therefore, when adding or removing tabs, instead of the whole TabSet markup being sent, only the affected portion of the DOM needs be transmitted. To facilitate this process, to be as optimal as possible, certain steps may be taken. None of these steps are necessary for the TabSet to function with dynamic tabs, they only help tune the updates. The exception being, when using cached tabs, it becomes necessary to limit the updates to leave other cached tabs intact, when adding, removing, or moving tabs.


Best Practices for the Facelet Page


Each portion of code given below is relevant and accomplishes a necessary part of a whole, which allows for optimal updates.

main.xhtml
<h:form id="exampleForm">
    <h:panelGroup id="tabSetPanel">
        <ace:tabSet id="tbset"
                    clientSide="false"
                    selectedIndex="#{dynamic.selectedTabIndex}">
            <c:forEach id="cForEachId" items="#{dynamic.movies}" var="tab">
                <ace:tabPane label="#{tab.title}" id="tab_#{tab.id}">
                    <f:subview id="sub_#{tab.id}">
                        <h:panelGroup id="pnlGrp">
                            <h3><h:outputText value="#{msgs['content.tab.plot.label']}"/></h3>
                            <h:outputText id="spn" value="#{tab.plot}"/>
                        </h:panelGroup>
                    </f:subview>
                </ace:tabPane>
            </c:forEach>
        </ace:tabSet>
    </h:panelGroup>
</h:form>

Going through the code, let's examine the relevance of each portion, one component and tag at a time.

<h:form id="exampleForm">

ace:tabSet does not need to be wrapped in a form, as it can instead use an ace:tabSetProxy with a form elsewhere in the Facelet page, and then be able to have form components within it, for each individual tab. In this tutorial we just happen to use the simplest method, of wrapping the ace:tabSet in an h:form, but it's not necessary for dynamic tabs.

<h:panelGroup id="tabSetPanel">

Again, wrapping the ace:tabSet in an h:panelGroup is not necessary for dynamic tabs, but what this illustrates, which does generally help optimise DOM updates, is that it's best to avoid placing content directly within a form, and instead place it within some container component that has an id. That way when components use the rendered property, and switch between being rendered and not, or the component tree has components dynamically added or removed, the DOM update is limited to the h:panelGroup, and does not cascade up to the form.

<ace:tabSet id="tbset"
    clientSide="false"
    selectedIndex="#{dynamic.selectedTabIndex}">

Giving the tabSet an explicit id, instead of a generated id is usually a good idea. It makes things more clear and obvious when debugging.

This tutorial is for server-side dynamic tabs. Usually with client-side, it's less that the tabs are dynamic and changing over time and more that they're simply configurable, and can be database driven, but essentially remain static after first view. As such, simply using c:forEach and ignoring the rest of this tutorial is generally sufficient for configurable client-side tabs.

The selectedIndex property is used in this tutorial simply for driving which tab is removed when the Remove Current button is pressed.

<c:forEach id="cForEachId" items="#{dynamic.movies}" var="tab">

The main thing with using c:forEach is that the items attribute should go to a list of objects where each item in the list has some sort of unique identifier that does not change over time, which we can access via the var attribute.

<ace:tabPane label="#{tab.title}" id="tab_#{tab.id}">
    <f:subview id="sub_#{tab.id}">

Both the ace:tabPane and the f:subview should have an explicit id, that's derived from the unique and unchanging id that the model has to represent that tab, which we access via the c:forEach var. What will not work is some kind of index value, since when a tab is inserted or removed from the beginning or middle of the list, the index change will cascade over all of the tabs after it in the list, which will cause all of them to update in the browser, and possibly cause other issues.

f:subview is necessary because it acts as a NamingContainer, so that the clientIds of everything within it will be different from tab to tab. Since we need to use explicit ids within the tabPane, and neither tabPane nor tabSet are NamingContainers themselves, there would be duplicate ids without using f:subview, if the tabs used the same content, say from using ui:include on the same included facelet file.

<h:panelGroup id="pnlGrp">

Just like how a container with an id was recommended to contain updates from cascading up to the form, it's best to put one right inside the f:subview, since the f:subview acts as a NamingContainer in a logical sense, but doesn't actually render anything itself, so this h:panelGroup will be the point at which the unique clientId from the f:subview actually gets rendered into the DOM.

<h3><h:outputText value="#{msgs['content.tab.plot.label']}"/></h3>
<h:outputText id="spn" value="#{tab.plot}"/>

At this point, no further special considerations need be taken, components need not have explicit ids, but they can have them as desired.



Java Tutorial Source


There is no special support required for dynamic tabs, in the Java code, aside from the previously mentioned unique and stable ids in the objects used to represent the tabs by the c:forEach.

public Dynamic() {
    String movieIdsCommaDelim = TutorialMessageUtils.getMessage("movies");
    String[] movieIds = movieIdsCommaDelim.split(",");
    for (String movieId : movieIds) {
        String title = TutorialMessageUtils.getMessage("movie." + movieId + ".title");
        String plot = TutorialMessageUtils.getMessage("movie." + movieId + ".plot");
        Movie movie = new Movie(movieId, title, plot);
        movies.add(movie);
    }
    ...
}

In the constructor, configurable tabs are demonstrated. Editing the included message bundle at tabSet-dynamic-tutorial/src/main/resources/messages_en.properties will be reflected in the tabs shown. Tabs can be added or removed by adding or removing the tab id from the comma delimited list under the movies key in the resource bundle, and then adding or removing the corresponding title and plot entries.

movies = shawshank,godfather,pulpfiction,casablanca

movie.shawshank.title = The Shawshank Redemption
movie.shawshank.plot = Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.
...

public void addTab(ActionEvent event) {
    int i = counter++;
    String title = TutorialMessageUtils.formatMessage("action.tabAdd.template.title", new Object[] {i});
    String plot = TutorialMessageUtils.formatMessage("action.tabAdd.template.plot", new Object[] {i});
    Movie movie = new Movie("movie"+i, title, plot);
    movies.add(movie);
    richText = TutorialMessageUtils.formatMessage("action.tabAdd", new Object[] {movie.getTitle()});
}

public void removeCurrent(ActionEvent event){
    Movie movie = movies.remove(this.selectedTabIndex);
    richText = TutorialMessageUtils.formatMessage("action.tabRemove", new Object[] {selectedTabIndex, movie.getTitle()});
    if (selectedTabIndex >= movies.size()) {
        selectedTabIndex = 0;
    }
}

Stable unique ids in the added tabs are accomplished by using an ever increasing counter in the bean, which is view scoped. Had the counter been in a session or application scoped bean, that would have worked as well.

When removing the current tab, if that was the last tab, we can't leave the selectedTabIndex value, since the TabSet will jump to depicting the first tab as the current one, without itself setting the selected tab index to 0, and then if the user attempts to remove that tab, it won't work, since it will be trying to remove past the end of the list, and not the first item in the list.

That concludes this overview of using dynamic and configurable tabs with ACE TabSet. If you're interested in further details of how this example works, take a look at the complete source code below; or sign up for ICEfaces training for a complete guide to this example and every feature of ICEfaces!

  Name Size Creator (Last Modifier) Creation Date Last Mod Date Comment  
File tabSet-dynamic-tutorial.zip 88 kB Mark Collette Apr 23, 2012 Apr 23, 2012  

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.

© Copyright 2018 ICEsoft Technologies Canada Corp.