ICEfaces
  1. ICEfaces
  2. ICE-8366

ace:tooltip - Add ability to dynamically change contents based off of dataTable contents

    Details

    • Type: Improvement Improvement
    • Status: Closed
    • Priority: Major Major
    • Resolution: Fixed
    • Affects Version/s: 3.1.0.RC1
    • Fix Version/s: 3.2
    • Component/s: ACE-Components
    • Labels:
      None
    • Environment:
      All
    • Affects:
      Documentation (User Guide, Ref. Guide, etc.), Sample App./Tutorial

      Description

      Its now possible to define an ace:tooltip outside of an ace:dataTable (or other iterative component). At the moment its not possible to dynamically change the contents of the tooltip based off of a row/cell.

      Withe the ice:panelTooltip this was accomplished by defining a contextValue for the panelGroup. This contextValue could then be retrieved when the displayListener was called through the DisplayEvent object.

        Issue Links

          Activity

          Hide
          Arturo Zambrano added a comment - - edited

          There have been several challenges in implementing this feature in a way that doesn't damage performance in big tables. While a prototype that follows a similar approach as ice:panelTooltip has already been developed, it was preferred to come up with a solution that minimizes the need for additional markup both in the facelet and in the rendered HTML document. Trying to satisfy all these requirements might not be possible and it's necessary to agree on which approach to implement, based on which requirements are more important. I now discuss the evolution of this development, obstacles and possible solutions.

          The changes made for ICE-8155 allowed the ace:tooltip component to target components within iterative containers while ace:tooltip is outside of such container. This was done by collecting the client ids of all the components that were to receive the tooltip functionality, in the render phase, and include them in the initialization script for the tooltip.

          Then, an initial effort to solve this issue was made by introducing a helper component called ace:tooltipContext, which was to be placed inside a table (or iterative container) and whose only job was to hold a value expression that would be evaluated inside the table and whose value would be passed to an event object argument for the display listener method. This component worked by wrapping the component that was triggering the tooltip event. This approach was modeled after the approach used by ice:panelGroup and ice:panelTooltip. This helper component needs to be rendered in the HTML document, so that the row can be detected, pretty much like the role of an ice:panelGroup.

          Since, it was preferred to avoid adding additional markup per row, a new approach was needed to solve this situation. Also, it was observed that with the current approach of specifying all the client ids of the nodes that were going to receive the tooltip functionality, there was a possibility that if during a dynamic update the component with the tooltip functionality changed its external representation (HTML markup), it would lose its tooltip functionality altogether, since the original nodes would be replaced on the page. So, an approach to use a helper component to wrap the entire table was attempted.

          This new approach consisted in wrapping the table in a component provisionally-named ace:tooltipSummoner, which would receive the tooltip functionality and act as a delegate for the inner components that were to actually trigger the tooltip. This involved collecting all the ids in the client from the triggerer element up to the tooltip summoner, passing them to the server, and them using VisitTree procedures to determine the row that triggered the tooltip display event. This did not work as expected, because when the tooltip functionality is applied to this ace:tooltipSummoner wrapper, it is not possible to know which inner element triggered the tooltip display event. The event object returned by jQuery would indicate that the 'target' of the event is the tooltip summoner itself, and not the specific node that was hovered over. Other target properties like 'relatedTarget', 'currentTarget' and 'delegateTarget' didn't return the specific node either. The reason why this happens is that the underlying jQuery plugin 'qtip' uses the .bind() method directly on the node that was specified (in this case the tooltip summoner), and it doesn't provide an option or a way to use delegated events, so the target will always be the element that received the tooltip functionality itself.

          One possible solution would involve modifying qtip to support delegated events and carry on with the approach described above. However, we've been using qtip as a black box since the development of ACE components started. Only very minor modifications have been made. Looking at the code reveals that event bindings are done at different points of the code (which consists of 2000+ lines) and that other parts of the code expect certain conditions and make certain assumptions regarding the registration of DOM events. Making major changes to the qtip code would require a considerable amount of time and effort, and it is uncertain how many side effects this could produce.

          Another possible solution is to go back to the simpler initial approach, using the ace:tooltipContext component. The problem with components losing the tooltip functionality by being replaced on the HTML page could be solved by simply applying the tooltip functionality to the ace:tooltipContext itself, since it simply renders a span element and never changes (since it only has fixed id and class attributes). This approach requires the least amount of effort. However, if the whole table changes at some point, it would be necessary to also re-run the tooltip script to re-apply the tooltip functionality, which could probably be done at the app code level.

          Another solution is a combination of the previous approaches and requires no additional components and uses some of the code developed for the ace:tooltipSummoner approach. It consists in collecting all the client ids, just as in the initial approach, then applying the tooltip functionality to those specified nodes, and then before the tooltip is displayed we collect all the ids of parent nodes, up to the document object so that we can use them in a VisitTree procedure to determine which row triggered the tooltip (there's already code for this). This approach would have the same situation as the ace:tooltipContext approach regarding losing the tooltip functionality if nodes are replaced, but it could be solved by applying the tooltip functionality to nodes that aren't likely to change (like h:panelGroup), wrapping the whole table cell. However it still involves additional markup. This approach requires a moderate amount of effort.

          Show
          Arturo Zambrano added a comment - - edited There have been several challenges in implementing this feature in a way that doesn't damage performance in big tables. While a prototype that follows a similar approach as ice:panelTooltip has already been developed, it was preferred to come up with a solution that minimizes the need for additional markup both in the facelet and in the rendered HTML document. Trying to satisfy all these requirements might not be possible and it's necessary to agree on which approach to implement, based on which requirements are more important. I now discuss the evolution of this development, obstacles and possible solutions. The changes made for ICE-8155 allowed the ace:tooltip component to target components within iterative containers while ace:tooltip is outside of such container. This was done by collecting the client ids of all the components that were to receive the tooltip functionality, in the render phase, and include them in the initialization script for the tooltip. Then, an initial effort to solve this issue was made by introducing a helper component called ace:tooltipContext, which was to be placed inside a table (or iterative container) and whose only job was to hold a value expression that would be evaluated inside the table and whose value would be passed to an event object argument for the display listener method. This component worked by wrapping the component that was triggering the tooltip event. This approach was modeled after the approach used by ice:panelGroup and ice:panelTooltip. This helper component needs to be rendered in the HTML document, so that the row can be detected, pretty much like the role of an ice:panelGroup. Since, it was preferred to avoid adding additional markup per row, a new approach was needed to solve this situation. Also, it was observed that with the current approach of specifying all the client ids of the nodes that were going to receive the tooltip functionality, there was a possibility that if during a dynamic update the component with the tooltip functionality changed its external representation (HTML markup), it would lose its tooltip functionality altogether, since the original nodes would be replaced on the page. So, an approach to use a helper component to wrap the entire table was attempted. This new approach consisted in wrapping the table in a component provisionally-named ace:tooltipSummoner, which would receive the tooltip functionality and act as a delegate for the inner components that were to actually trigger the tooltip. This involved collecting all the ids in the client from the triggerer element up to the tooltip summoner, passing them to the server, and them using VisitTree procedures to determine the row that triggered the tooltip display event. This did not work as expected, because when the tooltip functionality is applied to this ace:tooltipSummoner wrapper, it is not possible to know which inner element triggered the tooltip display event. The event object returned by jQuery would indicate that the 'target' of the event is the tooltip summoner itself, and not the specific node that was hovered over. Other target properties like 'relatedTarget', 'currentTarget' and 'delegateTarget' didn't return the specific node either. The reason why this happens is that the underlying jQuery plugin 'qtip' uses the .bind() method directly on the node that was specified (in this case the tooltip summoner), and it doesn't provide an option or a way to use delegated events, so the target will always be the element that received the tooltip functionality itself. One possible solution would involve modifying qtip to support delegated events and carry on with the approach described above. However, we've been using qtip as a black box since the development of ACE components started. Only very minor modifications have been made. Looking at the code reveals that event bindings are done at different points of the code (which consists of 2000+ lines) and that other parts of the code expect certain conditions and make certain assumptions regarding the registration of DOM events. Making major changes to the qtip code would require a considerable amount of time and effort, and it is uncertain how many side effects this could produce. Another possible solution is to go back to the simpler initial approach, using the ace:tooltipContext component. The problem with components losing the tooltip functionality by being replaced on the HTML page could be solved by simply applying the tooltip functionality to the ace:tooltipContext itself, since it simply renders a span element and never changes (since it only has fixed id and class attributes). This approach requires the least amount of effort. However, if the whole table changes at some point, it would be necessary to also re-run the tooltip script to re-apply the tooltip functionality, which could probably be done at the app code level. Another solution is a combination of the previous approaches and requires no additional components and uses some of the code developed for the ace:tooltipSummoner approach. It consists in collecting all the client ids, just as in the initial approach, then applying the tooltip functionality to those specified nodes, and then before the tooltip is displayed we collect all the ids of parent nodes, up to the document object so that we can use them in a VisitTree procedure to determine which row triggered the tooltip (there's already code for this). This approach would have the same situation as the ace:tooltipContext approach regarding losing the tooltip functionality if nodes are replaced, but it could be solved by applying the tooltip functionality to nodes that aren't likely to change (like h:panelGroup), wrapping the whole table cell. However it still involves additional markup. This approach requires a moderate amount of effort.
          Hide
          Arturo Zambrano added a comment - - edited

          qtip2 will have support for live/delegate events:
          http://craigsworks.com/projects/qtip2/tutorials/events/#live

          Show
          Arturo Zambrano added a comment - - edited qtip2 will have support for live/delegate events: http://craigsworks.com/projects/qtip2/tutorials/events/#live
          Hide
          Arturo Zambrano added a comment -

          Committed fix at revision 30640 in the trunk.

          After trying many different solutions, the following approach was implemented to solve this issue.

          A new component, ace:tooltipDelegate, was added, which will wrap around the actual iterative container that contains the target component. This new component will be referenced in the new 'forDelegate' attribute. It is no longer necessary to use 'forContainer'. In the client side, a jQuery .delegate() event will be registered on this ace:tooltipDelegate component to listen to the user event that will trigger the tooltips in the inner components. Once the event occurs, the code waits until the last node is reached in the event bubbling, then the process begins. At this point, starting from the target element and going up, the ids of the DOM nodes will be checked (if they exist) and if any of them ends with the id specified in the 'for' attribute, then the tooltip will be activated on the node that contains such id. The actual tooltip functionality will be initialized dynamically at this point and only for this specific DOM node. Then, a request will be made if the tooltip has a 'displayListener', sending the client id that was found, in a parameter. In the server, this id will be read and a VisitTree will be performed, starting from the tooltip delegate. At the point where this client id is found, the EL expression specified in the 'from' attribute will be evaluated and the result will be stored in the bean property specified in the 'to' attribute. Then, the display listener will be fired, and a regular response will be returned to the client, with whatever changes were made in the application. Finally, the tooltip will be shown, making use of updated content if that was the intention.

          There were many challenges to overcome to make this solution work. First of all, the underlying library qtip had to be upgraded to the second version, since the first version didn't have support for delegate events. Because the new version uses a different configuration approach, the old approach had to be converted to the new one. It wasn't just a simple mapping of options, since some features work very differently, like the hide and show effects, which now need to be coded manually. There were also slightly different behaviors in some cases. There's no 'beforeShow' event now, but a simple 'show' event, which was making the tooltip display before the updates were received from the server, which was causing the content not to reflect the new changes made by the application. This was fixed by adding more callbacks.

          Because events that were registered via jQuery's .delegate() call don't bubble up to the node where they were applied, but up to its immediate child, the ace:tooltipDelegate had to be created with specific rendering requirements to account for this behavior. Otherwise, a simple h:panelGroup would've been enough.

          Another concern was not breaking backwards compatibility. Since, the initial approach to specify a tooltip outside a data table used the 'forContainer' attribute, it had to be kept and a 'forDelegate' attribute was added to implement this approach. Also, since the displayListener has never taken any custom event objects, the row data had to be made available via two new attributes 'from' and 'to', where in 'from' one specifies an EL expression that is to be evaluated at the same row where the component that triggered the tooltip is located, and then the result is stored in the 'to' attribute.

          Show
          Arturo Zambrano added a comment - Committed fix at revision 30640 in the trunk. After trying many different solutions, the following approach was implemented to solve this issue. A new component, ace:tooltipDelegate, was added, which will wrap around the actual iterative container that contains the target component. This new component will be referenced in the new 'forDelegate' attribute. It is no longer necessary to use 'forContainer'. In the client side, a jQuery .delegate() event will be registered on this ace:tooltipDelegate component to listen to the user event that will trigger the tooltips in the inner components. Once the event occurs, the code waits until the last node is reached in the event bubbling, then the process begins. At this point, starting from the target element and going up, the ids of the DOM nodes will be checked (if they exist) and if any of them ends with the id specified in the 'for' attribute, then the tooltip will be activated on the node that contains such id. The actual tooltip functionality will be initialized dynamically at this point and only for this specific DOM node. Then, a request will be made if the tooltip has a 'displayListener', sending the client id that was found, in a parameter. In the server, this id will be read and a VisitTree will be performed, starting from the tooltip delegate. At the point where this client id is found, the EL expression specified in the 'from' attribute will be evaluated and the result will be stored in the bean property specified in the 'to' attribute. Then, the display listener will be fired, and a regular response will be returned to the client, with whatever changes were made in the application. Finally, the tooltip will be shown, making use of updated content if that was the intention. There were many challenges to overcome to make this solution work. First of all, the underlying library qtip had to be upgraded to the second version, since the first version didn't have support for delegate events. Because the new version uses a different configuration approach, the old approach had to be converted to the new one. It wasn't just a simple mapping of options, since some features work very differently, like the hide and show effects, which now need to be coded manually. There were also slightly different behaviors in some cases. There's no 'beforeShow' event now, but a simple 'show' event, which was making the tooltip display before the updates were received from the server, which was causing the content not to reflect the new changes made by the application. This was fixed by adding more callbacks. Because events that were registered via jQuery's .delegate() call don't bubble up to the node where they were applied, but up to its immediate child, the ace:tooltipDelegate had to be created with specific rendering requirements to account for this behavior. Otherwise, a simple h:panelGroup would've been enough. Another concern was not breaking backwards compatibility. Since, the initial approach to specify a tooltip outside a data table used the 'forContainer' attribute, it had to be kept and a 'forDelegate' attribute was added to implement this approach. Also, since the displayListener has never taken any custom event objects, the row data had to be made available via two new attributes 'from' and 'to', where in 'from' one specifies an EL expression that is to be evaluated at the same row where the component that triggered the tooltip is located, and then the result is stored in the 'to' attribute.
          Hide
          Arturo Zambrano added a comment -

          Changed ace:tooltipDelegate for ace:delegate; changed 'from' and 'to' attributes in ace:tooltip to 'fetch' and 'store' at revision 30726.

          Show
          Arturo Zambrano added a comment - Changed ace:tooltipDelegate for ace:delegate; changed 'from' and 'to' attributes in ace:tooltip to 'fetch' and 'store' at revision 30726.
          Hide
          Arturo Zambrano added a comment -

          Added improvement to not require a displayListener in order to use the delegate ability, added checks to avoid NPE's that could occur under certain scenarios, and updated tld documentation. Also, added demo to the showcase.

          Show
          Arturo Zambrano added a comment - Added improvement to not require a displayListener in order to use the delegate ability, added checks to avoid NPE's that could occur under certain scenarios, and updated tld documentation. Also, added demo to the showcase.

            People

            • Assignee:
              Arturo Zambrano
              Reporter:
              Arran Mccullough
            • Votes:
              1 Vote for this issue
              Watchers:
              1 Start watching this issue

              Dates

              • Created:
                Updated:
                Resolved: