Friday, May 21, 2010

Mixin to allow any element to submit a form and trigger an event

I modified the builtin LinkSubmit component to be able to use the same thing on any element.

I changed it to a mixin and did some other minor changes.


@MixinAfter
@IncludeJavaScriptLibrary("AnySubmit.js")
public class AnySubmit {

/**
* The name of the event that will be triggered if this component is the cause of the form submission. The default
* is "selected".
*/
@Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
private String event = EventConstants.SELECTED;

@Parameter(defaultPrefix = BindingConstants.LITERAL)
private String clientEvent = "change";

/**
* The value that will be made available to event handler method of this component when the form is
* submitted.
*/
@Parameter
private Object context;

/**
* If true (the default), then any notification sent by the component will be deferred until the end of the form
* submission (this is usually desirable).
*/
@Parameter
private boolean defer = true;

@InjectContainer
private ClientElement container;

@Inject
private ComponentResources resources;

@Inject
private RenderSupport renderSupport;

@Environmental
private FormSupport formSupport;

@Environmental
private Heartbeat heartbeat;

@Inject
private Request request;

@SuppressWarnings("serial")
private static class ProcessSubmission implements ComponentAction {

private final String clientId;

public ProcessSubmission(String clientId) {
this.clientId = clientId;
}

public void execute(AnySubmit component) {
component.processSubmission(clientId);
}
}

private void processSubmission(String clientId) {

String hiddenFieldName = clientId + ":hidden";

if (request.getParameter(hiddenFieldName) != null) {
final Object[] contextToPublish = getContext(request, clientId);
Runnable notification = new Runnable() {

public void run() {
resources.triggerEvent(event, contextToPublish, null);
}
};

if (defer)
formSupport.defer(notification);
else
heartbeat.defer(notification);
}
}

private Object[] getContext(Request request, String clientId) {
String contextFieldName = clientId + ":context";
String context = request.getParameter(contextFieldName);
if (context != null) {
return new Object[] { context };
}
String value = request.getParameter(clientId);
if (value != null) {
return new Object[] { value };
}
return null;
}

void beginRender() {
formSupport.store(this, new ProcessSubmission(container.getClientId()));
}

void afterRender(MarkupWriter writer) {
renderSupport.addInit("anySubmit", formSupport.getClientId(), container.getClientId(), clientEvent);
if (context != null) {
writer.element("input", "type", "hidden", "value", context, "name", container.getClientId() + ":context");
writer.end();
}
}
}






Tapestry.AnySubmit = Class.create({

initialize: function(formId, clientId, event)
{
this.form = $(formId);
this.element = $(clientId);

this.element.observe(event, this.onEvent.bindAsEventListener(this));
},

createHidden : function()
{
var hidden = new Element("input", { "type":"hidden",
"name": this.element.id + ":hidden",
"value": this.element.id});

this.element.insert({after:hidden});
},

onEvent : function(event)
{
Event.stop(event);

var onsubmit = this.form.onsubmit;

if (onsubmit == undefined || onsubmit.call(window.document, event))
{
this.createHidden();
this.form.submit();
}

return false;
}
});

Tapestry.Initializer.anySubmit = function(formId, clientId, event)
{
new Tapestry.AnySubmit(formId, clientId, event);
}

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