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);
}
});

12 comments:

  1. Hi Inge,

    I'm using your original ZoneUpdater implementation, here is my code:



    public void onSelectChange(String value) {
    System.out.println("AAA");
    }

    The onSelectChange method gets called successfully, but the problem I'm seeing is that the value (which i'm expecting to be the value of the Select is always null).

    I understand Tapestry has implemented something similar to this in the current SNAPSHOT version with the zone parameter on select, but unfortunately our company policy is to not use SNAPSHOTs.

    Looking to get this fixed in the next day or so, would appreciate your input.

    Thanks,
    Ben.

    ReplyDelete
  2. It appears the template was lost, here it is again without the square brackets!

    t:select t:id="selectAddress" t:value="selectedAddressIndex" t:model="addressModel" blankOption="never" t:mixins="zoneUpdater" zone="addressZone" t:event="selectChange" t:clientEvent="change"/

    ReplyDelete
  3. If we use zoneUpdater in a linksubmit component,
    what should be the "t:clientEvent", what should be the event handling method signagure in the jave code?

    For example
    MySubmit

    is "submit" the correct event?
    What is the method signature for "myEvent"?

    thanks,

    ReplyDelete
  4. Sorry the sample code line did not show correctly,
    here it is:

    <t:linksubmit t:id="myId" t:mixins="x/zoneUpdater" zone="myZone" zoneUpdater.event="myEvent" clientEvent="submit">MySubmit</t:linksubmit>

    ReplyDelete
  5. When I use this mixin to my select model, the ajax submit clears all the textfields in my form. To be more specific,the zone encloses the form and the form encloses several textfields and a select with the mixin attached to it. If I type any text into the textfield, it will disappear whenever I make a selection from the drop down menu. Any ideas?

    Thanks

    ReplyDelete
  6. Thank for this ZoneUpdater, it helped me a lot !

    @Alexis

    I encountered the same issue. The problem is that Tapestry copy the textfield value in the Java variable only when the form is submitted.

    Then, when your zone is updated, the value which is displayed in the textfields is the value contained in Java variables.

    There is one easy way to avoid this problem.
    On each component (textfield, select, etc.) add a ZoneUpdater mixin which will only update Java value.

    Example for a textfield whose value is "text1":
    void onText1Updated() {
    text1 = request.getParameter("param");
    }

    The variable takes the new value, and won't be cleared next time you will refresh the zone.

    I hope this is clear!

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. This breaks in Tapestry 5.2, and I'm in the process of trying to fix it. Any chance you can post an update?

    ReplyDelete
  9. @desikage
    The jumpstart project uses an updated version, check it out at http://jumpstart.doublenegative.com.au/jumpstart/examples/javascript/ajaxonevent

    ReplyDelete
  10. Thanks for this great mixin!

    But there seems to be an UTF-8 encoding bug. Using this version the mixin replaced all special (non-ASCII) characters with "." or "$".
    Regarding to this [http://ecmanaut.blogspot.ch/2006/07/encoding-decoding-utf8-in-javascript.html] I had only to replace one line in ZoneUpdater.js:
    updatedUrl = addParameter('param', param, updatedUrl);
    to
    updatedUrl = addRequestParameter('param', unescape(encodeURIComponent(param)), updatedUrl);

    It would be nice if you could update your blog post (and the jumpstart -- if this is possible).

    Regards,
    René

    ReplyDelete
  11. Hi again, I've had more problems with this mixin. Since the content of the form is sent via GET parameter, there are several difficulties. For example, if you write a '+', it gets replaced with a space ' ', because the '+' (and many other characters, I guess) are not encoded for the url. And also, the maximal length of the input is limited to approximately 1000 tokens (depends on the browser).

    I don't use this mixin anymore. Instead, I realized the same behaviour with a non-tapestry form, a zone that includes a tapestry-form and some js around. When the user writes text in the non-tapestry form, js copies it to the tapestry-form and clicks on the submit button. The form is ajaxly sent via POST and can be handled by the server in a normal onSuccessFromMyDataSendForm() {} method.
    That's probably not as elegant as a mixin would be, but it works for me. And, the most important thing, it all works with common tapestry tools (t:form, t:actionlink, etc) and it sends the data using POST.

    Regards,
    René

    ReplyDelete
  12. I think, needed to change line from
    if (this.element.value) {
    to
    if (this.element.val()) {

    ReplyDelete