Javascript integration into ``z3c.form``
Project description
This package is going to provide javascript support/enhancements to
the z3c.form library.
Detailed Documentation
**********************
===========================
Form Javascript Integration
===========================
This package is designed to provide a Python API to common Javascript
features for forms written with the ``z3c.form*`` packages. While the
reference backend-implementation is for the JQuery library, any other
Javascript library can be hooked into the Python API.
The documents are ordered in the way they should be read:
- ``jsaction.txt`` [must read]
This document describes how JS scripts can be connected to events on a
any widget, inclduing buttons.
- ``jsvalidator.txt`` [must read]
This document demonstrates how "live" widget value validation can be
achieved.
- ``jsevent.txt`` [advanced users]
This documents describes the generalization that allows hooking up script to
events on any field.
- ``jqueryrenderer.txt`` [advanced users]
This document demonstrates all necessary backend renderer components
necessary to accomplish any of the features of this package.
=============================
Javascript Events for Buttons
=============================
In the ``z3c.form`` package, buttons are most commonly rendered as "submit"
input fields within a form, meaning that the form will always be
submitted. When working with Javascript, on the other hand, a click on the
button often simply executes a script. The ``jsaction`` module of this package
is designed to implement the latter kind.
>>> from z3c.formjs import jsaction
Javascript Buttons
------------------
Before we can write a form that uses Javascript buttons, we have to define
them first. One common way to define buttons in ``z3c.form`` is to write an
schema describing them; so let's do that now:
>>> import zope.interface
>>> class IButtons(zope.interface.Interface):
... hello = jsaction.JSButton(title=u'Hello World!')
... dblhello = jsaction.JSButton(title=u'Double Hello World!')
Instead of declaring ``z3c.form.button.Button`` fields, we are now using a
derived Javascript button field. While there is no difference initially, they
will later be rendered differently. (Basically, ``JSButton`` fields render as
button widgets.)
Widget Selector
---------------
Like for regular fields, the action of the buttons is defined using handlers,
in our case Javascript handler. Selectors are used to determine the DOM
element or elements for which a handler is registered. The widget selector
uses a widget to provide the selector API:
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> from z3c.form.browser import text
>>> msg = text.TextWidget(request)
>>> msg.id = 'form-msg'
>>> msg.name = 'msg'
>>> selector = jsaction.WidgetSelector(msg)
>>> selector
<WidgetSelector "form-msg">
Since the widget selector can determine the widget's id, it is also an id
selector (see ``jsevent.txt``):
>>> from z3c.formjs import interfaces
>>> interfaces.IIdSelector.providedBy(selector)
True
>>> selector.id
'form-msg'
This has the advantage that we can reuse the renderer of the id
selector.
Javascript Event Subscriptions
------------------------------
As discussed in ``jsevent.txt``, all the Javascript event subscriptions are
stored on the view in a special attribute called ``jsSubscriptions``. While
updating the form, one can simply add subscriptions to this registry. So let's
say we have the following handler:
>>> def showSelectedWidget(event, selector, request):
... return 'alert("%r");' %(selector.widget)
We now want to connect this handler to the ``msg`` widget to be executed when
the mouse is clicked within this element:
>>> import zope.interface
>>> from z3c.formjs import jsevent
>>> class Form(object):
... zope.interface.implements(interfaces.IHaveJSSubscriptions)
... jsSubscriptions = jsevent.JSSubscriptions()
...
... def update(self):
... self.jsSubscriptions.subscribe(
... jsevent.CLICK, selector, showSelectedWidget)
>>> form = Form()
>>> form.update()
After registering the subscription-related renderers,
>>> from z3c.formjs import testing
>>> testing.setupRenderers()
we can use the subscription rendering viewlet to check the subscription
output:
>>> viewlet = jsevent.JSSubscriptionsViewlet(None, request, form, None)
>>> viewlet.update()
>>> print viewlet.render()
<script type="text/javascript">
$(document).ready(function(){
$("#form-msg").bind("click", function(){alert("<TextWidget 'msg'>");});
})
</script>
Forms with Javascript Buttons
-----------------------------
The next step is create the form. Luckily we do not need any fields to render
a form. Also, instead of using usual ``z3c.form.button.handler()`` function,
we now have a special handler decorator that connects a button to a Javascript
event. The output of the handler itself is a string that is used as the
Javascript script that is executed.
>>> from z3c.form import button, form
>>> class Form(form.Form):
... buttons = button.Buttons(IButtons)
...
... @jsaction.handler(buttons['hello'])
... def showHelloWorldMessage(self, event, selector):
... return 'alert("%s");' % selector.widget.title
...
... @jsaction.handler(buttons['dblhello'], event=jsevent.DBLCLICK)
... def showDoubleHelloWorldMessage(self, event, selector):
... return 'alert("%s");' % selector.widget.title
The ``handler()`` decorator takes two arguments, the button (acting as the DOM
element selector) and the event to which to bind the action. By default the
event is ``jsevent.CLICK``.
And that is really everything that is required from a user's point of
view. Let us now see how those handler declarations are converted into actions
and Javascript subscriptions. First we need to initialize the form:
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> demoform = Form(None, request)
We also need to register an adapter to create an action from a button:
>>> from z3c.form.interfaces import IButtonAction
>>> zope.component.provideAdapter(
... jsaction.JSButtonAction, provides=IButtonAction)
Finally, for the Javascript subscriptions to be registered, we need an event
listener that reacts to "after widget/action update" events:
>>> zope.component.provideHandler(jsaction.createSubscriptionsForWidget)
Action managers are instantiated using the form, request, and
context/content. A button-action-manager implementation is avaialble in the
``z3c.form.button`` package:
>>> actions = button.ButtonActions(demoform, request, None)
>>> actions.update()
Once the action manager is updated, the buttons should be available as
actions:
>>> actions.keys()
['hello', 'dblhello']
>>> actions['hello']
<JSButtonAction 'form.buttons.hello' u'Hello World!'>
Since special Javascript handlers were registered for those buttons, creating
and updating the actions has also caused the form to become an
``IHaveJSSubscriptions`` view:
>>> from z3c.formjs import interfaces
>>> interfaces.IHaveJSSubscriptions.providedBy(demoform)
True
>>> demoform.jsSubscriptions
<z3c.formjs.jsevent.JSSubscriptions object at ...>
The interesting part about button subscriptions is the selector.
>>> selector = list(demoform.jsSubscriptions)[0].selector
>>> selector
<WidgetSelector "form-buttons-hello">
As you can see, the system automatically created a widget selector:
>>> selector.id
'form-buttons-hello'
>>> selector.widget
<JSButtonAction 'form.buttons.hello' u'Hello World!'>
With the declarations in place, we can now go on.
Rendering the Form
------------------
Let's now see what we need to do to make the form render correctly and
completely.
>>> demoform = Form(None, request)
First we need some of the standard ``z3c.form`` registrations:
>>> from z3c.form import field, button
>>> zope.component.provideAdapter(field.FieldWidgets)
>>> zope.component.provideAdapter(button.ButtonActions)
Next we need to register the template for our button actions:
>>> from zope.pagetemplate.interfaces import IPageTemplate
>>> from z3c.form import widget
>>> from z3c.form.interfaces import IButtonWidget, INPUT_MODE
>>> from z3c.form.testing import getPath
>>> zope.component.provideAdapter(
... widget.WidgetTemplateFactory(getPath('button_input.pt'), 'text/html'),
... (None, None, None, None, IButtonWidget),
... IPageTemplate, name=INPUT_MODE)
We also need to setup a Javascript viewlet manager and register the
subscription viewlet for it, so that the subscriptions actually appear in the
HTML page. (This is a bit tedious to do using the Python API, but using ZCML
this is much simpler.)
* Hook up the "provider" TALES expression type:
>>> from zope.app.pagetemplate.engine import TrustedEngine
>>> from zope.contentprovider import tales
>>> TrustedEngine.registerType('provider', tales.TALESProviderExpression)
* Create a viewlet manager that does not require security to be setup:
>>> from zope.viewlet import manager
>>> class JSViewletManager(manager.ViewletManagerBase):
... def filter(self, viewlets):
... return viewlets
* Register the viewlet manager as a content provider known as "javascript":
>>> from z3c.form.interfaces import IFormLayer
>>> from zope.contentprovider.interfaces import IContentProvider
>>> zope.component.provideAdapter(
... JSViewletManager,
... (None, IFormLayer, None),
... IContentProvider,
... name='javascript')
* Register the JS Subscriber viewlet for this new viewlet manager:
>>> from zope.viewlet.interfaces import IViewlet
>>> zope.component.provideAdapter(
... jsevent.JSSubscriptionsViewlet,
... (None, IFormLayer, interfaces.IHaveJSSubscriptions,
... JSViewletManager), IViewlet, name='subscriptions')
Finally, we need a template for our form:
>>> testing.addTemplate(demoform, 'buttons_form.pt')
We can now render the form:
>>> demoform.update()
>>> print demoform.render()
<html>
<head>
<script type="text/javascript">
$(document).ready(function(){
$("#form-buttons-hello").bind("click",
function(){alert("Hello World!");});
$("#form-buttons-dblhello").bind("dblclick",
function(){alert("Double Hello World!");});
})
</script>
</head>
<body>
<div class="action">
<input type="button" id="form-buttons-hello"
name="form.buttons.hello" class="buttonWidget jsbutton-field"
value="Hello World!" />
</div>
<div class="action">
<input type="button" id="form-buttons-dblhello"
name="form.buttons.dblhello" class="buttonWidget jsbutton-field"
value="Double Hello World!" />
</div>
</body>
</html>
As you can see, the subscriptions are correctly placed into the header, while
the buttons render as usual with exception to the input type, which is now a
"button".
Multiple Handlers
-----------------
Since there are multiple events in Javascript, one element can have multiple
handlers. So let's define a new form that declares two handlers for the same
button:
>>> class Form(form.Form):
... buttons = button.Buttons(IButtons).select('hello')
...
... @jsaction.handler(buttons['hello'])
... def showHelloWorldMessage(self, event, selector):
... return 'alert("Hello World!");'
...
... @jsaction.handler(buttons['hello'], event=jsevent.DBLCLICK)
... def showDoubleHelloWorldMessage(self, event, selector):
... return 'alert("Hello World! x 2");'
Let's now instantiate and update the form:
>>> demoform = Form(None, request)
>>> demoform.update()
The subscriptions are now available:
>>> list(demoform.jsSubscriptions)
[<JSSubscription
event=<JSEvent "click">,
selector=<WidgetSelector "form-buttons-hello">,
handler=<JSHandler <function showHelloWorldMessage ...>>>,
<JSSubscription
event=<JSEvent "dblclick">,
selector=<WidgetSelector "form-buttons-hello">,
handler=<JSHandler <function showDoubleHelloWorldMessage ...>>>]
Next we can look at a case where one handler is registered for all buttons and
events, and another overrides the click of the "hello" button to something
else:
>>> from z3c.form.interfaces import IButton
>>> class Form(form.Form):
... buttons = button.Buttons(IButtons)
...
... @jsaction.handler(IButton, interfaces.IJSEvent)
... def showHelloWorldMessage(self, event, selector):
... return '''alert("The event '%s' occured.");''' %event.name
...
... @jsaction.handler(buttons['hello'], event=jsevent.CLICK)
... def showDoubleHelloWorldMessage(self, event, selector):
... return 'alert("Hello World clicked!");'
>>> demoform = Form(None, request)
>>> demoform.update()
Rendering the subscriptions gives the following result:
>>> renderer = zope.component.getMultiAdapter(
... (demoform.jsSubscriptions, request), interfaces.IRenderer)
>>> renderer.update()
>>> print renderer.render()
$(document).ready(function(){
$("#...-hello").bind("click", function(){alert("Hello World clicked!");});
$("#...-hello").bind("dblclick", function(){alert("The ...");});
$("#...-hello").bind("change", function(){alert("The ...");});
$("#...-hello").bind("load", function(){alert("The ...");});
$("#...-hello").bind("blur", function(){alert("The ...");});
$("#...-hello").bind("focus", function(){alert("The ...");});
$("#...-hello").bind("keydown", function(){alert("The ...");});
$("#...-hello").bind("keyup", function(){alert("The ...");});
$("#...-hello").bind("mousedown", function(){alert("The ...");});
$("#...-hello").bind("mousemove", function(){alert("The ...");});
$("#...-hello").bind("mouseout", function(){alert("The ...");});
$("#...-hello").bind("mouseover", function(){alert("The ...");});
$("#...-hello").bind("mouseup", function(){alert("The ...");});
$("#...-hello").bind("resize", function(){alert("The ...");});
$("#...-hello").bind("select", function(){alert("The ...");});
$("#...-hello").bind("submit", function(){alert("The ...");});
$("#...-dblhello").bind("click", function(){alert("The ...");});
$("#...-dblhello").bind("dblclick", function(){alert("The ...");});
$("#...-dblhello").bind("change", function(){alert("The ...");});
$("#...-dblhello").bind("load", function(){alert("The ...");});
$("#...-dblhello").bind("blur", function(){alert("The ...");});
$("#...-dblhello").bind("focus", function(){alert("The ...");});
$("#...-dblhello").bind("keydown", function(){alert("The ...");});
$("#...-dblhello").bind("keyup", function(){alert("The ...");});
$("#...-dblhello").bind("mousedown", function(){alert("The ...");});
$("#...-dblhello").bind("mousemove", function(){alert("The ...");});
$("#...-dblhello").bind("mouseout", function(){alert("The ...");});
$("#...-dblhello").bind("mouseover", function(){alert("The ...");});
$("#...-dblhello").bind("mouseup", function(){alert("The ...");});
$("#...-dblhello").bind("resize", function(){alert("The ...");});
$("#...-dblhello").bind("select", function(){alert("The ...");});
$("#...-dblhello").bind("submit", function(){alert("The ...");});
})
While this output might seem excessive, it demonstrates that the generic
``IJSEvent`` subscription truly causes a subscription to all events. Further,
a more specific directive takes precendence over the more generic one. This is
due to the built-in adapter registry of the ``JSHandlers`` class.
Attaching Events to Form Fields
-------------------------------
Javascript handlers do not only work for buttons, but also for fields. Let's
create a simple schema that we can use to create a form:
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... name = zope.schema.TextLine(title=u'Name')
... age = zope.schema.Int(title=u'Age')
Even though somewhat pointless, whenever the "age" field is clicked on or the
"name" widget value changed, we would like to get an alert:
>>> class PersonAddForm(form.AddForm):
... fields = field.Fields(IPerson)
...
... @jsaction.handler(fields['age'])
... def ageClickEvent(self, event, selector):
... return 'alert("The Age was Clicked!");'
...
... @jsaction.handler(fields['name'], event=jsevent.CHANGE)
... def nameChangeEvent(self, event, selector):
... return 'alert("The Name was Changed!");'
We also need to register all of the default ``z3c.form`` registrations:
>>> from z3c.form.testing import setupFormDefaults
>>> setupFormDefaults()
After adding a simple template for the form, it can be rendered:
>>> addform = PersonAddForm(None, request)
>>> testing.addTemplate(addform, 'simple_edit.pt')
>>> addform.update()
>>> print addform.render()
<html>
<head>
<script type="text/javascript">
$(document).ready(function(){
$("#form-widgets-name").bind("change",
function(){alert("The Name was Changed!");});
$("#form-widgets-age").bind("click",
function(){alert("The Age was Clicked!");});
})
</script>
</head>
<body>
<form action=".">
<div class="row">
<label for="form-widgets-name">Name</label>
<input type="text" id="form-widgets-name" name="form.widgets.name"
class="textWidget textline-field" value="" />
</div>
<div class="row">
<label for="form-widgets-age">Age</label>
<input type="text" id="form-widgets-age" name="form.widgets.age"
class="textWidget int-field" value="" />
</div>
<div class="action">
<input type="submit" id="form-buttons-add" name="form.buttons.add"
class="submitWidget button-field" value="Add" />
</div>
</form>
</body>
</html>
As you can see, the form rendered perferctly, even allowing classic and
Javascript handlers to co-exist.
Appendix A: Javascript Event Handlers Manager
---------------------------------------------
The ``IJSEventHandlers`` implementataion (``JSHandlers`` class) is really an
advanced component with great features, so it deserves some additional
attention.
>>> handlers = jsaction.JSHandlers()
>>> handlers
<JSHandlers []>
When a handlers component is initialized, it creates an internal adapter
registry. If a handler is registered for a button, it simply behaves as an
instance-adapter.
>>> handlers._registry
<zope.interface.adapter.AdapterRegistry object at ...>
The object itself is pretty simple. To add a handler, we first have to create
a handler, ...
>>> def doSomething(form, event, selector):
... pass
>>> handler = jsaction.JSHandler(doSomething)
and a field/button:
>>> button1 = jsaction.JSButton(name='button1', title=u'Button 1')
Let's now add the handler:
>>> handlers.addHandler(button1, jsevent.CLICK, handler)
But you can also register handlers for groups of fields, either by interface
or class:
>>> class SpecialButton(jsaction.JSButton):
... pass
>>> handlers.addHandler(
... SpecialButton, jsevent.CLICK, jsaction.JSHandler('specialAction'))
>>> handlers
<JSHandlers
[<JSHandler <function doSomething at ...>>,
<JSHandler 'specialAction'>]>
Now all special buttons should use that handler:
>>> button2 = SpecialButton(name='button2', title=u'Button 2')
>>> button3 = SpecialButton(name='button3', title=u'Button 3')
>>> handlers.getHandlers(button2)
((<JSEvent "click">, <JSHandler 'specialAction'>),)
>>> handlers.getHandlers(button3)
((<JSEvent "click">, <JSHandler 'specialAction'>),)
However, registering a more specific handler for button 2 will override the
general handler:
>>> handlers.addHandler(
... button2, jsevent.CLICK, jsaction.JSHandler('specificAction2'))
>>> handlers.getHandlers(button2)
((<JSEvent "click">, <JSHandler 'specificAction2'>),)
>>> handlers.getHandlers(button3)
((<JSEvent "click">, <JSHandler 'specialAction'>),)
The same flexibility that is available to the field is also available for the
event.
>>> handlers = jsaction.JSHandlers()
So let's register a generic handler for all events:
>>> handlers.addHandler(
... jsaction.JSButton, jsevent.JSEvent,
... jsaction.JSHandler('genericEventAction'))
So when asking for the handlers of button 1, we get a very long list:
>>> handlers.getHandlers(button1)
((<JSEvent "click">, <JSHandler 'genericEventAction'>),
(<JSEvent "dblclick">, <JSHandler 'genericEventAction'>),
(<JSEvent "change">, <JSHandler 'genericEventAction'>),
(<JSEvent "load">, <JSHandler 'genericEventAction'>),
(<JSEvent "blur">, <JSHandler 'genericEventAction'>),
(<JSEvent "focus">, <JSHandler 'genericEventAction'>),
(<JSEvent "keydown">, <JSHandler 'genericEventAction'>),
(<JSEvent "keyup">, <JSHandler 'genericEventAction'>),
(<JSEvent "mousedown">, <JSHandler 'genericEventAction'>),
(<JSEvent "mousemove">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseout">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseover">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseup">, <JSHandler 'genericEventAction'>),
(<JSEvent "resize">, <JSHandler 'genericEventAction'>),
(<JSEvent "select">, <JSHandler 'genericEventAction'>),
(<JSEvent "submit">, <JSHandler 'genericEventAction'>))
So at this point you might ask: How is the complete set of events determined?
At this point we use the list of all events as listed in the
``jsevent.EVENTS`` variable.
Let's now register a special handler for the "click" event:
>>> handlers.addHandler(
... button1, jsevent.CLICK, jsaction.JSHandler('clickEventAction'))
So this registration takes precedence over the generic one:
>>> handlers.getHandlers(button1)
((<JSEvent "click">, <JSHandler 'clickEventAction'>),
(<JSEvent "dblclick">, <JSHandler 'genericEventAction'>),
(<JSEvent "change">, <JSHandler 'genericEventAction'>),
(<JSEvent "load">, <JSHandler 'genericEventAction'>),
(<JSEvent "blur">, <JSHandler 'genericEventAction'>),
(<JSEvent "focus">, <JSHandler 'genericEventAction'>),
(<JSEvent "keydown">, <JSHandler 'genericEventAction'>),
(<JSEvent "keyup">, <JSHandler 'genericEventAction'>),
(<JSEvent "mousedown">, <JSHandler 'genericEventAction'>),
(<JSEvent "mousemove">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseout">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseover">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseup">, <JSHandler 'genericEventAction'>),
(<JSEvent "resize">, <JSHandler 'genericEventAction'>),
(<JSEvent "select">, <JSHandler 'genericEventAction'>),
(<JSEvent "submit">, <JSHandler 'genericEventAction'>))
You can also add handlers objects:
>>> handlers = jsaction.JSHandlers()
>>> handlers.addHandler(
... button1, jsevent.CLICK, jsaction.JSHandler('button1ClickAction'))
>>> handlers2 = jsaction.JSHandlers()
>>> handlers2.addHandler(
... button2, jsevent.CLICK, jsaction.JSHandler('button2ClickAction'))
>>> handlers + handlers2
<JSHandlers
[<JSHandler 'button1ClickAction'>,
<JSHandler 'button2ClickAction'>]>
However, adding other components is not supported:
>>> handlers + 1
Traceback (most recent call last):
...
NotImplementedError
The handlers also provide a method to copy the handlers to a new instance:
>>> copy = handlers.copy()
>>> isinstance(copy, jsaction.JSHandlers)
True
>>> copy is handlers
False
This is commonly needed when one wants to extend the handlers of a super-form.
Appendix B: The Subscription-Creating Event Subscriber
------------------------------------------------------
The ``createSubscriptionsForWidget(event)`` event subscriber listens to
``IAfterWidgetUpdateEvent`` events and is responsible for looking up any
Javascript action handlers and create event subscriptions for them.
So let's setup the environment:
>>> class Form(form.Form):
... buttons = button.Buttons(IButtons)
...
... @jsaction.handler(buttons['hello'])
... def showHelloWorldMessage(self, event, selector):
... return 'alert("Hello World!");'
>>> form = Form(None, request)
Of course, not just any widget can have Javascript handlers. First of all, the
widget must be a field widget:
>>> from z3c.form import widget
>>> simpleWidget = widget.Widget(request)
>>> jsaction.createSubscriptionsForWidget(
... widget.AfterWidgetUpdateEvent(simpleWidget))
>>> interfaces.IHaveJSSubscriptions.providedBy(form)
False
And even if the widget is a field widget,
>>> from z3c.form.browser.button import ButtonFieldWidget
>>> helloWidget = ButtonFieldWidget(form.buttons['hello'], request)
it still needs to be a form-aware widget:
>>> jsaction.createSubscriptionsForWidget(
... widget.AfterWidgetUpdateEvent(helloWidget))
>>> interfaces.IHaveJSSubscriptions.providedBy(form)
False
So let's now make it work and add the form to the widget:
>>> from z3c.form.interfaces import IFormAware
>>> helloWidget.form = form
>>> zope.interface.alsoProvides(helloWidget, IFormAware)
After the subscriber successfully completes, we should have a sJavascript
subscription attached to the form:
>>> jsaction.createSubscriptionsForWidget(
... widget.AfterWidgetUpdateEvent(helloWidget))
>>> interfaces.IHaveJSSubscriptions.providedBy(form)
True
>>> list(form.jsSubscriptions)
[<JSSubscription
event=<JSEvent "click">, selector=<WidgetSelector "hello">,
handler=<JSHandler <function showHelloWorldMessage at ...>>>]
Finally, if the form does not have any Javascript handlers, in other words, it
does not have a ``jsHandlers`` attribute, then the subscriber also aborts:
>>> form = Form(None, request)
>>> helloWidget.form = object()
>>> jsaction.createSubscriptionsForWidget(
... widget.AfterWidgetUpdateEvent(helloWidget))
>>> interfaces.IHaveJSSubscriptions.providedBy(form)
False
And that's all.
==========================
JavaScript Form Validation
==========================
This package also supports widget value validation via Javascript. In
particular, the ``jsvalidator`` module implements server-side validation via
AJAX.
>>> from z3c.formjs import jsvalidator
There are two components to the validation API. The first is the validator, a
form mix-in class that makes the validation functionality via a URL and
defines the communication protocol of the validation; for example, it defines
what path must be accessed for the validation and what data to send and
return. The second component is the validation script, which is responsible
for defining the Javascript code that is executed when validation is
requested.
Message Validator
-----------------
The goal of the specific message validator is to validate a value, then
convert any error into a message and insert the message into the page's
content.
So let's do some necessary setups:
>>> from z3c.form.testing import setupFormDefaults
>>> setupFormDefaults()
>>> import zope.component
>>> from z3c.form import error
>>> zope.component.provideAdapter(error.ValueErrorViewSnippet)
We now create a simple form in which all widgets will be validated:
>>> import zope.interface
>>> import zope.schema
>>> class IAddress(zope.interface.Interface):
... zip = zope.schema.Int(title=u"Zip Code")
>>> from z3c.form import form, field
>>> from z3c.form.interfaces import IField
>>> from z3c.formjs import jsevent, jsaction
>>> class AddressEditForm(jsvalidator.MessageValidator, form.AddForm):
... fields = field.Fields(IAddress)
...
... @jsaction.handler(IField, event=jsevent.CHANGE)
... def fieldValidator(self, event, selector):
... return self.ValidationScript(self, selector.widget).render()
After instantiating the form, ...
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> edit = AddressEditForm(None, request)
>>> edit.update()
we can execute the handler to ensure we get some output:
>>> from z3c.formjs import testing
>>> testing.setupRenderers()
>>> from z3c.formjs import jsaction
>>> print edit.fieldValidator(
... None, jsaction.WidgetSelector(edit.widgets['zip']), request)
$.get('/validate', function(data){ alert(data) })
Validators use AJAX handlers to communicate with the server. Commonly the AJAX
handler is looked up via the "ajax" view -- see ``ajax.txt`` for more
details. In this case we just create a small helper function:
>>> from z3c.formjs import ajax
>>> def AJAXPlugin(view):
... return ajax.AJAXRequestTraverserPlugin(view, view.request)
>>> from z3c.formjs import ajax, interfaces
>>> from zope.publisher.interfaces.browser import IBrowserRequest
>>> zope.component.provideSubscriptionAdapter(
... ajax.AJAXRequestTraverserPlugin,
... (interfaces.IFormTraverser, IBrowserRequest))
we can traverse to the ``validate`` method from the "ajax" view and render
it. Let's first render some valid input:
>>> request = TestRequest(form={'widget-name' : 'zip',
... 'form.widgets.zip' : u'29132'})
>>> edit = AddressEditForm(None, request)
>>> edit.update()
>>> AJAXPlugin(edit).publishTraverse(None, 'validate')()
u''
As you can see there is no error message. Let's now provide an invalid ZIP
code. As you can see, we get the expected error message:
>>> request = TestRequest(form={'widget-name': 'zip',
... 'form.widgets.zip':'notazipcode'})
>>> edit = AddressEditForm(None, request)
>>> edit.update()
>>> AJAXPlugin(edit).publishTraverse(None, 'validate')()
u'The system could not process the given value.'
Of course, one cannot just traverse to any attribute in the form:
>>> AJAXPlugin(edit).publishTraverse(None, 'ValidationScript')()
Traceback (most recent call last):
...
NotFound: Object: <AddressEditForm ...>, name: 'ValidationScript'
And that's it.
===============================
Connecting to Javascript Events
===============================
The ``jsevent`` module of this package implements a mechanism to connect a
Javascript script to an event of a DOM element. So let's have a look at how
this works.
>>> from z3c.formjs import interfaces, jsevent
To implement this functionality, we need to model three components: events,
DOM elements (selector), and the script (handler). We will also need a manager
to keep track of all the mappings. This is indeed somewhat similar to the Zope
3 event model, though we do not need DOM elements to connect the events there.
Subscription Manager
--------------------
So first we need to create a subscription manager in which to collect the
subscriptions:
>>> manager = jsevent.JSSubscriptions()
Initially, we have no registered events:
>>> list(manager)
[]
We now want to subscribe to the "click" event of a DOM element with the id
"message". When the event occurs, we would like to display a simple "Hello
World" message.
The events are available in all capital letters, for example:
>>> jsevent.CLICK
<JSEvent "click">
The DOM element is selected using a selector, in our case an id selector:
>>> selector = jsevent.IdSelector('message')
>>> selector
<IdSelector "message">
The handler of the event is a callable accepting the event, selector and the
request:
>>> def showHelloWorldAlert(event, selector, request):
... return u'alert("Hello World!")'
We have finally all the pieces together to subscribe the event:
>>> manager.subscribe(jsevent.CLICK, selector, showHelloWorldAlert)
<JSSubscription event=<JSEvent "click">,
selector=<IdSelector "message">,
handler=<function showHelloWorldAlert at ...>>
So now we can see the subscription:
>>> list(manager)
[<JSSubscription event=<JSEvent "click">,
selector=<IdSelector "message">,
handler=<function showHelloWorldAlert at ...>>]
So now, how does this get rendered into Javascript code? Since this package
strictly separates definition from rendering, a renderer will be responsible
to produce the output.
Renderers
---------
So let's define some renderers for the various components. We have already
prepared renderers for testing purposes in the ``testing`` support module. The
first one is for the id selector
>>> import zope.component
>>> from z3c.formjs import testing
>>> zope.component.provideAdapter(testing.IdSelectorRenderer)
Of course, like all view components, the renderer supports the update/render
pattern. We can now render the selector:
>>> from zope.publisher.browser import TestRequest
>>> request = TestRequest()
>>> renderer = zope.component.getMultiAdapter(
... (selector, request), interfaces.IRenderer)
>>> renderer.update()
>>> renderer.render()
u'#message'
Next we need a renderer for the subscription. Let's assume we can bind the
subscription as follows: ``$(<selector>).bind("<event>", <script>)``
>>> zope.component.provideAdapter(testing.SubscriptionRenderer)
Rendering the subscription then returns this:
>>> renderer = zope.component.getMultiAdapter(
... (list(manager)[0], request), interfaces.IRenderer)
>>> renderer.update()
>>> print renderer.render()
$("#message").bind("click", function(){alert("Hello World!")});
And now to the grant finale. We create a renderer for the subscription manager.
>>> zope.component.provideAdapter(testing.ManagerRenderer)
Let's now render the entire manager.
>>> renderer = zope.component.getMultiAdapter(
... (manager, request), interfaces.IRenderer)
>>> renderer.update()
>>> print renderer.render()
$(document).ready(function(){
$("#message").bind("click", function(){alert("Hello World!")});
})
The Subscription Decorator
--------------------------
When defining JS event subscriptions from within a presentation component,
using the low-level subscription API is somewhat cumbersome. Thus, there
exists a decorator called ``subscribe``, which can convert a component method
as a subscription handler. Let's have a look:
>>> class MyView(object):
...
... @jsevent.subscribe(jsevent.IdSelector('myid'), jsevent.DBLCLICK)
... def alertUser(event, selector, request):
... return u"alert('`%s` event occured on DOM element `%s`');" %(
... event.name, selector.id)
As you can see, the function is never really meant to be a method, but a
subscription handler; thus no ``self`` as first argument. The subscription is
now available in the subscriptions manager of the view:
>>> list(MyView.jsSubscriptions)
[<JSSubscription event=<JSEvent "dblclick">,
selector=<IdSelector "myid">,
handler=<function alertUser at ...>>]
Let's now render the subscription:
>>> renderer = zope.component.getMultiAdapter(
... (list(MyView.jsSubscriptions)[0], request), interfaces.IRenderer)
>>> renderer.update()
>>> print renderer.render()
$("#myid").bind("dblclick",
function(){alert('`dblclick` event occured on DOM element `myid`');});
Javascript Viewlet
------------------
Putting in the Javascript by hand in every layout is a bit lame. Instead we
can just register a viewlet for the JS viewlet manager that renders the
subscriptions if a manager is found.
To use the viewlet we need a view that provides a subscription manager:
>>> class View(object):
... zope.interface.implements(interfaces.IHaveJSSubscriptions)
... jsSubscriptions = manager
We can now initialize, update, and finally render the viewlet:
>>> viewlet = jsevent.JSSubscriptionsViewlet(
... object(), request, View(), object())
>>> viewlet.update()
>>> print viewlet.render()
<script type="text/javascript">
$(document).ready(function(){
$("#message").bind("click", function(){alert("Hello World!")});
})
</script>
Selectors
---------
The module provides several DOM element selectors. It is the responsibility of
the corresponding rednerer to interpret the selector.
Id Selector
~~~~~~~~~~~
The id selector selects a DOM element by id, as seen above. It is simply
initialized using the the id:
>>> idselect = jsevent.IdSelector('myid')
>>> idselect
<IdSelector "myid">
The id is also available as attribute:
>>> idselect.id
'myid'
We already saw before how it gets rendered:
>>> renderer = zope.component.getMultiAdapter(
... (idselect, request), interfaces.IRenderer)
>>> renderer.update()
>>> renderer.render()
u'#myid'
CSS Selector
~~~~~~~~~~~~
The CSS selector selects a DOM element using an arbitrary CSS selector
expression. This selector is initialized using the expression:
>>> cssselect = jsevent.CSSSelector('div.myclass')
>>> cssselect
<CSSSelector "div.myclass">
The CSS selector expression is also available as attribute:
>>> cssselect.expr
'div.myclass'
Let's now see an example on how the CSS selector can be rendered:
>>> zope.component.provideAdapter(testing.CSSSelectorRenderer)
>>> renderer = zope.component.getMultiAdapter(
... (cssselect, request), interfaces.IRenderer)
>>> renderer.update()
>>> renderer.render()
u'div.myclass'
Since most JS libraries support CSS selectors by default, the renderer simply
converts the expression to unicode.
Available Events
----------------
This package maps all of the available JavaScript events. Here is the complete
list:
>>> jsevent.CLICK
<JSEvent "click">
>>> jsevent.DBLCLICK
<JSEvent "dblclick">
>>> jsevent.CHANGE
<JSEvent "change">
>>> jsevent.LOAD
<JSEvent "load">
>>> jsevent.BLUR
<JSEvent "blur">
>>> jsevent.FOCUS
<JSEvent "focus">
>>> jsevent.KEYDOWN
<JSEvent "keydown">
>>> jsevent.KEYUP
<JSEvent "keyup">
>>> jsevent.MOUSEDOWN
<JSEvent "mousedown">
>>> jsevent.MOUSEMOVE
<JSEvent "mousemove">
>>> jsevent.MOUSEOUT
<JSEvent "mouseout">
>>> jsevent.MOUSEOVER
<JSEvent "mouseover">
>>> jsevent.MOUSEUP
<JSEvent "mouseup">
>>> jsevent.RESIZE
<JSEvent "resize">
>>> jsevent.SELECT
<JSEvent "select">
>>> jsevent.SUBMIT
<JSEvent "submit">
These are also provided as utilities so they can be looked up by name.
>>> import zope.component
>>> zope.component.provideUtility(jsevent.CLICK, name='click')
Of course, we can now just look up the utility:
>>> zope.component.getUtility(interfaces.IJSEvent, 'click')
<JSEvent "click">
=======
CHANGES
=======
Version 0.2.0 (7/18/2007)
--------------------------
- Feature: Registration of public AJAX server calls via a simple
decorator. The calls are made available via a special ``ajax`` view on the
original view.
- Feature: Allow registering of JS subscriptions via a decorator within the
presentation component.
- Feature: Added a new CSS selector.
- Feature: Implementation of AJAX-driven widget value validation.
- Restructure: Completely overhauled the entire API to be most easy to use and
have the most minimal implementation.
- Bug: The package is now 100% tested.
- Feature: Implementation of AJAX request handlers in forms.
Version 0.1.0 (6/29/2007)
-------------------------
- Initial Release
* Feature: JS event association with fields and buttons.
====
TODO
====
- A way to write a javascript function in python such that you can
render a call to it from python
class SomeForm:
@jsfunction('bar') # bar is the namespace
def foo(self):
return 'alert("foo");'
renderJSFunction(SomeForm.foo)
'function z3c_formjs_foo(){ alert("foo"); }'
renderJSCall(SomeForm.foo)
'bar_foo()'
- ajax form submission - ala "save" button
- ajax widget switching
- client side js validators for simple fields. (maybe we can use an
existing library?)
the z3c.form library.
Detailed Documentation
**********************
===========================
Form Javascript Integration
===========================
This package is designed to provide a Python API to common Javascript
features for forms written with the ``z3c.form*`` packages. While the
reference backend-implementation is for the JQuery library, any other
Javascript library can be hooked into the Python API.
The documents are ordered in the way they should be read:
- ``jsaction.txt`` [must read]
This document describes how JS scripts can be connected to events on a
any widget, inclduing buttons.
- ``jsvalidator.txt`` [must read]
This document demonstrates how "live" widget value validation can be
achieved.
- ``jsevent.txt`` [advanced users]
This documents describes the generalization that allows hooking up script to
events on any field.
- ``jqueryrenderer.txt`` [advanced users]
This document demonstrates all necessary backend renderer components
necessary to accomplish any of the features of this package.
=============================
Javascript Events for Buttons
=============================
In the ``z3c.form`` package, buttons are most commonly rendered as "submit"
input fields within a form, meaning that the form will always be
submitted. When working with Javascript, on the other hand, a click on the
button often simply executes a script. The ``jsaction`` module of this package
is designed to implement the latter kind.
>>> from z3c.formjs import jsaction
Javascript Buttons
------------------
Before we can write a form that uses Javascript buttons, we have to define
them first. One common way to define buttons in ``z3c.form`` is to write an
schema describing them; so let's do that now:
>>> import zope.interface
>>> class IButtons(zope.interface.Interface):
... hello = jsaction.JSButton(title=u'Hello World!')
... dblhello = jsaction.JSButton(title=u'Double Hello World!')
Instead of declaring ``z3c.form.button.Button`` fields, we are now using a
derived Javascript button field. While there is no difference initially, they
will later be rendered differently. (Basically, ``JSButton`` fields render as
button widgets.)
Widget Selector
---------------
Like for regular fields, the action of the buttons is defined using handlers,
in our case Javascript handler. Selectors are used to determine the DOM
element or elements for which a handler is registered. The widget selector
uses a widget to provide the selector API:
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> from z3c.form.browser import text
>>> msg = text.TextWidget(request)
>>> msg.id = 'form-msg'
>>> msg.name = 'msg'
>>> selector = jsaction.WidgetSelector(msg)
>>> selector
<WidgetSelector "form-msg">
Since the widget selector can determine the widget's id, it is also an id
selector (see ``jsevent.txt``):
>>> from z3c.formjs import interfaces
>>> interfaces.IIdSelector.providedBy(selector)
True
>>> selector.id
'form-msg'
This has the advantage that we can reuse the renderer of the id
selector.
Javascript Event Subscriptions
------------------------------
As discussed in ``jsevent.txt``, all the Javascript event subscriptions are
stored on the view in a special attribute called ``jsSubscriptions``. While
updating the form, one can simply add subscriptions to this registry. So let's
say we have the following handler:
>>> def showSelectedWidget(event, selector, request):
... return 'alert("%r");' %(selector.widget)
We now want to connect this handler to the ``msg`` widget to be executed when
the mouse is clicked within this element:
>>> import zope.interface
>>> from z3c.formjs import jsevent
>>> class Form(object):
... zope.interface.implements(interfaces.IHaveJSSubscriptions)
... jsSubscriptions = jsevent.JSSubscriptions()
...
... def update(self):
... self.jsSubscriptions.subscribe(
... jsevent.CLICK, selector, showSelectedWidget)
>>> form = Form()
>>> form.update()
After registering the subscription-related renderers,
>>> from z3c.formjs import testing
>>> testing.setupRenderers()
we can use the subscription rendering viewlet to check the subscription
output:
>>> viewlet = jsevent.JSSubscriptionsViewlet(None, request, form, None)
>>> viewlet.update()
>>> print viewlet.render()
<script type="text/javascript">
$(document).ready(function(){
$("#form-msg").bind("click", function(){alert("<TextWidget 'msg'>");});
})
</script>
Forms with Javascript Buttons
-----------------------------
The next step is create the form. Luckily we do not need any fields to render
a form. Also, instead of using usual ``z3c.form.button.handler()`` function,
we now have a special handler decorator that connects a button to a Javascript
event. The output of the handler itself is a string that is used as the
Javascript script that is executed.
>>> from z3c.form import button, form
>>> class Form(form.Form):
... buttons = button.Buttons(IButtons)
...
... @jsaction.handler(buttons['hello'])
... def showHelloWorldMessage(self, event, selector):
... return 'alert("%s");' % selector.widget.title
...
... @jsaction.handler(buttons['dblhello'], event=jsevent.DBLCLICK)
... def showDoubleHelloWorldMessage(self, event, selector):
... return 'alert("%s");' % selector.widget.title
The ``handler()`` decorator takes two arguments, the button (acting as the DOM
element selector) and the event to which to bind the action. By default the
event is ``jsevent.CLICK``.
And that is really everything that is required from a user's point of
view. Let us now see how those handler declarations are converted into actions
and Javascript subscriptions. First we need to initialize the form:
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> demoform = Form(None, request)
We also need to register an adapter to create an action from a button:
>>> from z3c.form.interfaces import IButtonAction
>>> zope.component.provideAdapter(
... jsaction.JSButtonAction, provides=IButtonAction)
Finally, for the Javascript subscriptions to be registered, we need an event
listener that reacts to "after widget/action update" events:
>>> zope.component.provideHandler(jsaction.createSubscriptionsForWidget)
Action managers are instantiated using the form, request, and
context/content. A button-action-manager implementation is avaialble in the
``z3c.form.button`` package:
>>> actions = button.ButtonActions(demoform, request, None)
>>> actions.update()
Once the action manager is updated, the buttons should be available as
actions:
>>> actions.keys()
['hello', 'dblhello']
>>> actions['hello']
<JSButtonAction 'form.buttons.hello' u'Hello World!'>
Since special Javascript handlers were registered for those buttons, creating
and updating the actions has also caused the form to become an
``IHaveJSSubscriptions`` view:
>>> from z3c.formjs import interfaces
>>> interfaces.IHaveJSSubscriptions.providedBy(demoform)
True
>>> demoform.jsSubscriptions
<z3c.formjs.jsevent.JSSubscriptions object at ...>
The interesting part about button subscriptions is the selector.
>>> selector = list(demoform.jsSubscriptions)[0].selector
>>> selector
<WidgetSelector "form-buttons-hello">
As you can see, the system automatically created a widget selector:
>>> selector.id
'form-buttons-hello'
>>> selector.widget
<JSButtonAction 'form.buttons.hello' u'Hello World!'>
With the declarations in place, we can now go on.
Rendering the Form
------------------
Let's now see what we need to do to make the form render correctly and
completely.
>>> demoform = Form(None, request)
First we need some of the standard ``z3c.form`` registrations:
>>> from z3c.form import field, button
>>> zope.component.provideAdapter(field.FieldWidgets)
>>> zope.component.provideAdapter(button.ButtonActions)
Next we need to register the template for our button actions:
>>> from zope.pagetemplate.interfaces import IPageTemplate
>>> from z3c.form import widget
>>> from z3c.form.interfaces import IButtonWidget, INPUT_MODE
>>> from z3c.form.testing import getPath
>>> zope.component.provideAdapter(
... widget.WidgetTemplateFactory(getPath('button_input.pt'), 'text/html'),
... (None, None, None, None, IButtonWidget),
... IPageTemplate, name=INPUT_MODE)
We also need to setup a Javascript viewlet manager and register the
subscription viewlet for it, so that the subscriptions actually appear in the
HTML page. (This is a bit tedious to do using the Python API, but using ZCML
this is much simpler.)
* Hook up the "provider" TALES expression type:
>>> from zope.app.pagetemplate.engine import TrustedEngine
>>> from zope.contentprovider import tales
>>> TrustedEngine.registerType('provider', tales.TALESProviderExpression)
* Create a viewlet manager that does not require security to be setup:
>>> from zope.viewlet import manager
>>> class JSViewletManager(manager.ViewletManagerBase):
... def filter(self, viewlets):
... return viewlets
* Register the viewlet manager as a content provider known as "javascript":
>>> from z3c.form.interfaces import IFormLayer
>>> from zope.contentprovider.interfaces import IContentProvider
>>> zope.component.provideAdapter(
... JSViewletManager,
... (None, IFormLayer, None),
... IContentProvider,
... name='javascript')
* Register the JS Subscriber viewlet for this new viewlet manager:
>>> from zope.viewlet.interfaces import IViewlet
>>> zope.component.provideAdapter(
... jsevent.JSSubscriptionsViewlet,
... (None, IFormLayer, interfaces.IHaveJSSubscriptions,
... JSViewletManager), IViewlet, name='subscriptions')
Finally, we need a template for our form:
>>> testing.addTemplate(demoform, 'buttons_form.pt')
We can now render the form:
>>> demoform.update()
>>> print demoform.render()
<html>
<head>
<script type="text/javascript">
$(document).ready(function(){
$("#form-buttons-hello").bind("click",
function(){alert("Hello World!");});
$("#form-buttons-dblhello").bind("dblclick",
function(){alert("Double Hello World!");});
})
</script>
</head>
<body>
<div class="action">
<input type="button" id="form-buttons-hello"
name="form.buttons.hello" class="buttonWidget jsbutton-field"
value="Hello World!" />
</div>
<div class="action">
<input type="button" id="form-buttons-dblhello"
name="form.buttons.dblhello" class="buttonWidget jsbutton-field"
value="Double Hello World!" />
</div>
</body>
</html>
As you can see, the subscriptions are correctly placed into the header, while
the buttons render as usual with exception to the input type, which is now a
"button".
Multiple Handlers
-----------------
Since there are multiple events in Javascript, one element can have multiple
handlers. So let's define a new form that declares two handlers for the same
button:
>>> class Form(form.Form):
... buttons = button.Buttons(IButtons).select('hello')
...
... @jsaction.handler(buttons['hello'])
... def showHelloWorldMessage(self, event, selector):
... return 'alert("Hello World!");'
...
... @jsaction.handler(buttons['hello'], event=jsevent.DBLCLICK)
... def showDoubleHelloWorldMessage(self, event, selector):
... return 'alert("Hello World! x 2");'
Let's now instantiate and update the form:
>>> demoform = Form(None, request)
>>> demoform.update()
The subscriptions are now available:
>>> list(demoform.jsSubscriptions)
[<JSSubscription
event=<JSEvent "click">,
selector=<WidgetSelector "form-buttons-hello">,
handler=<JSHandler <function showHelloWorldMessage ...>>>,
<JSSubscription
event=<JSEvent "dblclick">,
selector=<WidgetSelector "form-buttons-hello">,
handler=<JSHandler <function showDoubleHelloWorldMessage ...>>>]
Next we can look at a case where one handler is registered for all buttons and
events, and another overrides the click of the "hello" button to something
else:
>>> from z3c.form.interfaces import IButton
>>> class Form(form.Form):
... buttons = button.Buttons(IButtons)
...
... @jsaction.handler(IButton, interfaces.IJSEvent)
... def showHelloWorldMessage(self, event, selector):
... return '''alert("The event '%s' occured.");''' %event.name
...
... @jsaction.handler(buttons['hello'], event=jsevent.CLICK)
... def showDoubleHelloWorldMessage(self, event, selector):
... return 'alert("Hello World clicked!");'
>>> demoform = Form(None, request)
>>> demoform.update()
Rendering the subscriptions gives the following result:
>>> renderer = zope.component.getMultiAdapter(
... (demoform.jsSubscriptions, request), interfaces.IRenderer)
>>> renderer.update()
>>> print renderer.render()
$(document).ready(function(){
$("#...-hello").bind("click", function(){alert("Hello World clicked!");});
$("#...-hello").bind("dblclick", function(){alert("The ...");});
$("#...-hello").bind("change", function(){alert("The ...");});
$("#...-hello").bind("load", function(){alert("The ...");});
$("#...-hello").bind("blur", function(){alert("The ...");});
$("#...-hello").bind("focus", function(){alert("The ...");});
$("#...-hello").bind("keydown", function(){alert("The ...");});
$("#...-hello").bind("keyup", function(){alert("The ...");});
$("#...-hello").bind("mousedown", function(){alert("The ...");});
$("#...-hello").bind("mousemove", function(){alert("The ...");});
$("#...-hello").bind("mouseout", function(){alert("The ...");});
$("#...-hello").bind("mouseover", function(){alert("The ...");});
$("#...-hello").bind("mouseup", function(){alert("The ...");});
$("#...-hello").bind("resize", function(){alert("The ...");});
$("#...-hello").bind("select", function(){alert("The ...");});
$("#...-hello").bind("submit", function(){alert("The ...");});
$("#...-dblhello").bind("click", function(){alert("The ...");});
$("#...-dblhello").bind("dblclick", function(){alert("The ...");});
$("#...-dblhello").bind("change", function(){alert("The ...");});
$("#...-dblhello").bind("load", function(){alert("The ...");});
$("#...-dblhello").bind("blur", function(){alert("The ...");});
$("#...-dblhello").bind("focus", function(){alert("The ...");});
$("#...-dblhello").bind("keydown", function(){alert("The ...");});
$("#...-dblhello").bind("keyup", function(){alert("The ...");});
$("#...-dblhello").bind("mousedown", function(){alert("The ...");});
$("#...-dblhello").bind("mousemove", function(){alert("The ...");});
$("#...-dblhello").bind("mouseout", function(){alert("The ...");});
$("#...-dblhello").bind("mouseover", function(){alert("The ...");});
$("#...-dblhello").bind("mouseup", function(){alert("The ...");});
$("#...-dblhello").bind("resize", function(){alert("The ...");});
$("#...-dblhello").bind("select", function(){alert("The ...");});
$("#...-dblhello").bind("submit", function(){alert("The ...");});
})
While this output might seem excessive, it demonstrates that the generic
``IJSEvent`` subscription truly causes a subscription to all events. Further,
a more specific directive takes precendence over the more generic one. This is
due to the built-in adapter registry of the ``JSHandlers`` class.
Attaching Events to Form Fields
-------------------------------
Javascript handlers do not only work for buttons, but also for fields. Let's
create a simple schema that we can use to create a form:
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... name = zope.schema.TextLine(title=u'Name')
... age = zope.schema.Int(title=u'Age')
Even though somewhat pointless, whenever the "age" field is clicked on or the
"name" widget value changed, we would like to get an alert:
>>> class PersonAddForm(form.AddForm):
... fields = field.Fields(IPerson)
...
... @jsaction.handler(fields['age'])
... def ageClickEvent(self, event, selector):
... return 'alert("The Age was Clicked!");'
...
... @jsaction.handler(fields['name'], event=jsevent.CHANGE)
... def nameChangeEvent(self, event, selector):
... return 'alert("The Name was Changed!");'
We also need to register all of the default ``z3c.form`` registrations:
>>> from z3c.form.testing import setupFormDefaults
>>> setupFormDefaults()
After adding a simple template for the form, it can be rendered:
>>> addform = PersonAddForm(None, request)
>>> testing.addTemplate(addform, 'simple_edit.pt')
>>> addform.update()
>>> print addform.render()
<html>
<head>
<script type="text/javascript">
$(document).ready(function(){
$("#form-widgets-name").bind("change",
function(){alert("The Name was Changed!");});
$("#form-widgets-age").bind("click",
function(){alert("The Age was Clicked!");});
})
</script>
</head>
<body>
<form action=".">
<div class="row">
<label for="form-widgets-name">Name</label>
<input type="text" id="form-widgets-name" name="form.widgets.name"
class="textWidget textline-field" value="" />
</div>
<div class="row">
<label for="form-widgets-age">Age</label>
<input type="text" id="form-widgets-age" name="form.widgets.age"
class="textWidget int-field" value="" />
</div>
<div class="action">
<input type="submit" id="form-buttons-add" name="form.buttons.add"
class="submitWidget button-field" value="Add" />
</div>
</form>
</body>
</html>
As you can see, the form rendered perferctly, even allowing classic and
Javascript handlers to co-exist.
Appendix A: Javascript Event Handlers Manager
---------------------------------------------
The ``IJSEventHandlers`` implementataion (``JSHandlers`` class) is really an
advanced component with great features, so it deserves some additional
attention.
>>> handlers = jsaction.JSHandlers()
>>> handlers
<JSHandlers []>
When a handlers component is initialized, it creates an internal adapter
registry. If a handler is registered for a button, it simply behaves as an
instance-adapter.
>>> handlers._registry
<zope.interface.adapter.AdapterRegistry object at ...>
The object itself is pretty simple. To add a handler, we first have to create
a handler, ...
>>> def doSomething(form, event, selector):
... pass
>>> handler = jsaction.JSHandler(doSomething)
and a field/button:
>>> button1 = jsaction.JSButton(name='button1', title=u'Button 1')
Let's now add the handler:
>>> handlers.addHandler(button1, jsevent.CLICK, handler)
But you can also register handlers for groups of fields, either by interface
or class:
>>> class SpecialButton(jsaction.JSButton):
... pass
>>> handlers.addHandler(
... SpecialButton, jsevent.CLICK, jsaction.JSHandler('specialAction'))
>>> handlers
<JSHandlers
[<JSHandler <function doSomething at ...>>,
<JSHandler 'specialAction'>]>
Now all special buttons should use that handler:
>>> button2 = SpecialButton(name='button2', title=u'Button 2')
>>> button3 = SpecialButton(name='button3', title=u'Button 3')
>>> handlers.getHandlers(button2)
((<JSEvent "click">, <JSHandler 'specialAction'>),)
>>> handlers.getHandlers(button3)
((<JSEvent "click">, <JSHandler 'specialAction'>),)
However, registering a more specific handler for button 2 will override the
general handler:
>>> handlers.addHandler(
... button2, jsevent.CLICK, jsaction.JSHandler('specificAction2'))
>>> handlers.getHandlers(button2)
((<JSEvent "click">, <JSHandler 'specificAction2'>),)
>>> handlers.getHandlers(button3)
((<JSEvent "click">, <JSHandler 'specialAction'>),)
The same flexibility that is available to the field is also available for the
event.
>>> handlers = jsaction.JSHandlers()
So let's register a generic handler for all events:
>>> handlers.addHandler(
... jsaction.JSButton, jsevent.JSEvent,
... jsaction.JSHandler('genericEventAction'))
So when asking for the handlers of button 1, we get a very long list:
>>> handlers.getHandlers(button1)
((<JSEvent "click">, <JSHandler 'genericEventAction'>),
(<JSEvent "dblclick">, <JSHandler 'genericEventAction'>),
(<JSEvent "change">, <JSHandler 'genericEventAction'>),
(<JSEvent "load">, <JSHandler 'genericEventAction'>),
(<JSEvent "blur">, <JSHandler 'genericEventAction'>),
(<JSEvent "focus">, <JSHandler 'genericEventAction'>),
(<JSEvent "keydown">, <JSHandler 'genericEventAction'>),
(<JSEvent "keyup">, <JSHandler 'genericEventAction'>),
(<JSEvent "mousedown">, <JSHandler 'genericEventAction'>),
(<JSEvent "mousemove">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseout">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseover">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseup">, <JSHandler 'genericEventAction'>),
(<JSEvent "resize">, <JSHandler 'genericEventAction'>),
(<JSEvent "select">, <JSHandler 'genericEventAction'>),
(<JSEvent "submit">, <JSHandler 'genericEventAction'>))
So at this point you might ask: How is the complete set of events determined?
At this point we use the list of all events as listed in the
``jsevent.EVENTS`` variable.
Let's now register a special handler for the "click" event:
>>> handlers.addHandler(
... button1, jsevent.CLICK, jsaction.JSHandler('clickEventAction'))
So this registration takes precedence over the generic one:
>>> handlers.getHandlers(button1)
((<JSEvent "click">, <JSHandler 'clickEventAction'>),
(<JSEvent "dblclick">, <JSHandler 'genericEventAction'>),
(<JSEvent "change">, <JSHandler 'genericEventAction'>),
(<JSEvent "load">, <JSHandler 'genericEventAction'>),
(<JSEvent "blur">, <JSHandler 'genericEventAction'>),
(<JSEvent "focus">, <JSHandler 'genericEventAction'>),
(<JSEvent "keydown">, <JSHandler 'genericEventAction'>),
(<JSEvent "keyup">, <JSHandler 'genericEventAction'>),
(<JSEvent "mousedown">, <JSHandler 'genericEventAction'>),
(<JSEvent "mousemove">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseout">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseover">, <JSHandler 'genericEventAction'>),
(<JSEvent "mouseup">, <JSHandler 'genericEventAction'>),
(<JSEvent "resize">, <JSHandler 'genericEventAction'>),
(<JSEvent "select">, <JSHandler 'genericEventAction'>),
(<JSEvent "submit">, <JSHandler 'genericEventAction'>))
You can also add handlers objects:
>>> handlers = jsaction.JSHandlers()
>>> handlers.addHandler(
... button1, jsevent.CLICK, jsaction.JSHandler('button1ClickAction'))
>>> handlers2 = jsaction.JSHandlers()
>>> handlers2.addHandler(
... button2, jsevent.CLICK, jsaction.JSHandler('button2ClickAction'))
>>> handlers + handlers2
<JSHandlers
[<JSHandler 'button1ClickAction'>,
<JSHandler 'button2ClickAction'>]>
However, adding other components is not supported:
>>> handlers + 1
Traceback (most recent call last):
...
NotImplementedError
The handlers also provide a method to copy the handlers to a new instance:
>>> copy = handlers.copy()
>>> isinstance(copy, jsaction.JSHandlers)
True
>>> copy is handlers
False
This is commonly needed when one wants to extend the handlers of a super-form.
Appendix B: The Subscription-Creating Event Subscriber
------------------------------------------------------
The ``createSubscriptionsForWidget(event)`` event subscriber listens to
``IAfterWidgetUpdateEvent`` events and is responsible for looking up any
Javascript action handlers and create event subscriptions for them.
So let's setup the environment:
>>> class Form(form.Form):
... buttons = button.Buttons(IButtons)
...
... @jsaction.handler(buttons['hello'])
... def showHelloWorldMessage(self, event, selector):
... return 'alert("Hello World!");'
>>> form = Form(None, request)
Of course, not just any widget can have Javascript handlers. First of all, the
widget must be a field widget:
>>> from z3c.form import widget
>>> simpleWidget = widget.Widget(request)
>>> jsaction.createSubscriptionsForWidget(
... widget.AfterWidgetUpdateEvent(simpleWidget))
>>> interfaces.IHaveJSSubscriptions.providedBy(form)
False
And even if the widget is a field widget,
>>> from z3c.form.browser.button import ButtonFieldWidget
>>> helloWidget = ButtonFieldWidget(form.buttons['hello'], request)
it still needs to be a form-aware widget:
>>> jsaction.createSubscriptionsForWidget(
... widget.AfterWidgetUpdateEvent(helloWidget))
>>> interfaces.IHaveJSSubscriptions.providedBy(form)
False
So let's now make it work and add the form to the widget:
>>> from z3c.form.interfaces import IFormAware
>>> helloWidget.form = form
>>> zope.interface.alsoProvides(helloWidget, IFormAware)
After the subscriber successfully completes, we should have a sJavascript
subscription attached to the form:
>>> jsaction.createSubscriptionsForWidget(
... widget.AfterWidgetUpdateEvent(helloWidget))
>>> interfaces.IHaveJSSubscriptions.providedBy(form)
True
>>> list(form.jsSubscriptions)
[<JSSubscription
event=<JSEvent "click">, selector=<WidgetSelector "hello">,
handler=<JSHandler <function showHelloWorldMessage at ...>>>]
Finally, if the form does not have any Javascript handlers, in other words, it
does not have a ``jsHandlers`` attribute, then the subscriber also aborts:
>>> form = Form(None, request)
>>> helloWidget.form = object()
>>> jsaction.createSubscriptionsForWidget(
... widget.AfterWidgetUpdateEvent(helloWidget))
>>> interfaces.IHaveJSSubscriptions.providedBy(form)
False
And that's all.
==========================
JavaScript Form Validation
==========================
This package also supports widget value validation via Javascript. In
particular, the ``jsvalidator`` module implements server-side validation via
AJAX.
>>> from z3c.formjs import jsvalidator
There are two components to the validation API. The first is the validator, a
form mix-in class that makes the validation functionality via a URL and
defines the communication protocol of the validation; for example, it defines
what path must be accessed for the validation and what data to send and
return. The second component is the validation script, which is responsible
for defining the Javascript code that is executed when validation is
requested.
Message Validator
-----------------
The goal of the specific message validator is to validate a value, then
convert any error into a message and insert the message into the page's
content.
So let's do some necessary setups:
>>> from z3c.form.testing import setupFormDefaults
>>> setupFormDefaults()
>>> import zope.component
>>> from z3c.form import error
>>> zope.component.provideAdapter(error.ValueErrorViewSnippet)
We now create a simple form in which all widgets will be validated:
>>> import zope.interface
>>> import zope.schema
>>> class IAddress(zope.interface.Interface):
... zip = zope.schema.Int(title=u"Zip Code")
>>> from z3c.form import form, field
>>> from z3c.form.interfaces import IField
>>> from z3c.formjs import jsevent, jsaction
>>> class AddressEditForm(jsvalidator.MessageValidator, form.AddForm):
... fields = field.Fields(IAddress)
...
... @jsaction.handler(IField, event=jsevent.CHANGE)
... def fieldValidator(self, event, selector):
... return self.ValidationScript(self, selector.widget).render()
After instantiating the form, ...
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> edit = AddressEditForm(None, request)
>>> edit.update()
we can execute the handler to ensure we get some output:
>>> from z3c.formjs import testing
>>> testing.setupRenderers()
>>> from z3c.formjs import jsaction
>>> print edit.fieldValidator(
... None, jsaction.WidgetSelector(edit.widgets['zip']), request)
$.get('/validate', function(data){ alert(data) })
Validators use AJAX handlers to communicate with the server. Commonly the AJAX
handler is looked up via the "ajax" view -- see ``ajax.txt`` for more
details. In this case we just create a small helper function:
>>> from z3c.formjs import ajax
>>> def AJAXPlugin(view):
... return ajax.AJAXRequestTraverserPlugin(view, view.request)
>>> from z3c.formjs import ajax, interfaces
>>> from zope.publisher.interfaces.browser import IBrowserRequest
>>> zope.component.provideSubscriptionAdapter(
... ajax.AJAXRequestTraverserPlugin,
... (interfaces.IFormTraverser, IBrowserRequest))
we can traverse to the ``validate`` method from the "ajax" view and render
it. Let's first render some valid input:
>>> request = TestRequest(form={'widget-name' : 'zip',
... 'form.widgets.zip' : u'29132'})
>>> edit = AddressEditForm(None, request)
>>> edit.update()
>>> AJAXPlugin(edit).publishTraverse(None, 'validate')()
u''
As you can see there is no error message. Let's now provide an invalid ZIP
code. As you can see, we get the expected error message:
>>> request = TestRequest(form={'widget-name': 'zip',
... 'form.widgets.zip':'notazipcode'})
>>> edit = AddressEditForm(None, request)
>>> edit.update()
>>> AJAXPlugin(edit).publishTraverse(None, 'validate')()
u'The system could not process the given value.'
Of course, one cannot just traverse to any attribute in the form:
>>> AJAXPlugin(edit).publishTraverse(None, 'ValidationScript')()
Traceback (most recent call last):
...
NotFound: Object: <AddressEditForm ...>, name: 'ValidationScript'
And that's it.
===============================
Connecting to Javascript Events
===============================
The ``jsevent`` module of this package implements a mechanism to connect a
Javascript script to an event of a DOM element. So let's have a look at how
this works.
>>> from z3c.formjs import interfaces, jsevent
To implement this functionality, we need to model three components: events,
DOM elements (selector), and the script (handler). We will also need a manager
to keep track of all the mappings. This is indeed somewhat similar to the Zope
3 event model, though we do not need DOM elements to connect the events there.
Subscription Manager
--------------------
So first we need to create a subscription manager in which to collect the
subscriptions:
>>> manager = jsevent.JSSubscriptions()
Initially, we have no registered events:
>>> list(manager)
[]
We now want to subscribe to the "click" event of a DOM element with the id
"message". When the event occurs, we would like to display a simple "Hello
World" message.
The events are available in all capital letters, for example:
>>> jsevent.CLICK
<JSEvent "click">
The DOM element is selected using a selector, in our case an id selector:
>>> selector = jsevent.IdSelector('message')
>>> selector
<IdSelector "message">
The handler of the event is a callable accepting the event, selector and the
request:
>>> def showHelloWorldAlert(event, selector, request):
... return u'alert("Hello World!")'
We have finally all the pieces together to subscribe the event:
>>> manager.subscribe(jsevent.CLICK, selector, showHelloWorldAlert)
<JSSubscription event=<JSEvent "click">,
selector=<IdSelector "message">,
handler=<function showHelloWorldAlert at ...>>
So now we can see the subscription:
>>> list(manager)
[<JSSubscription event=<JSEvent "click">,
selector=<IdSelector "message">,
handler=<function showHelloWorldAlert at ...>>]
So now, how does this get rendered into Javascript code? Since this package
strictly separates definition from rendering, a renderer will be responsible
to produce the output.
Renderers
---------
So let's define some renderers for the various components. We have already
prepared renderers for testing purposes in the ``testing`` support module. The
first one is for the id selector
>>> import zope.component
>>> from z3c.formjs import testing
>>> zope.component.provideAdapter(testing.IdSelectorRenderer)
Of course, like all view components, the renderer supports the update/render
pattern. We can now render the selector:
>>> from zope.publisher.browser import TestRequest
>>> request = TestRequest()
>>> renderer = zope.component.getMultiAdapter(
... (selector, request), interfaces.IRenderer)
>>> renderer.update()
>>> renderer.render()
u'#message'
Next we need a renderer for the subscription. Let's assume we can bind the
subscription as follows: ``$(<selector>).bind("<event>", <script>)``
>>> zope.component.provideAdapter(testing.SubscriptionRenderer)
Rendering the subscription then returns this:
>>> renderer = zope.component.getMultiAdapter(
... (list(manager)[0], request), interfaces.IRenderer)
>>> renderer.update()
>>> print renderer.render()
$("#message").bind("click", function(){alert("Hello World!")});
And now to the grant finale. We create a renderer for the subscription manager.
>>> zope.component.provideAdapter(testing.ManagerRenderer)
Let's now render the entire manager.
>>> renderer = zope.component.getMultiAdapter(
... (manager, request), interfaces.IRenderer)
>>> renderer.update()
>>> print renderer.render()
$(document).ready(function(){
$("#message").bind("click", function(){alert("Hello World!")});
})
The Subscription Decorator
--------------------------
When defining JS event subscriptions from within a presentation component,
using the low-level subscription API is somewhat cumbersome. Thus, there
exists a decorator called ``subscribe``, which can convert a component method
as a subscription handler. Let's have a look:
>>> class MyView(object):
...
... @jsevent.subscribe(jsevent.IdSelector('myid'), jsevent.DBLCLICK)
... def alertUser(event, selector, request):
... return u"alert('`%s` event occured on DOM element `%s`');" %(
... event.name, selector.id)
As you can see, the function is never really meant to be a method, but a
subscription handler; thus no ``self`` as first argument. The subscription is
now available in the subscriptions manager of the view:
>>> list(MyView.jsSubscriptions)
[<JSSubscription event=<JSEvent "dblclick">,
selector=<IdSelector "myid">,
handler=<function alertUser at ...>>]
Let's now render the subscription:
>>> renderer = zope.component.getMultiAdapter(
... (list(MyView.jsSubscriptions)[0], request), interfaces.IRenderer)
>>> renderer.update()
>>> print renderer.render()
$("#myid").bind("dblclick",
function(){alert('`dblclick` event occured on DOM element `myid`');});
Javascript Viewlet
------------------
Putting in the Javascript by hand in every layout is a bit lame. Instead we
can just register a viewlet for the JS viewlet manager that renders the
subscriptions if a manager is found.
To use the viewlet we need a view that provides a subscription manager:
>>> class View(object):
... zope.interface.implements(interfaces.IHaveJSSubscriptions)
... jsSubscriptions = manager
We can now initialize, update, and finally render the viewlet:
>>> viewlet = jsevent.JSSubscriptionsViewlet(
... object(), request, View(), object())
>>> viewlet.update()
>>> print viewlet.render()
<script type="text/javascript">
$(document).ready(function(){
$("#message").bind("click", function(){alert("Hello World!")});
})
</script>
Selectors
---------
The module provides several DOM element selectors. It is the responsibility of
the corresponding rednerer to interpret the selector.
Id Selector
~~~~~~~~~~~
The id selector selects a DOM element by id, as seen above. It is simply
initialized using the the id:
>>> idselect = jsevent.IdSelector('myid')
>>> idselect
<IdSelector "myid">
The id is also available as attribute:
>>> idselect.id
'myid'
We already saw before how it gets rendered:
>>> renderer = zope.component.getMultiAdapter(
... (idselect, request), interfaces.IRenderer)
>>> renderer.update()
>>> renderer.render()
u'#myid'
CSS Selector
~~~~~~~~~~~~
The CSS selector selects a DOM element using an arbitrary CSS selector
expression. This selector is initialized using the expression:
>>> cssselect = jsevent.CSSSelector('div.myclass')
>>> cssselect
<CSSSelector "div.myclass">
The CSS selector expression is also available as attribute:
>>> cssselect.expr
'div.myclass'
Let's now see an example on how the CSS selector can be rendered:
>>> zope.component.provideAdapter(testing.CSSSelectorRenderer)
>>> renderer = zope.component.getMultiAdapter(
... (cssselect, request), interfaces.IRenderer)
>>> renderer.update()
>>> renderer.render()
u'div.myclass'
Since most JS libraries support CSS selectors by default, the renderer simply
converts the expression to unicode.
Available Events
----------------
This package maps all of the available JavaScript events. Here is the complete
list:
>>> jsevent.CLICK
<JSEvent "click">
>>> jsevent.DBLCLICK
<JSEvent "dblclick">
>>> jsevent.CHANGE
<JSEvent "change">
>>> jsevent.LOAD
<JSEvent "load">
>>> jsevent.BLUR
<JSEvent "blur">
>>> jsevent.FOCUS
<JSEvent "focus">
>>> jsevent.KEYDOWN
<JSEvent "keydown">
>>> jsevent.KEYUP
<JSEvent "keyup">
>>> jsevent.MOUSEDOWN
<JSEvent "mousedown">
>>> jsevent.MOUSEMOVE
<JSEvent "mousemove">
>>> jsevent.MOUSEOUT
<JSEvent "mouseout">
>>> jsevent.MOUSEOVER
<JSEvent "mouseover">
>>> jsevent.MOUSEUP
<JSEvent "mouseup">
>>> jsevent.RESIZE
<JSEvent "resize">
>>> jsevent.SELECT
<JSEvent "select">
>>> jsevent.SUBMIT
<JSEvent "submit">
These are also provided as utilities so they can be looked up by name.
>>> import zope.component
>>> zope.component.provideUtility(jsevent.CLICK, name='click')
Of course, we can now just look up the utility:
>>> zope.component.getUtility(interfaces.IJSEvent, 'click')
<JSEvent "click">
=======
CHANGES
=======
Version 0.2.0 (7/18/2007)
--------------------------
- Feature: Registration of public AJAX server calls via a simple
decorator. The calls are made available via a special ``ajax`` view on the
original view.
- Feature: Allow registering of JS subscriptions via a decorator within the
presentation component.
- Feature: Added a new CSS selector.
- Feature: Implementation of AJAX-driven widget value validation.
- Restructure: Completely overhauled the entire API to be most easy to use and
have the most minimal implementation.
- Bug: The package is now 100% tested.
- Feature: Implementation of AJAX request handlers in forms.
Version 0.1.0 (6/29/2007)
-------------------------
- Initial Release
* Feature: JS event association with fields and buttons.
====
TODO
====
- A way to write a javascript function in python such that you can
render a call to it from python
class SomeForm:
@jsfunction('bar') # bar is the namespace
def foo(self):
return 'alert("foo");'
renderJSFunction(SomeForm.foo)
'function z3c_formjs_foo(){ alert("foo"); }'
renderJSCall(SomeForm.foo)
'bar_foo()'
- ajax form submission - ala "save" button
- ajax widget switching
- client side js validators for simple fields. (maybe we can use an
existing library?)
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
z3c.formjs-0.2.0.tar.gz
(44.9 kB
view hashes)
Built Distribution
z3c.formjs-0.2.0-py2.4.egg
(59.8 kB
view hashes)