Monday, March 15, 2010

Check all mixin

This is a VERY simple mixin that turns a regular checkbox into a controller checkbox. It's a nice example of how easy it is to create these elegant little mixins and components in Tapestry 5!

Example: <input type="checkkbox" t:type="any" t:mixins="checkall" t:controlledClass="element-selected"/>

CheckAll.java

/**
* Mixin for checkboxes that should control the checked state of a group of other checkboxes. All the controlled checkboxes need to
* have the CSS class specified in the controlledClass parameter.
*
* @author Inge
*
*/
public class CheckAll {

/**
* The controller checkbox that this mixin applies to.
*/
@InjectContainer
private ClientElement container;

@Environmental
private RenderSupport renderSupport;

/**
* The CSS class that all controlled checkboxes must have to be controlled by the master checkbox.
*/
@Parameter(required = true, defaultPrefix = BindingConstants.LITERAL)
private String controlledClass;

void afterRender() {
renderSupport.addScript("new CheckAll('%s', '%s')", container.getClientId(), controlledClass);
}
}


CheckAll.js

var CheckAll = Class.create( {
initialize : function(controllerId, controlledClass) {
this.controller = $(controllerId);
this.controlledClass = controlledClass;
this.controller.observe('change', this.toggle.bindAsEventListener(this));
},
toggle: function() {
var checked = this.controller.checked;
var controlledCheckboxes = $$('input.' + this.controlledClass);
controlledCheckboxes.each(function(checkbox) {
checkbox.checked = checked;
});
}
});

Friday, March 5, 2010

New and better (?) ZoneUpdater

I recently sat down and did some evaluation of the ZoneUpdater code. I found that it is a bit hard to configure and debug, so I decided to do a little refactoring to trade some flexibility for readability and usability.

I also wanted to add a missing feature, the ability to update a zone simply by calling a javascript function. This is a nice way of "object orienting" different features/sections on a page, by giving different sections their own client API that they can communicate with.

Most important changes:
- Removed support for specifying another listening element than the container
- Cleaner code
- Request parameter instead of encoding parameter into link context.
- Generates a javascript variable to easy zone update through javascript.

I also wanted to add an internal listener that triggers the actual event with context, so the page/component listener doesn't have to inject the Request and do request.getParameter("param"). Didn't succeed so far.

Anyway, here's the modified code. No code formatting this time, but it should be short enough to be readable. Enjoy :)

ZoneUpdater.java


@IncludeJavaScriptLibrary( { "ZoneUpdater.js" })
public class ZoneUpdater {

@Inject
private ComponentResources resources;

@Environmental
private RenderSupport renderSupport;

/**
* The event to listen for on the client. If not specified, zone update can only be triggered manually through calling updateZone on the JS object.
*/
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private String clientEvent;

/**
* The event to listen for in your component class
*/
@Parameter(defaultPrefix = BindingConstants.LITERAL, required = true)
private String event;

@Parameter(defaultPrefix = BindingConstants.LITERAL, value = "default")
private String prefix;

/**
* The element we attach ourselves to
*/
@InjectContainer
private ClientElement element;

@Parameter
private Object[] context;

/**
* The zone to be updated by us.
*/
@Parameter(defaultPrefix = BindingConstants.LITERAL, required = true)
private String zone;

void afterRender() {
String url = resources.createEventLink(event, context).toAbsoluteURI();
String elementId = element.getClientId();
JSONObject spec = new JSONObject();
spec.put("url", url);
spec.put("elementId", elementId);
spec.put("event", clientEvent);
spec.put("zone", zone);

renderSupport.addScript("%sZoneUpdater = new ZoneUpdater(%s)", prefix, spec.toString());
}
}





ZoneUpdater.js


var ZoneUpdater = Class.create({
initialize: function(spec) {
this.element = $(spec.elementId);
this.url = spec.url;
$T(this.element).zoneId = spec.zone;
if (spec.event) {
this.event = spec.event;
this.element.observe(this.event, this.updateZone.bindAsEventListener(this));
}
},
updateZone: function() {
var zoneManager = Tapestry.findZoneManager(this.element);
if ( !zoneManager ) return;

var updatedUrl = this.url;
if (this.element.value) {
var param = this.element.value;
if (param) {
updatedUrl = addParameter('param', param, updatedUrl); // You need to provide your own function for this...
}
}
zoneManager.updateFromURL(updatedUrl);
}
});

Tuesday, October 20, 2009

Link with confirmation

 

This is  a component that adds confirmation to a regular EventLink. It does this by using the TapestryExt.js library introduced in my previous post to trigger a link after intercepting and cancelling it first. It works both with and without zones.

It is used excactly like an EventLink, with an additional “message” parameter, to display in confirmation box.

For now, I only implemented a standard javascript confirm box. But it shouldn’t be too hard extending this with more fancy interaction with DHTML.

I’m not completely comfortable with the techniques used here. Some of it looks a lot like a hack in my opinion, especially the part about intercepting/cancelling a zone listener and re-adding it afterwards using copy-paste from tapestry.js.

ConfirmLink.java:

/**
 * Displays a javascript confirmation box before actually triggering the link.
 * 
 * @author Inge
 *
 */
@IncludeJavaScriptLibrary({"TapestryExt.js, "ConfirmLink.js"})
public class ConfirmLink implements ClientElement {    
  @Environmental
  private RenderSupport renderSupport;  
  
  @Parameter("literal:Do you really want to delete?")
  private String message;
  @Component(publishParameters="event,zone,context", inheritInformalParameters=true)
  private EventLink confirmLink;
  
  void afterRender() {
    String url = confirmLink.getLink().toRedirectURI();
    renderSupport.addScript("new ConfirmLink('%s', '%s', '%s')", confirmLink.getClientId(), url, message);
  }
  
  public String getClientId() {
    return confirmLink.getClientId();
  }
}

 



ConfirmLink.js



var ConfirmLink = Class.create({
  initialize: function(element, url, message) {
    this.element = $(element);
    this.url = url;
    this.message = message;
    this.initListeners = this.initListeners.bind(this);
    this.initListeners.defer();
    // Makes it run after Tapestry:init to be able to delete
    // zone listener added by Tapestry and start listening for
    // our modded version instead.    
  },
  onclick: function(event) {
    Event.stop(event);
    if (confirm(this.message)) {
      TapestryExt.triggerLink(this.element, this.url);
    }
  },
  initListeners: function() {
    this.element.stopObserving('click'); // Remove zone update, will be re-added later by TapestryExt.js.
    this.element.observe('click', this.onclick.bindAsEventListener(this));
  }
});

Friday, October 16, 2009

Missing javascript

I think there are some important things missing from Tapestry.js to release the full potential of Tapetsry 5 and AJAX. I have started working on a javascript class, TapestryExt, that extends the functionality of Tapestry.js.

My wish is to see all of this and more built in to tapestry.

 

/**
 * Commonly needed functions that are absent in the default Tapestry js lib.
 */
var TapestryExt = {
  /**
   * If the specified form listens for the Tapestry.FORM_PROCESS_SUBMIT_EVENT event
   * (all forms with a zone specified do), it will AJAX-submit and update its zone after.
   */
  submitZoneForm : function(element) {    
    element = $(element)
    if (!element) {
      Tapestry.error('Could not find form to trigger AJAX submit on');
      return;
    }
    element.fire(Tapestry.FORM_PROCESS_SUBMIT_EVENT);
  },
  /**
   * Trigger any link, zone or not. 
   * 
   * If no url is provided, the href attribute of element will be used.
   */
  triggerLink : function(element, url) {
    element = $(element);
    if (!url) {
      url = element.href;
    }    
    if (!url) {
      Tapestry.error('Unable to find url to trigger on element ' + element.id);
    }
    var zoneObject = Tapestry.findZoneManager(element);
    if (zoneObject) {    
      zoneObject.updateFromURL(url);
    }
    else {
      // Regular link
      // Note: this will skip all event observers on this link!
      window.location.href = url;
    }
  }  
}

Thursday, May 21, 2009

Simple OnEvent mixin

This mixin is heavily based on Chenillekit OnEvent. I found that CK’s mixin had a different and too specific focus to fit my needs. I wanted to write an event mixin that does nothing but trigger an event. This is what I came up with, I believe the result is quite easy to understand, and should work as a nice foundation for other similar components and mixins.

Usage:

<input type=”button” value=”Answer yes” t:type=”any” t:id=”yesButton” t:mixins=”onEvent” t:event=”click” callback=”updateStatus”/>

Object onClickFromYesButton() {

return “Positive”; // Arguments for javascript function “updateStatus”

}

OnEvent.java

@IncludeJavaScriptLibrary("OnEvent.js")
public class OnEvent {
  @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL)
  private String event;
  @Parameter
  private Object[] context;
  @Environmental
  private RenderSupport renderSupport;
  @InjectContainer
  private ClientElement container;
  @Parameter(defaultPrefix = BindingConstants.LITERAL)
  private String callback;
  @Inject
  private ComponentResources componentResources;
  void afterRender() {
    Link link = componentResources.createEventLink(event, context);
    String script = "new OnEvent('%s', '%s', '%s', '%s')";
    renderSupport.addScript(script, container.getClientId(), event, link.toRedirectURI(), callback);
  }
}


OnEvent.js



var OnEvent = Class.create({
    initialize: function(elementId, event, url, callback)
    {
        if (!$(elementId))
            throw(elementId + " doesn't exist!");
        this.element = $(elementId);
        this.callback = callback;
        this.url = url;
        this.element.observe(event, this.eventHandler.bindAsEventListener(this));
    },
    eventHandler: function(event)
    {
        new Ajax.Request(this.url, {
            method: 'get',
			onFailure: function(t)
            {
                alert('Error communication with the server: ' + t.responseText.stripTags());
            },
            onException: function(t, exception)
            {
                alert('Error communication with the server: ' + exception.message);
            },
            onSuccess: function(t)
            {
                if (this.callback)
				{
					var funcToEval = this.callback + "(" + t.responseText + ")";
					eval(funcToEval);
				}
			}.bind(this)
        });
    }
});

Friday, May 15, 2009

Update a zone on any client side event

 

Usage:

<input type=”text” t:type=”textField” value=”myValue” t:mixins=”zoneUpdater” zone=”searchResultZone” event=”performSearch” clientEvent=”onkeyup”/>

Then all you have to do is providing a listener method for the performSearch-event in your component/page class.

There is some room for improvement here in terms of more flexibility on event link context and other things. But it works.

There might be improvements in the Tapestry js library to make it easier to obtain and trigger zones. This code was developed with T 5.0.18.

 

ZoneUpdater.java

@IncludeJavaScriptLibrary("ZoneUpdater.js")
public class ZoneUpdater {
  public static final String PLACEHOLDER = "XXX";
  @Inject
  private ComponentResources resources;
  @Environmental
  private RenderSupport renderSupport;
  @Parameter(defaultPrefix = BindingConstants.LITERAL)
  private String clientEvent;
  @Parameter(defaultPrefix = BindingConstants.LITERAL, required = true)
  private String event;
  @InjectContainer
  private ClientElement element;
  @Parameter
  private Object[] context;
  @Parameter(defaultPrefix = BindingConstants.LITERAL)
  // To enable popups to fire events on this document, enter "document" here.
  private String listeningElement;
  @Parameter(defaultPrefix = BindingConstants.LITERAL, required = true)
  private String zone;
  protected Link createLink(Object[] context) {
    if (context == null) {
      context = new Object[1];
    }
    context = ArrayUtils.add(context, PLACEHOLDER); // To be replaced by javascript
    return resources.createEventLink(event, context);
  }
  void afterRender() {
    String link = createLink(context).toAbsoluteURI();
    String elementId = element.getClientId();
    if (clientEvent == null) {
      clientEvent = event;
    }
    if (listeningElement == null) {
      listeningElement = "$('" + elementId + "')";
    }
    renderSupport.addScript("new ZoneUpdater('%s', %s, '%s', '%s', '%s', '%s')", elementId, listeningElement, clientEvent, link, zone, PLACEHOLDER);
  }
}

 





ZoneUpdater.js





var ZoneUpdater = Class.create();
ZoneUpdater.prototype = {
	initialize: function(zoneElementId, listeningElement, event, link, zone, placeholder) {
		this.zoneElement = $(zoneElementId);
		this.event = event;
		this.link = link;
		this.placeholder = placeholder;		
		$T(this.zoneElement).zoneId = zone;			
		listeningElement.observe(this.event, this.updateZone.bindAsEventListener(this));
	},	
	updateZone: function(event) {
	    var zoneObject = Tapestry.findZoneManager(this.zoneElement);
	    if ( !zoneObject ) return;
	    var param;
	    if (this.zoneElement.value) {
	    	param = this.zoneElement.value;
	    }
	    if (!param) param = ' ';
	    param = this.encodeForUrl(param);
	    var updatedLink = this.link.gsub(this.placeholder, param);
	    zoneObject.updateFromURL(updatedLink);		
	},
	encodeForUrl: function(string) {
		/**
		 * See equanda.js for updated version of this
		 */
		string = string.replace(/\r\n/g,"\n");
	    var res = "";
	    for (var n = 0; n < string.length; n++)
	    {
	        var c = string.charCodeAt( n );
	        if ( '$' == string.charAt( n ) )
	        {
	            res += '$$';
	        }
	        else if ( this.inRange( c, "AZ" ) || this.inRange( c, "az" ) || this.inRange( c, "09" ) || this.inRange( c, ".." ) )
	        {
	            res += string.charAt( n )
	        }
	        else
	        {
	            var tmp = c.toString(16);
	            while ( tmp.length < 4 ) tmp = "0" + tmp;
	            res += '$' + tmp;
	        }
	    }
	    return res;
	},
	inRange: function(code, range) {
		return code >= range.charCodeAt( 0 ) &&  code <= range.charCodeAt( 1 );
	}
}

Wednesday, April 15, 2009

Open a page in a popup window

This mixin can be applied to any element that has an onclick event. It opens the specified page in a new window. There is room for more parameters here for styling the window, and possibly for using other events than onclick as a trigger.

Usage:

<input type=”button” value=”View shopping cart” t:mixins=”popupPageLink” page=”cart/view” context=”cartId”/>

Java:

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ClientElement;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.Link;
import org.apache.tapestry5.RenderSupport;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.IncludeJavaScriptLibrary;
import org.apache.tapestry5.annotations.InjectContainer;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.ioc.annotations.Inject;
@IncludeJavaScriptLibrary("PopupPageLink.js")
public class PopupPageLink {
  @Inject
  private ComponentResources resources;
  @Environmental
  private RenderSupport renderSupport;
  @InjectContainer
  private ClientElement container;
  @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL)
  private String page;
  
  @Parameter(defaultPrefix = BindingConstants.LITERAL, value="800")
  private String width;
  
  @Parameter(defaultPrefix = BindingConstants.LITERAL, value="600")
  private String height;
  @Parameter
  private Object[] context;
  void afterRender() {
    Link link = resources.createPageLink(page, true, context);
    renderSupport.addScript("new PopupPageLink('%s', '%s', %s, %s);", container.getClientId(), link, width, height);
  }
}


Javascript:



var PopupPageLink = Class.create();
PopupPageLink.prototype = {
	initialize: function(id, link, width, height) {
		this.element = $(id);
		this.link = link;
		this.width = width;
		this.height = height;
		Event.observe(this.element, 'click', this.onclick.bindAsEventListener(this));
	},
	onclick: function() {
		var	name = 'dialogWindow';
		var win = window.open(this.link,name,'width=' + this.width + ',height=' + this.height + ',resizable=yes,scrollbars=yes,menubar=no,screenX=0,screenY=0,left=0,top=0' );
	    win.focus();
	}
}