This is a historical document in which Tapestry's Howard Lewis Ship describes the motivations and plan for significantly changing Tapestry's client-side functionality starting in Tapestry 5.4. This plan closely matches the actual results delivered in Tapestry 5.4, but this document is mostly kept for historical reference.
Tapestry 5 has had a interesting mix of characteristics.
On the one hand, it has had a large number of features that work, and work well, right out of the box, with no special configuration or setup. This includes client-side validation, dynamic content updates, simple animations, progressive enhancement, and other standard Ajax and DHTML use cases.
In addition, Tapestry has evolved, from Tapestry 5.0 through 5.3, into a quite capable provisioning framework:
- Libraries, stacks, and other resources are exposed to the browser with a versioned URL and far-future expires header, to support aggressive client-caching
- Compressible resources will be automatically GZip compressed if the client supports it
Common examples of the challenges imposed by Tapestry include implementing a Confirm mixin, customizing behavior when a Zone component is dynamically updated, or any number of issues related to Forms, form elements, and Ajax updates.
Dependence on Prototype/Scriptaculous
Tapestry made an early choice to embrace Prototype and Scriptaculous at a time when this made sense, circa 2006-2007.
The goal was to have Tapestry provide a client-side API, the
Tapestry namespace, that in turn would delegate complex behaviors (including DOM element selection, event management, and XmlHttpRequest processing) to a foundational framework. The goal was to isolate all the direct dependencies on Prototype in such a way that it would be possible, in the future, to swap out for a different foundational framework, such as jQuery or ExtJS. Unfortunately, expediency has proven to make this goal even less reachable!
Lack of Documentation
There has not, to date, been an adequate documentation of the
Tapestry namespaces, beyond the code itself.
Lack of Module Structure
Tapestry.Initializers) namespace, and the mechanics of full-page and partial-page renders (described more fully below).
Many users are perplexed by how Tapestry performs initialization. In typical web page construction, the developer would create a
<script> block at the bottom of the page, and do initializations there. In Tapestry, it can be much more complex:
- The initialization functions must be monkey patched into the T5.initializers (or older Tapestry.Initializers) namespace.
- The affected element must have a unique id attribute, used to coordinate the initialization in the client web browser. (Tapestry assists with unique id allocation, but it would be much better if unique ids were not necessary.)
This often feels like overkill, but it is necessary for a number of desirable characteristics:
- Initialization code occurs in a single Tapestry-generated
<script>block at the end of the page (just before the
- There is limited support for structuring the order of initialization
- The mechanism works transparently in both full-page render requests (traditional) and partial-page render requests (Ajax)
Despite this, the Tapestry approach can feel very "heavy". In a bespoke page, initialization that may affect many elements of the page often takes the form of a single event handler, attached to the
<body> element, that catches events that bubble up from much lower in the DOM. The single handler function identifies the applicable elements using CSS selectors, including those that are based on HTML5 data- attributes. Additional data- attributes will define additional behavior ... for example, a URL for a triggered request. This is "light" because:
- There's a single event handler function (rather than a unique handler function instance per element)
- The event handler may be anonymous (there's no name, or possibility of collision)
- Elements are identified by DOM structure and CSS rather than their unique id (the element will often not have an id attribute)
- Additional necessary configuration is directly attached to the element, rather than split
- As the page is dynamically updated, there is no extra "bookkeeping" for added or removed elements; new elements inserted into the DOM dynamically are recognized as easily as those that were present on the initial render
By contrast, Tapestry is "heavy":
- The initialization function must have a unique name
- The element must have a unique id, to it can be located by the initialization function
- The event handlers are attached directly to the element
- Duplicated elements will have duplicated event handlers
- Additional behavior is specified as a JSON object passed to the initialization function
- Injecting new elements into the DOM requires invoking initialization functions to wire up the necessary event handlers
The goals for Tapestry 5.4 are:
- Break the dependency on Prototype and allow Tapestry to be used with any client-side "foundation" framework, seamlessly: minimally, this should include jQuery
- Optimize for fast page loads
- Backwards compatibility to the Tapestry 5.3 approach until at least 5.5 or 5.6
- Simplify Tapestry's client-side behavior, but make it easier to hook into, extend, and override
Slow Page Load and Initialization
Request.isXHR() method), the server-side event handler would return a response that can not be handled in a traditional request, and the user would see the Tapestry exception report page.
It is not clear how this same functionality will be supported in Tapestry 5.4 as the asynchronous module loading makes it difficult to know when all modules have been loaded and all initialization functions have been invoked.
Mapping Modules to Assets
On the server side, Tapestry will map the path to a classpath asset.
There must be provisions for the following options:
- A module may be overridden (for instance, to work around a bug), in which case a specific asset may be used for the module, rather than the default
- A module's content may be aggregated with other related modules (much like a Tapestry 5.3 stack), especially in production. (A request for any module should provide the aggregated set of modules; RequireJS will not need to send additional requests for the other modules.)
- Module content (aggregated or not) should be minimized
In addition, it may be reasonable to have Tapestry automatically (or via some configuration) wrap CommonJS modules as AMD modules. (Traditionally, Tapestry has configured this kind of behavior via service contributions, but there is ample evidence that this could be done using external configuration, perhaps using a JSON file in the module package, to control aggregation, wrapping, and other aspects the process. This would be more agile, as it would not require restarts when the configuration changes.)
Modules will be stored on the classpath, in a
modulejs package below each library's root package. Modules within that package are referenced by their name relative to the package. (A rarely used feature of Tapestry is that a component library name may be mapped to multiple packages; resolving a module name may require a search among the packages. There is the expectation that the developer will ensure that there are no duplications that would lead to ambiguities.)
Under this system, module
core/pubsub would be the file
pubsub.js in the package
org.apache.tapestry5.corelib.modulejs, since Tapestry's component library 'core' is mapped to package
Certain key modules, such as Underscore may be mapped at the root level, as they are used so often.
- require one or more modules
- require a module (that exports a single function) and invoke the function, passing zero or more values. (Values passed to module functions may be limited to String and JSONObject.)
- require a module and a function name and invoke named function exported by the module, passing zero or more values
The intent here is to support shifting of client-side behavior from the 5.3 style, an approach that involved monkey-patching functions onto
T5.initializers, and move the same logic into modules, preferably with simpler parameters. It is also expected that there will be greater use of
data- prefixed HTML5 attributes in place of separate configuration, as outlined above.
Tapestry.ZoneManager class to enable new behavior when a Zone element is updated, relying on a PubSub message to learn when the Zone was updated, and perform the desired updates or animations there.
Expose Global Message Catalog to Client
Tapestry currently maintains two global message catalogs; a global server-side catalog (usually named
and a client-side catalog. (app.properties provides
application-specific messages, and overrides of other messages provided
by Tapestry and other third-party libraries. The global message catalog
is actually a composite of all of these sources.) The client-side catalog is smaller, more limited, and less extensible.
Allowing the client application to have full access to the entire message catalog would make maintaining the catalog simpler, and make it easier to keep client-side and server-side messages consistent.
For security purposes, it should be possible to exclude some keys from the message catalog exposed to the client. In addition, keys whose values include
String.format() productions (for example,
%s) should be excluded, as those productions are meaningless in the client.
Partial Page Update Response
A key part of Tapestry's dynamic behavior has been the partial page update; a specific JSON reply to Ajax requests (usually initiated via a Zone component).
The format and behavior of the response has evolved from release to release.
When an Ajax request is processed by the server, the response should handle any of a number of outcomes:
- Redirect the entire page to a new URL (on the server, or elsewhere)
- A server-side error to be presented to the user. (This was greatly enhanced in 5.3 to present the full exception report in a pop-up iframe.)
- Update the content of an implicit (originating) element; typically the element for the Zone that triggered the request
- Update the content of any number of other elements (identified by their client-side id)
- Inject new CSS links into the page
- Peform initializations (using
T5.initializers) ... but only after all content updates have occurred
For Tapestry 5.4, a number of changes are planned:
- Tapestry 5.3 style initializations will be a specific application of 5.4 style module requirement and invocation
- IMMEDIATE may occur before DOM changes
- The response will be embeddable inside other JSONObject responses.
tapestry top-level key. An available function will be provided that takes an arbitrary JSONObject, extracts the
tapestry key and handles it, then invokes a provided callback before the module requirement and invocation step. The intent is for requests that perform purely data oriented operations, the server-side can not only provide a response, but can piggy back client-side updates in the response.
Maintaining Backwards Compatibility
Backwards compatibility is the greatest challenge here; ideally, applications (and third party libraries) that were written for Tapestry 5.3 will continue to operate unchanged in Tapestry 5.4.
At the same time, much of what Tapestry 5.3 does on the client and server should be deprecated (and hopefully, simplified).
Compatibility mode will be initially enabled, via a symbol value.
Tapestry namespaces available in Tapestry 5.3.
The implementations of these namespaces will be reconstructed in terms of the new module system. The loading of the compatibility layer will occur during full page render.
In Tapestry 5.3 and earlier, Tapestry automatically includes a default CSS link on all pages. This CSS file acts as a partial CSS reset (normalizing the look of the application across common browsers), and provides a large number of CSS rules that many Tapestry components expect to be present. The CSS rules are all given a "t-" (for Tapestry) prefix.
For Tapestry 5.4, this default CSS link will be changed to be the default Twitter Bootstrap. This will not only refresh the Tapestry look and feel, but will provide a better structure for customizing the application's look and feel.
As with today, it will be possible to override the location of this CSS file (for example, to use a newer version of Bootstrap than is packaged in the application, or an application-specific customized version).
This will entail some changes to some components, to make use of reasonable or equivalent Bootstrap CSS classes, rather than the Tapestry 5.3 classes.
Twitter Bootstrap also includes a number of jQuery-based plugins; these will be exposed in the module system.
Content Delivery Network Integration
Determining what assets are available is somewhat problematic as Tapestry mixes server-side only resources (.class files, .tml files, etc.) freely with assets that might be exposed to the browser. (This should never have been the case, but that's hindsight.) Some of those server-side resource may expose details, such as other server hosts and potentially user names and passwords, that should never be exposed to the client.
In addition, a "walk" of the classpath to locate potential exportable assets can be quite expensive (though not considerably more so than what Tapestry already does at startup to identify page and component classes).
To be determined. ExtJS inlcudes it own system for dynamically loading ExtJS modules, as well as expressing dependencies between them. Its capabilities overlap what RequireJS offers. It would be nice if, in an ExtJS application, the ExtJS loader could be used instead of RequireJS, or at least, ensure that they do not interfere with each other.
This is a big undertaking; this document is not a contract, and is certainly not complete, but is only starting point for discussions about what will be forthcoming in Tapestry 5.4.