Friday Apr 13, 2012

Skinning af:selectOneChoice

A question came in today about how to skin the selection button (Image of a drop down list)  of an <af:selectOneChoice>. If you have a delve in the ADF Skinning editor, you'll find that there are selectors for the selectOneChoice when in compact mode (af|selectOneChoice::compact-dropdown-icon-style), however, there is not a selector for the icon in the "normal" mode. I had a quick delve into the skinning source files that you can find in the adf-richclient-impl-11.jar and likewise there seemed to be no association there. However, a quick sample page and a peek with Chrome developer tools revealed the problem.  The af:selectOneChoice gets rendered in the browser as a good old <select> element (reasonable enough!). Herein lies the problem, and the reason why there is no skin selector. The <select> HTML element does not have a standard way of replacing the image used for the dropdown button.  If you have a search around with your favorite search engine, you can find various workarounds and solutions for this.  For example, using Chrome and Safari you can define the following for the select element:

select {
  -webkit-appearance: listbox;
  background-image: url(blob.png); 
  background-position: center right;
  background-repeat: no-repeat;
  }

Which gives a very exciting select box: Image of updated selectOneChoice .

Wednesday Apr 11, 2012

More Animation - Self Dismissing Dialogs

In my earlier articles on animation, I discussed various slide, grow and  flip transitions for items and containers.  In this article I want to discuss a fade animation and specifically the use of fades and auto-dismissal for informational dialogs.  If you use a Mac, you may be familiar with Growl as a notification system, and the nice way that messages that are informational just fade out after a few seconds. So in this blog entry I wanted to discuss how we could make an ADF popup behave in the same way. This can be an effective way of communicating information to the user without "getting in the way" with modal alerts. This of course, has been done before, but everything I've seen previously requires something like JQuery to be in the mix when we don't really need it to be. 

The solution I've put together is nice and generic and will work with either <af:panelWindow> or <af:dialog> as a the child of the popup. In terms of usage it's pretty simple to use we  just need to ensure that the popup itself has clientComponent is set to true and includes the animation JavaScript (animateFadingPopup) on a popupOpened event:

<af:popup id="pop1" clientComponent="true">
  <af:panelWindow title="A Fading Message...">
   ...
 </af:panelWindow>
  <af:clientListener method="animateFadingPopup" type="popupOpened"/>
</af:popup> 

 The popup can be invoked in the normal way using showPopupBehavior or JavaScript, no special code is required there.

As a further twist you can include an additional clientAttribute called preFadeDelay to define a delay before the fade itself starts (the default is 5 seconds) . To set the delay to just 2 seconds for example:

<af:popup ...>
  ...
  <af:clientAttribute name="preFadeDelay" value="2"/>
  <af:clientListener method="animateFadingPopup" type="popupOpened"/> 
</af:popup>

The Animation Styles 

As before, we have a couple of CSS Styles which define the animation, I've put these into the skin in my case, and, as in the other articles, I've only defined the transitions for WebKit browsers (Chrome, Safari) at the moment. In this case, the fade is timed at 5 seconds in duration.

.popupFadeReset {
  opacity: 1;
}

.popupFadeAnimate {
  opacity: 0;
  -webkit-transition: opacity 5s ease-in-out;
}

As you can see here, we are achieving the fade by simply setting the CSS opacity property.

The JavaScript

The final part of the puzzle is, of course, the JavaScript, there are four functions, these are generic (apart from the Style names which, if you've changed above, you'll need to reflect here):

  1. The initial function invoked from the popupOpened event,  animateFadingPopup which starts a timer and provides the initial delay before we start to fade the popup.
  2. The function that applies the fade animation to the popup - initiatePopupFade.
  3. The callback function - closeFadedPopup used to reset the style class and correctly hide the popup so that it can be invoked again and again.  
  4. A utility function - findFadeContainer, which is responsible for locating the correct child component of the popup to actually apply the style to.

Function - animateFadingPopup

This function, as stated is the one hooked up to the popupOpened event via a clientListener. Because of when the code is called it does not actually matter how you launch the popup, or if the popup is re-used from multiple places. All usages will get the fade behavior.

/**
 * Client listener which will kick off the animation to fade the dialog and register
 * a callback to correctly reset the popup once the animation is complete
 * @param event
 */
function animateFadingPopup(event) {
  var fadePopup = event.getSource();
  var fadeCandidate = false;
  //Ensure that the popup is initially Opaque
  //This handles the situation where the user has dismissed
  //the popup whilst it was in the process of fading
  var fadeContainer = findFadeContainer(fadePopup);
  if (fadeContainer != null) {
    fadeCandidate = true;
    fadeContainer.setStyleClass("popupFadeReset");
  }
  //Only continue if we can actually fade this popup
  if (fadeCandidate) {
    //See if a delay has been specified
    var waitTimeSeconds = event.getSource().getProperty('preFadeDelay');
    //Default to 5 seconds if not supplied
    if (waitTimeSeconds == undefined) {
      waitTimeSeconds = 5;
    }
    // Now call the fade after the specified time
    var fadeFunction = function () {
      initiatePopupFade(fadePopup);
    };
    var fadeDelayTimer = setTimeout(fadeFunction, (waitTimeSeconds * 1000));
  }
}

The things to note about this function is the initial check that we have to do to ensure that the container is currently visible and reset it's style to ensure that it is.  This is to handle the situation where the popup has begun the fade, and yet the user has still explicitly dismissed the popup before it's complete and in doing so has prevented the callback function (described later) from executing. In this particular situation the initial display of the dialog will be (apparently) missing it's normal animation but at least it becomes visible to the user (and most users will probably not notice this difference in any case).

You'll notice that the style that we apply to reset the  opacity - popupFadeReset, is not applied to the popup component itself but rather the dialog or panelWindow within it. More about that in the description of the next function findFadeContainer().

Finally, assuming that we have a suitable candidate for fading, a JavaScript  timer is started using the specified preFadeDelay wait time (or 5 seconds if that was not supplied). When this timer expires then the main animation styleclass will be applied using the initiatePopupFade() function

Function - findFadeContainer

As a component, the <af:popup> does not support styleClass attribute, so we can't apply the animation style directly.  Instead we have to look for the container within the popup which defines the window object that can have a style attached.  This is achieved by the following code:

/**
 * The thing we actually fade will be the only child
 * of the popup assuming that this is a dialog or window
 * @param popup
 * @return the component, or null if this is not valid for fading
 */
function findFadeContainer(popup) {
  var children = popup.getDescendantComponents();
  var fadeContainer = children[0];
  if (fadeContainer != undefined) {
    var compType = fadeContainer.getComponentType();
    if (compType == "oracle.adf.RichPanelWindow" 
     || compType == "oracle.adf.RichDialog") {
      return fadeContainer;
    }
  }
  return null;
}

 So what we do here is to grab the first child component of the popup and check its type. Here I decided to limit the fade behaviour to only <af:dialog> and <af:panelWindow>. This was deliberate.  If  we apply the fade to say an <af:noteWindow> you would see the text inside the balloon fade, but the balloon itself would hang around until the fade animation was over and then hide.  It would of course be possible to make the code smarter to walk up the DOM tree to find the correct <div> to apply the style to in order to hide the whole balloon, however, that means that this JavaScript would then need to have knowledge of the generated DOM structure, something which may change from release to release, and certainly something to avoid. So, all in all, I think that this is an OK restriction and frankly it's windows and dialogs that I wanted to fade anyway, not balloons and menus. You could of course extend this technique and handle the other types should you really want to.

One thing to note here is the selection of the first (children[0]) child of the popup. It does not matter if there are non-visible children such as clientListener before the <af:dialog> or <af:panelWindow> within the popup, they are not included in this array, so picking the first element in this way seems to be fine, no matter what the underlying ordering is within the JSF source.  If you wanted a super-robust version of the code you might want to iterate through the children array of the popup to check for the right type, again it's up to you. 

Function -  initiatePopupFade

 On to the actual fading. This is actually very simple and at it's heart, just the application of the popupFadeAnimate style to the correct component and then registering a callback to execute once the fade is done.

/**
 * Function which will kick off the animation to fade the dialog and register
 * a callback to correctly reset the popup once the animation is complete
 * @param popup the popup we are animating
 */
function initiatePopupFade(popup) {
  //Only continue if the popup has not already been dismissed 
  if (popup.isPopupVisible()) {
    //The skin styles that define the animation 
    var fadeoutAnimationStyle = "popupFadeAnimate";
    var fadeAnimationResetStyle = "popupFadeReset";

    var fadeContainer = findFadeContainer(popup);
    if (fadeContainer != null) {
      var fadeContainerReal = AdfAgent.AGENT.getElementById(fadeContainer.getClientId());
      //Define the callback this will correctly reset the popup once it's disappeared
      var fadeCallbackFunction = function (event) {
        closeFadedPopup(popup, fadeContainer, fadeAnimationResetStyle);
        event.target.removeEventListener("webkitTransitionEnd", fadeCallbackFunction);
      };
      //Initiate the fade
      fadeContainer.setStyleClass(fadeoutAnimationStyle);
      //Register the callback to execute once fade is done
      fadeContainerReal.addEventListener("webkitTransitionEnd", fadeCallbackFunction, false);
    }
  }
}

I've added some extra checks here though. First of all we only start the whole process if the popup is still visible. It may be that the user has closed the popup before the delay timer has finished so there is no need to start animating in that case. Again we use the findFadeContainer() function to locate the correct component to apply the style to, and additionally we grab the DOM id that represents that container.  This physical ID is required for the registration of the callback function. The closeFadedPopup() call is then registered on the callback so as to correctly close the now transparent (but still there) popup.

Function -  closeFadedPopup

The final function just cleans things up:

/**
 * Callback function to correctly cancel and reset the style in the popup
 * @param popup id of the popup so we can close it properly
 * @param contatiner the window / dialog within the popup to actually style
 * @param resetStyle the syle that sets the opacity back to solid
 */
function closeFadedPopup(popup, container, resetStyle) {
  container.setStyleClass(resetStyle);
  popup.cancel();
} 

First of all we reset the style to make the popup contents opaque again and then we cancel the popup.  This will ensure that any of your user code that is waiting for a popup cancelled event will actually get the event, additionally if you have done this as a modal window / dialog it will ensure that the glasspane is dismissed and you can interact with the UI again. 

What's Next?

There are several ways in which this technique could be used, I've been working on a popup here, but you could apply the same approach to in-line messages. As this code (in the popup case) is generic it will make s pretty nice declarative component and maybe, if I get time, I'll look at constructing a formal Growl component using a combination of this technique, and active data push. Also, I'm sure the above code can be improved a little too.  Specifically things like registering a popup cancelled listener to handle the style reset so that we don't loose the subtle animation that takes place when the popup is opened in that situation where the user has closed the in-fade dialog.

Wednesday Feb 29, 2012

CSS Animations - The Panel Flip

In the my final (for now) discussion on CSS3 animations with ADF Faces Rich Client  UI I wanted to discuss the most complex, but fundamentally useful (from a technique perspective) of the three animations shown in the introduction posting. The first two animations that I showcased were simply triggered by the browser's inherent mouse-over functionality. I didn't actually need to do anything to make the animation happen apart from assigning the correct styleClass to the panel or button.  In this final animation I'm triggering the animation from code and what's more I'm actually chaining together two animations with a server side call made in the middle. To break it down here's the logical flow:

Initiate a rotation of 90 degrees --> Change the contents of the panel -->  Return rotation to 0 degrees.

So the question is, how do we chain all of these actions together, given that the initial rotation will itself take a certain amount of time (2 seconds)?  Well, to handle this we have to write a little bit of JavaScript  that will first of all set the styleClass to provide the first 90deg animation and also register a listener function which will execute once the rotation is finished.  This callback function then executes the Server code we need and then kicks off the reverse animation. 

The Animations  

So first of all, the two styles that define the animations. This should be fairly obvious and understandable by now:

.rotateOut {
    -webkit-transition-property: -webkit-transform;
    -webkit-transform: rotateY(90deg);
    -webkit-transition-timing-function: ease-in-out;
    -webkit-transition-duration: 2s;
)
.rotateReturn {
    -webkit-transition-property: -webkit-transform;
    -webkit-transform: rotateY(0deg);
    -webkit-transition-timing-function: ease-in-out;
    -webkit-transition-duration: 2s;
}

The JSF components

In this example I'm using a switcher which is controlled by a managed bean  called uiManager. This switcher will either display the panelHeader that contains the inputForm or the comments screen. Contrary to your expectation though, it's not these panels managed by the switcher that we'll actually be animating, but rather a parent of the switcher, another panelGroupLayout. So the hierarchy looks a  like this:

  1. Panel group layout that will be animated
  2. Decorative box that will apparently rotate  
  3. Panel group layout that will be refreshed to show the change in contents
  4. Switcher 
<af:panelGroupLayout layout="vertical" 
                     id="flip" 
                     clientComponent="true"
                     binding="#{animationBB.flipPanel}" 
                     styleClass="rotateReturn">
<af:decorativeBox theme="dark">
  <f:facet name="center">
      <af:panelGroupLayout id="refresh" 
                           layout="vertical" 
                           binding="#{animationBB.refreshPanel}">
        <af:switcher defaultFacet="ASide" 
                     facetName="#{uiManager.flipAnimationSelectedSide}" >
          <f:facet name="ASide">
            <af:panelHeader text="Basic Information"> 
              ...
            </af:panelHeader> 
          </f:facet> 
          <f:facet name="BSide"> 
            <af:panelHeader text="Comments">
              ...
            </af:panelHeader> 
          </f:facet> 
        </af:switcher> 
      </af:panelGroupLayout>
    </f:facet> 
  </af:decorativeBox>
</af:panelGroupLayout>  

Notice that the outer panelGroupLayout id="flip" has a binding to a backing bean for the page, this is so we can get the correct clientId back for it (look back at my Visual Notifications During Long Running Transactions article for details of this technique). The inner pgl, id="refresh", also has a binding to the backing bean which we will use later in the serverListener to force a refresh on that panel and the switcher inside it.

 Within each of the switcher "panels" is a button (not shown above) that will actually trigger the animation.  Here's the code for the button on the A-Side:

<af:commandButton text="More..." 
                  partialSubmit="true"
                  clientComponent="true" 
                  id="cbflip1">
  <af:clientAttribute name="flipPanel"
                      value="#{animationBB.flipClientId}"/>
  <af:clientListener method="animateFlipPanel"
                     type="action"/>
  <af:serverListener type="flipEvent"
                     method="#{animationBB.handlePanelFipEvent}"/>
</af:commandButton>

The code for the B-Side button is identical. To break that down we have three things of interest:

  1. A clientAttribute that is used to send the client id of the panel that we want to spin to JavaScript
  2. A clientListener to actually wire up a JavaScript  event to the button press
  3. A serverListener which identifies the server side method that will be called half way through the animation to tell the switcher to change contents.

So let's look at each of those:

The Client Attribute

The client attribute is bound to the page backing bean where there is a simple function that asks the panel that will be "flipped" for it's client ID, in this case the flipPanel variable contains the reference to the RichPanelGroupLayout which is bound to that outer panel:

/**
 * Gets the correct client component ID for this panel in the context in which the 
 * panel is placed. This provides a safe way of getting the client ID if the component
 * is embedded in a region or similar where the computed path may change depending on the use.
 * @return clientID for use in JavaScript 
 */
 public String  getFlipClientId() {
   return flipPanel.getClientId(FacesContext.getCurrentInstance());
}

The JavaScript

The clientListener on the button calls the animateFlipPanel method. This is stored in a .js file referenced using the <af:resource> tag in the metaContainer facet of the document:

function animateFlipPanel(event) {
    //Get the ID of the panel to flip. 
    //This has been passed in via the clientAttribute 
    var fpId = event.getSource().getProperty('flipPanel');
    //Grab a reference to this button that raised the 
    //event. we need this to register the callback against
    var raisingComponent = event.getSource();

    //First animation sends us to 90deg
    var transition = "rotateOut";
    flipPanel = AdfPage.PAGE.findComponentByAbsoluteId(fpId);
    initiateClientFlip(raisingComponent, flipPanel, transition);
} 

 And here's the method that assigns the animation:

/**
 * Kick off the animation - rotate the panel to 90 degrees which will make it  
 * disappear as it's sideways on to us.
 * Then register the callback to rotate back once the animation is finished
 * @param raisingComponent - the button that kicked things off
 * @param flipPanel - the PGL to animate (assign style to) 
 * @param transition - the style to apply
 */
function initiateClientFlip(raisingComponent, flipPanel, transition) {
  //Set the style to animate the rotation
  flipPanel.setStyleClass(transition);
    
  /* Setup a callback to reverse the animation once the transition is finished.
   * Note that we remove the transition listener once it's executed so that 
   *   it is not called by the flipBack transition as well 
   * Note also that we have to attach the listener to the underlying DOM object
   */
    
  // Get the DOM object that represents this panelGroupLayout
  var flipPanelReal = AdfAgent.AGENT.getElementById(flipPanel.getClientId());
   
  //Define the callback 
  var reverseTransition = "rotateReturn"
  var flipBackFunction = function (event) 
           {animateFlipBack(raisingComponent, flipPanel, reverseTransition); 
            event.target.removeEventListener("webkitTransitionEnd",flipBackFunction);};
    
  // Add the transition listener to queue up both the animation back  
  // and the server side change which will apparently flip the
  // contents of the panel 
  flipPanelReal.addEventListener("webkitTransitionEnd", flipBackFunction,false);
} 

So the actual animation part of the above code is trivial, we just set the new style to kick that off.  The exciting part comes in the definition of the eventListener (webkitTransitionEnd) which is set up to execute once the animation has finished. Like everything that I have covered in this short series, the code here is webkit (Chrome & Safari) specific. Similar events exist both in generic terms for the future and in specific versions for FireFox and IE.

And finally the animateFlipBack() function and a convenience method that it uses:

 /**
 * Once the initial animation is done this method is invoked to change the contents of the panel and then
 * animate back to the starting point giving the illusion of a full 180 degree flip
 * @param raisingComponent - reference to the button so we can invoke the serverListener
 * @param flipPanel - pgl to animate
 * @param reverseTransition - animation style
 */
function animateFlipBack(raisingComponent, flipPanel, reverseTransition) {
  // Call the event on the server which will cause the switcher to 
  // change it's contents and issue a PPR event
  raiseServerFlipEvent(raisingComponent);

  //Now start the return animation
  flipPanel.setStyleClass(reverseTransition);   
}

/**
 * Simple function to queue up the server side event on the button that 
 * instigated the flip. This event does not need to send a payload
 * @param raisingComponent - the button 
 */
function raiseServerFlipEvent(raisingComponent){
    var flipEvent = new AdfCustomEvent(raisingComponent,"flipEvent",{},false);
    flipEvent.queue(true);
}

The serverListener 

The role of the serverListener code is to toggle the facet that the switcher is currently displaying and then force the panel containing the switcher to refresh with a PPR event:

/**
 * Event raised from JavaScript to tell us that the flip animation is underway and we 
 * need to change the content on the panel 
 * @param clientEvent
 */
public void handlePanelFipEvent(ClientEvent clientEvent) {
  String currentSide = _uiManager.getFlipAnimationSelectedSide();
  String targetSide = UIManager.A_SIDE;
  if (currentSide.equals(UIManager.A_SIDE)){
      targetSide = UIManager.B_SIDE;
  }
  _logger.info("Flipping to " + targetSide);
  getUiManager().setFlipAnimationSelectedSide(targetSide);
        
  // And queue the client refresh
  AdfFacesContext.getCurrentInstance().addPartialTarget(getRefreshPanel());
}

 This function as you can see is a toggle based on the current value stored in the uiManager.  The UIManager itself is a session scoped bean that just holds the current side value.  It is injected into this backing bean using a managed property in the managed bean definition. The function getRefreshPanel() returns a reference to the pgl that surrounds the switcher (id="refresh" in the hierarchy above). 

Wrap Up 

The technique that I've discussed within this article may seem a little complex but in fact it's all quite logical and can be quite easily extended and made generic. You can see how more complex animations may be achieved by stringing together a whole sequence of callbacks to execute after the previous animation has completed and how you can wire in server side calls into the mix. 

Tuesday Feb 21, 2012

CSS Animations - Slide Out Panel

In this second the detailed articles on the animation prototype I'm going to look at the animation of a whole group of components at once in the form of a slide out panel menu effect. This animation combines two effects (if you look closely),  The actual slide-out, and, in parallel, a shadow around the panel.  Just like the first example, no Java or JavaScript code is required for this example, we can just use default CSS hover functionality to trigger the animation.

Again, just a warning.  This prototype was only set up for the WebKit based browsers (Safari and Chrome). So where I talk about -webkit* styles below be aware that these will have different names on FireFox (usually -moz*) and IE. A stylesheet that handles more browsers will have to include these versions as well. 

The start point for this article assumes that your style sheet (via a custom skin) already exists (go back to the first article if you've not read this yet).

However, before we get too excited about the animations we need to think a little about layout.  The last thing we want to do with these funky effects is to mess up the rest of the screen, and so at this stage, where we've not got any scripting involved, we need to make sure that space is available in the UI for the slide out. To do this I started out by laying out my components thus:

<af:panelGroupLayout (Horizontal)>
  <af:panelGroupLayout (Vertical)> - will be animated
    [Menu] 
  </af:panelGroupLayout> 
  <af:spacer .../>  
  <af:panelGroupLayout (Vertical)>
    [Decorative Box etc.] 
  </af:panelGroupLayout>  
</af:panelGroupLayout> 

So basically the panelGroupLayout that will be animated already has space "reserved" for it on the screen and, at design time, it occupies that slot. At this stage you also need to work out how wide the panel that you are animating will be once populated. This is so you can push it off screen, but still expose enough for the mouse to hover over. In my case the pgl comes out at around 100px once populated.

Hint: To keep things simple with these "container" animations I recommend that you attach effects to a vertical panelGroupLayout. In HTML terms these components get rendered as a simple html <div> which is perfect and makes things much easier to debug if the styles don't seem to be working.

On to the animation. When I started this animation I was not sure what components I would be animating so, rather than defining a specific sub-type as I did for the button in the last article, I'm defining a generic styleClass that could be used with any components. Again we need the base class(.slideout) and the :hover selector for it.

.slideout
{
}
.slideout:hover
{
} 

Again concentrating on the base selector first, let's look at the transitions. This time I'm kicking it up a notch (to coin a phrase) by desiring two concurrent animations, the slide out and the shadow.  Just like the last example with the commandButton we want to pair effects so that any effect I put on the hover needs to be mirrored and reversed by the base selector. This way, both the mouse-over and mouse-out will be smoothly animated.

So here's the base selector.  This "parks" the panel off screen with a little bit hanging out for the mouse to hover over and start the animation:

.slideout
{
  position: relative;
  -webkit-transition-property: left, -webkit-box-shadow; ;  
  -webkit-transition-timing-function: linear, ease-out;
  -webkit-transition-duration: 1s, 1s;  
  -webkit-box-shadow: 0px 0px 0px;
  left: -80px; 
} 

The story here is going to be similar to the simple button scaling discussed in part 1, but there are some differences worthy of mention.  First of all the position property. As part of this animation we're actually moving a <div> and all it's contents around the screen. I want to do that in relation to it's initial position (remember that we started out with the panel in a conventional layout). So the relative keyword is used for this. The other option here is absolute. Try setting this to absolute if you give this a go, you'll notice that the results can be, shall we say unpredictable. You need to be really careful if you're trying to move things around out of context and could end up fighting with the ADF layout algorythms. So stick to relative for now!

Next the -webkit-transition-property - this time it contains two properties, separated by a comma: left and -webkit-box-shadow. So I'm declaring here that those are the two properties I'll be animating the change in. As I'm now dealing with two properties the other -webkit-transition* properties also contain two values separated by a comma and those refer to each property transition. Thus if you look at the timing function, you'll see how the animation in the change of the left value will be linear whereas the shadow will use the ease-in-out rate of change just like the button did.  Again have a play with this and see what the difference is between the timing effects, you have total control. 

Both animations take place concurrently and take place over the same 1s duration although they could be of different durations.  In the next article I'll be looking at how animations can be queued up to provide more complex effects. 

Finally we have the endpoints for  the shadow (basically no shadow) and left. Notice with the left property, it is set to -80px. Recall that the positioning for this style is relative, so this really just tells the panel to shift 80px to the left from its starting position, which happens, in this case, to place it mostly, but not quite all, off screen.

Next the hover selector, it is, as you would expect pretty similar except that we move the pgl back to where it belongs at 0px (relative) and introduces some shadow:

.slideout:hover
{
  position: relative;
  -webkit-transition-property: left, -webkit-box-shadow;  
  -webkit-transition-timing-function: linear, ease-in;
  -webkit-transition-duration: 1s, 1s;  
  -webkit-box-shadow: 10px 10px 5px #888;
  left: 0px;
}

The final step is, of course, to assign this style to the pgl itself. Again this is done with the styleClass property 

<af:panelGroupLayout id="pglmenu" layout="vertical" styleClass="slideout"> 
... 

There you have it.  In the final article in this series I'll be investigating a more complex animation involving JavaScript and other goodies - stay tuned! 

Monday Feb 20, 2012

CSS Animations - Basic Hover Scaling

In this first of the detailed articles on the animation prototype I wanted to look at the basic setup of application and the simplest of the animations - the Pick Me! button.  The Pick Me! button animates to draw attention to itself as the mouse moves over it.  The actual animation itself is a simple scaling so it's a good place to start. 

Again, just a warning.  This prototype was only set up for the WebKit based browsers (Safari and Chrome). So where I talk about -webkit* styles below be aware that these will have different names on FireFox (usually -moz*) and IE. A stylesheet that handles more browsers will have to include these versions as well. 

The basis of all of this work is a custom skin so that's the first thing to create. In 11.1.2 this is really as simple as going to the New Gallery and creating a new skin in the project.  Here I've extended the fusionFx-simple-v2.desktop skin as I want to keep the standard look and feel.  If you've not created a skin for your applications before, there are plenty of articles out there to help you, so I won't dwell on that.

Now in the skin itself I want to create a sub-type of button which will do the animation.  I don't want to animate every button, that would probably make the users seasick.  So the first step is to create a sub-type of the button thus:

af|commandButton.zoomOnFocus
{
} 

This sub-type can then  applied to the selected buttons using the styleClass property:

<af:commandButton text="Pick Me!" styleClass="zoomOnFocus" ...>

Now to implement the  zoom behaviour, fortunately this does not involve writing any code apart from the actual CSS to provide the style. To do this we define a :hover selector for the zoomOnFocus in the skin:

af|commandButton.zoomOnFocus:hover
{
} 

Now we fill that in with the desired animation, I'll explain each of the lines next:

af|commandButton.zoomOnFocus:hover
{
  -webkit-transition-property: -webkit-transform;
  -webkit-transform: scale(2,2); 
  -webkit-transition-timing-function: ease-in-out;
  -webkit-transition-duration: 1s;  
}

The first line,  -webkit-transition-property defines which property, -webkit-transform, should be animated. The second line -webkit-transform itself defines the end result of the animation which is a scale function. This takes two arguments for the x and y scaling respectively, so scale(2,2) doubles the size of the button in both directions.

The third line  -webkit-transition-timing-function, defines the progression of the animation in time. There are various options for this, the ease-in-out function starts the animation slowly, then speeds up and then slows down again towards the end. This provides a pleasing effect to the eye.

Finally -webkit-transition-duration defines how long the animation should take.

If you do all of this, the zoom will work nicely as you move the mouse over the button, however, as you move the mouse out the style will snap back instantly, so we need to complete the recipe with an animation the other way on the base style:

af|commandButton.zoomOnFocus
{
  -webkit-transition-property: -webkit-transform;
  -webkit-transform: scale(1,1); 
  -webkit-transition-timing-function: ease-in-out;
   -webkit-transition-duration: 1s;  
}

This style is pretty much identical to the :hover version.  The only difference is the scale() factor which is changed to 1,1 to return the button to it's normal size.  With this all in place, the button will gracefully shrink back to its normal size. 

So that's the first, very simple animation, in the next article in the series I'll look at the only slightly more complex slide-out panel animation. 

Friday Feb 17, 2012

Experiments with CSS animations in ADF Applications - Introduction

We have had several ADF users experimenting with JQuery to achieve animation effects and that made me wonder how feasible it would be to work with pure CSS3 animations within the context of an ADF application.  From the point of view of the product, we're thinking about how we might express a broader range of animations in a declaritive way, but for now I wanted to see what was possible with the product as it stands.  To wet your appetite, here's a short video of my initial prototype.  I'll explain all of the effects in follow up articles - so be patient with me and, of course, keep coming back to the blog for more!

This prototype was written in 11.1.2.1, but there is nothing there that would not work in 11.1.1.n. Also I've only worked with WebKit browsers (Chrome / Safari so far),  this is still an area where the different browser families need different treatment. This will be an exercise for you the reader. 

If you have an up to date version of Chrome, Safari or IE9 you can play the following short video in-line, Firefox users, sorry I had problems with encoding to ogg format, you'll have to download:

If not download it from here (Size: 374k) 

I've used three sets of animations here:

  1. Simple animated zoom on the Pick Me! button. It grows and then shrinks as the mouse moves over and out.
  2. Panel slide-out effect with shadow for the "menu" panel.
  3. Panel flip effect where the panel rotates through 180 degrees to show some different content, the kind of effect you'll see on your phone fairly often. (I'll admit that this one is very cool)

Tuesday Jan 24, 2012

Visual Notification During Long Running Transactions

From time to time, there will be transactions within your application which do not finish within the "blink of an eye". Later this week I'll be writing up a series of articles specifically on launching long running tasks asynchronously, but first I wanted to cover the case of a transaction which has to happen in real-time but ties up the browser whilst it's doing so. 

A file upload is a typical example of this. The user selects a 500MiB file and presses submit, it's probably time to go and get a cup of tea.  So the question is, how do you tell the user to go and find something more interesting to do for a while and, for that matter, please don't close your browser window...

Both Frank Nimphius and Andrejus Baranovskis have written articles in the past  (How-to show a glasspane and splash screen for long running queries and ,Glasspane in ADF Faces RC, respectively). However, both of those articles concentrated on showing a dialog as a glasspane to block user input and notifiy the user of some information.  In my case the upload was already in a dialog, so popping up another layered dialog on top of that would be ugly, so I wanted to find a way to display a loading indicator of some sort, in-line. 

The result is a JavaScript routine that can be called from  a clientListener which will either pop the glasspane and dialog if you point it to a dialog, otherwise it will display an inline component, such as an image with a spinning logo or some text.

The Script

 Here is the JavaScript, the first method showWhenBusy(), is the one called from the clientListener. It reads the ID of the component that we want to show/hide from an clientAttribute called loadingIndicatorId. This makes the code nice and generic as we've not had to hardcode component IDs. 

//Global variable to hold the component ref.
var loadingIndicatorComponent; 

function showWhenBusy(event) {
  //get the dialog or other component we want to show and hide
  var componentId = event.getSource().getProperty('loadingIndicatorId');
  loadingIndicatorComponent = AdfPage.PAGE.findComponent(componentId);
    
  if (loadingIndicatorComponent != null) {
    AdfPage.PAGE.addBusyStateListener(loadingIndicatorComponent,handleBusyStateCallback);        
    event.preventUserInput();
  }
  else {
    AdfLogger.LOGGER.logMessage(AdfLogger.SEVERE, "Requested indicator compoenent not found");
  }
}

As you can see, all that this method does is to store the indicator component into a global JS variable and then create a busy state listener that the framework will invoke as it starts and ends the blocking operation.

The Listener

The listener is where all of the work happens. here we first of all check to see if the requested indicator component is a dialog or not, and then based on the busy state we do the right thing to show or hide.  In the case of the dialog this is a simple matter of calling the show() and hide() methods on the component.  In the case of any other component we achieve the effect by setting the CSS display style.  Note that in order to do this, we need to get a handle to the real DOM element that represents this component.  This is what the call to AdfAgent.AGENT.getElementById() call is doing:

function handleBusyStateCallback(event){
        
  if(loadingIndicatorComponent != null){
    // Check is this is a dialog as 
    // this needs different treatment
    var isDialog =
        (loadingIndicatorComponent.getComponentType() == "oracle.adf.RichPopup");

  if (event.isBusy()){     if (isDialog){       loadingIndicatorComponent.show();        }       else {       loadingIndicatorComponentId = AdfAgent.AGENT.getElementById(loadingIndicatorComponent.getClientId());         loadingIndicatorComponentId.style.display = "inherit";       }     }     else {     if (isDialog){       loadingIndicatorComponent.hide();       }       else {       loadingIndicatorComponentId = AdfAgent.AGENT.getElementById(loadingIndicatorComponent.getClientId());         loadingIndicatorComponentId.style.display = "none";       }

      AdfPage.PAGE.removeBusyStateListener(loadingIndicatorComponent, handleBusyState);     }  }    }

Wiring it up 

In order to call the script here we need to have a reference to it in the page. The normal place would be using an <af:resource> tag in the metaContainer facet of the document:

<af:document>
  ...
  <f:facet name="metaContainer">
    <af:resource type="javascript" source="/js/longRunningNotification.js"/>
  </f:facet>
</af:document>

Then the triggering component itself  uses a client listener to wire up the action and a clientAttribute to pass in the value of the required indicator component:

<af:commandButton text="Start Fong File Upload with Inline Message"
                  id="cb_upload_i"
                  partialSubmit="true">
  <af:clientAttribute name="loadingIndicatorId"
                      value="#{requestScope.uploadBB.loadingIndicatorId}"/>
  <af:clientListener method="showWhenBusy"
                     type="action"/>
</af:commandButton> 

Notice that in this case, rather than passing a hardcoded ID through to the clientAttribute I'm calling a  backing bean getter (#{requestScope.uploadBB.loadingIndicatorId}). The idea of this is that we can ask the component itself for it's correct ID, reducing the margin for error. I have to give Frank the credit for this, it was his idea as we discussed this issue.

Set Up the Indicator Component 

For this to work, the component that I'm using as the indicator  needs a few attributes set:

  1. rendered and visible must be true
  2. clientComponent must be true 
  3. bindings must be set to associate the component with a reference in a backing bean
  4. If the component is not a dialog then we need to set it's initial display state to none so it will not be visible. This is done with inlineStyle.   

Here's a sample of a panelBox that we might use as the "busy" indicator: 

<af:panelBox text="Uploading your large file...." id="pb1"
             clientComponent="true"
             binding="#{uploadBB.loadingBox}"
             inlineStyle="display:none;">
 <af:panelGroupLayout id="pgl5" layout="horizontal">
    <af:spacer width="60" height="10" id="s1"/>
    <af:image source="/images/working.gif" id="i1"/>
 </af:panelGroupLayout>
</af:panelBox

Finally Wiring the ID 

The only missing bit now is how we get the ID of the component above into the clientAttribute that the JavaScript method is pulling. Recall that this was bound to the expression "uploadBB.loadingIndicatorId". So here's the implementation of that getter that lives in the page backing bean:

public String getLoadingIndicatorId() {
  return getLoadingBox().getClientId(FacesContext.getCurrentInstance());
} 

I think that this nicely extends Frank's technique to open up a whole new range of UI possibilities when you're doing something that is going to take some time and want to keep the user entertained. 

Friday Jan 20, 2012

Break Group Formatting in a Table - Part 1

From time to time, interesting questions come my way from the community which warrent a little investigation time and in turn capturing of that information in a posting like this. Just such a problem arrived in my in-tray yesterday. Simply put, how do you create break group style formatting in an ADF table? By break group formatting I mean the supression of repeating values in a column, something which is a very common reporting requirement although not so common in a transactional UI. However, if the data is read-only and pre-sorted, why not?

To make it clear here's an example:

Example of Break Group formatting in a table

This is one of those problems that you can address in several ways. you could of course manipulate the data set, say for example, your View Object. if using ADF BC, to have a calculated column that you pre-process to contain the grouping text or not, however, this has some drawbacks. Firstly I dislike the idea of manipulating the data model for the sake of a UI vanity, secondly you have to find the right point in time to run through the transient attribute to populate it correctly, and finally if you filter the data in any way then you'll have to re-do that calculation. 

So is there a way you can do this in the JSF layer? Well yes, that's how I created the screenshot above. I'll reveal a couple of ways to do it, but first, let's get into what the logical process needs to be. Basically when we go to render say the department name we want to look at the previous value for the same attribute in the collection and if the value matches we supress display, otherwise we print it. So that's simple enough but let's review the tools we have to play with. Within the node-stamp of a table we have access to some contextual information in the form of var (the row information we're processing) and varstatus (which gives access to the row which can be very useful in some circumstances). We could perhaps see, that if our data model backing the table was some sort of indexed List, we could use a value of (<VarStatus>.count-1) to reach into the list and get the previous value. Indeed that might be an approach worth having a play with now I come to think about it, however, lets think about another way we could look at the previously stamped value. Logically all we have to do is to store the last value we printed and not print again if this row has the same value. If this row has a different value then we do print, and also we replace the stored value with the new one, and so the process continues. Great, so how do we do it?  

Everybody likes to cheat if they (think that they) can get away with it!

In this approach I've resorted to one of the great JSF "cheats". In out of the box JSF you can't execute arbitrary method calls as part of page rendering (however, more on that in the next article where I will discuss creating a custom EL function to do this as well), but there is one way in which you can make a call that passes a value. This is when you access a Map in an expression. e.g. #{mybean['foo']}. Behind the scenes this is calling the get() method on the Map interface and passing the supplied key - "foo" in this case, as the argument. So you see, we do have a technique for calling functions in-line. This technique has been covered before by me and by others, for example my good buddy Lucas Jellema.

So the basic approach here involves creating a managed bean which supplies a fake Map, where the get() method actually proxies for the check we want to make. Here's an extract from that class. I've not shown all of the code - you do have to implement the compelete Map interface in terms of creating the empty methods, but fear not, JDeveloper's Source -> Implement Interface option will do all the work for you. The important bit is the get() method shown in the extract here:

public class UIManager {
    //Holds the DepartmentId of the last one we rendered
    Integer _breakGroupLastKey = new Integer(-1);
    
    //The fake Map
    Map _breakGroupMap = new BreakGroupFakeMap();

    //Getter for the face map. No setter required
    public Map getBreakGroupMap() {
        return _breakGroupMap;
    }

    //Implementation of the fake map as an inner class
    public class BreakGroupFakeMap implements Map{

        @Override
        public Object get(Object key) {
            Boolean retValue = false;
            if (key instanceOf Integer){
              if (((Integer)key).equals(_breakGroupLastKey)){
                  retValue = true;
              }
              else {
                  retValue = false;
                  _breakGroupLastKey = (Integer)key;
              }
            }
            return retValue;
        }
...

And then we wire that into the column that we want to break on, in this case I'm displaying the DepartmentName and using DepartmentId as the key, so DepartmentId is also defined in the underlying binding. You can see here how the current DepartmentId is passed in using the Map style expression

<af:column headerText="Department" id="c1">
  <af:outputText value="#{row.DepartmentName}" 
                 visible="#{!uiManager.breakGroupMap[row.DepartmentId]}" 
                 id="ot1"/>                            
</af:column>       

You will of course need to think about the scope in which this bean lives, ViewScope is probably the most suitable. In the next posting I'll look at the slightly more advanced option of creating an custom EL function.  

See also Part 2 in this series

About

Hawaii, Yes! Duncan has been around Oracle technology way too long but occasionally has interesting things to say. He works in the Development Tools Division at Oracle, but you guessed that right? In his spare time he contributes to the Hudson CI Server Project at Eclipse
Follow DuncanMills on Twitter

Note that comments on this blog are moderated so (1) There may be a delay before it gets published (2) I reserve the right to ignore silly questions and comment spam is not tolerated - it gets deleted so don't even bother, we all have better things to do with our lives.
However, don't be put off, I want to hear what you have to say!

Search

Archives
« April 2014
MonTueWedThuFriSatSun
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
       
Today