[Checkins] SVN: z3c.formjs/trunk/src/z3c/formjs/ Worked on properly testing the ``jsaction`` module.

Stephan Richter srichter at cosmos.phy.tufts.edu
Sat Jul 7 22:28:45 EDT 2007


Log message for revision 77600:
  Worked on properly testing the ``jsaction`` module.
  

Changed:
  U   z3c.formjs/trunk/src/z3c/formjs/interfaces.py
  U   z3c.formjs/trunk/src/z3c/formjs/jsaction.py
  U   z3c.formjs/trunk/src/z3c/formjs/jsaction.txt

-=-
Modified: z3c.formjs/trunk/src/z3c/formjs/interfaces.py
===================================================================
--- z3c.formjs/trunk/src/z3c/formjs/interfaces.py	2007-07-08 01:26:38 UTC (rev 77599)
+++ z3c.formjs/trunk/src/z3c/formjs/interfaces.py	2007-07-08 02:28:45 UTC (rev 77600)
@@ -121,17 +121,50 @@
     """A button that just connects to javascript handlers."""
 
 
-class IJSEventHandler(IButtonHandler):
-    """A button handler for javascript buttons."""
+class IJSEventHandler(zope.interface.Interface):
+    """An action handler of Javascript events for fields and buttons."""
 
-    def __call__(selector):
-        """call the handler, passing it the form."""
+    def __call__(event, selector, request):
+        """Call the handler.
 
+        The result should be a string containing the script to be executed
+        when the event occurs on the specified DOM element.
 
+        The event *must* provide the ``IJSEvent`` interface. The selector
+        *must* be an ``IWidgetSelector`` component.
+        """
+
+
 class IJSEventHandlers(zope.interface.Interface):
-    pass
+    """Javascript event handlers for fields and buttons."""
 
+    def addHandler(field, event, handler):
+        """Add a new handler for the fiven field and event specification.
 
+        The ``field`` and ``event`` arguments can either be instances, classes
+        or specifications/interfaces. The handler *must* provide the
+        ``IJSEventHandler`` interface.
+        """
+
+    def getHandlers(field):
+        """Get a list of (event, handler) pairs for the given field.
+
+        The event is a component providing the ``IJSEvent`` interface, and the
+        handler is the same component that has been added before.
+        """
+
+    def copy():
+        """Copy this object and return the copy."""
+
+    def __add__(other):
+        """Add another handlers object.
+
+        During the process a copy of the current handlers object should be
+        created and the other one is added to the copy. The return value is
+        the copy.
+        """
+
+
 # -----[ Validator ]--------------------------------------------------------
 
 

Modified: z3c.formjs/trunk/src/z3c/formjs/jsaction.py
===================================================================
--- z3c.formjs/trunk/src/z3c/formjs/jsaction.py	2007-07-08 01:26:38 UTC (rev 77599)
+++ z3c.formjs/trunk/src/z3c/formjs/jsaction.py	2007-07-08 02:28:45 UTC (rev 77600)
@@ -70,7 +70,13 @@
 
 
 class JSHandlers(object):
-    """Javascript event handlers for fields and buttons."""
+    """Advanced Javascript event handlers for fields and buttons.
+
+    This implementation of the Javascript event handlers interface used an
+    adapter registry to manage more general versus more specific handler
+    registrations. When asked for handlers, the registry will always return
+    the most specific one for each event.
+    """
     zope.interface.implements(interfaces.IJSEventHandlers)
 
     def __init__(self):
@@ -78,7 +84,7 @@
         self._handlers = ()
 
     def addHandler(self, field, event, handler):
-        """See interfaces.IEventHandlers"""
+        """See interfaces.IJSEventHandlers"""
         # Create a specification for the field and event
         fieldSpec = util.getSpecification(field)
         eventSpec = util.getSpecification(event)
@@ -92,7 +98,7 @@
         self._handlers += ((field, event, handler),)
 
     def getHandlers(self, field):
-        """See interfaces.IButtonHandlers"""
+        """See interfaces.IJSEventHandlers"""
         fieldProvided = zope.interface.providedBy(field)
         handlers = ()
         for event in jsevent.EVENTS:
@@ -104,25 +110,24 @@
         return handlers
 
     def copy(self):
-        """See interfaces.IButtonHandlers"""
-        handlers = Handlers()
-        for button, handler in self._handlers:
-            handlers.addHandler(button, handler)
+        """See interfaces.IJSEventHandlers"""
+        handlers = JSHandlers()
+        for field, event, handler in self._handlers:
+            handlers.addHandler(field, event, handler)
         return handlers
 
     def __add__(self, other):
-        """See interfaces.IButtonHandlers"""
-        if not isinstance(other, Handlers):
+        """See interfaces.IJSEventHandlers"""
+        if not isinstance(other, JSHandlers):
             raise NotImplementedError
         handlers = self.copy()
-        for button, handler in other._handlers:
-            handlers.addHandler(button, handler)
+        for field, event, handler in other._handlers:
+            handlers.addHandler(field, event, handler)
         return handlers
 
     def __repr__(self):
-        return '<JSHandlers %r>' % (
-            self.__class__.__name__,
-            [handler for button, handler in self._handlers])
+        return '<%s %r>' % (self.__class__.__name__,
+                            [handler for f, e, handler in self._handlers])
 
 
 class JSHandler(object):

Modified: z3c.formjs/trunk/src/z3c/formjs/jsaction.txt
===================================================================
--- z3c.formjs/trunk/src/z3c/formjs/jsaction.txt	2007-07-08 01:26:38 UTC (rev 77599)
+++ z3c.formjs/trunk/src/z3c/formjs/jsaction.txt	2007-07-08 02:28:45 UTC (rev 77600)
@@ -2,7 +2,7 @@
 Javascript Events for Buttons
 =============================
 
-In the ``z3c.form`` package buttons are most commonly rendered as "submit"
+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
@@ -16,7 +16,7 @@
 
 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:
+schema describing them; so let's do that now:
 
   >>> import zope.interface
   >>> class IButtons(zope.interface.Interface):
@@ -25,14 +25,17 @@
 
 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.
+will later be rendered differently. (Basically, ``JSButton`` fields render as
+button widgets.)
 
 
 Widget Selector
 ---------------
 
-The widget selector, in contrast to the id selector, accepts a widget and
-provides a selector API.
+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()
@@ -40,13 +43,14 @@
   >>> 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:
+selector (see ``jsevent.txt``):
 
   >>> from z3c.formjs import interfaces
   >>> interfaces.IIdSelector.providedBy(selector)
@@ -54,13 +58,24 @@
   >>> selector.id
   'form-msg'
 
-This has the advantage that we do not need another renderer for this
-selector. Thus, within a form we can use the following pattern to subscribe a
-widget to an event:
+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
 
@@ -75,19 +90,20 @@
   >>> form = Form()
   >>> form.update()
 
-After registering the renderers,
+After registering the subscription-related renderers,
 
   >>> from z3c.formjs import testing
   >>> testing.setupRenderers()
 
-we can use the viewlet to check the subscription output:
+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 None>");});
+      $("#form-msg").bind("click", function(){alert("<TextWidget 'msg'>");});
     })
   </script>
 
@@ -118,8 +134,8 @@
 element selector) and the event to which to bind the action. By default the
 event is ``jsevent.CLICK``.
 
-And that's really everything that is required from a user's point of
-view. Let's now see how those handler declarations are converted into actions
+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
@@ -153,8 +169,9 @@
   >>> actions['hello']
   <JSButtonAction 'form.buttons.hello' u'Hello World!'>
 
-Since these are special Javascript actions, updating them has also caused the
-form to become an ``IHaveJSSubscriptions`` view:
+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
 
@@ -169,20 +186,21 @@
   >>> selector
   <WidgetSelector "form-buttons-hello">
 
-This special action selector is a derivative of the id selector and keeps the
-action.
+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
-completly.
+completely.
 
   >>> demoform = Form(None, request)
 
@@ -205,8 +223,9 @@
   ...     IPageTemplate, name=INPUT_MODE)
 
 We also need to setup a Javascript viewlet manager and register the
-subscription viewlet for it. (This is a bit tedious to do using the Python
-API, but using ZCML this is much simpler.)
+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:
 
@@ -272,7 +291,11 @@
     </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
 -----------------
 
@@ -308,8 +331,9 @@
        selector=<WidgetSelector "form-buttons-hello">,
        handler=<JSHandler <function showDoubleHelloWorldMessage ...>>>]
 
-Let's now look at a case where one handler is registered for all buttons and
-events, and another that overrides the click of hello to something else:
+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):
@@ -326,7 +350,7 @@
   >>> demoform = Form(None, request)
   >>> demoform.update()
 
-Let's now have a look at the rendered subscriptions:
+Rendering the subscriptions gives the following result:
 
   >>> renderer = zope.component.getMultiAdapter(
   ...     (demoform.jsSubscriptions, request), interfaces.IRenderer)
@@ -373,13 +397,6 @@
 due to the built-in adapter registry of the ``JSHandlers`` class.
 
 
-
-Submit and Javascript Buttons Together
---------------------------------------
-
-XXX: to be done
-
-
 Attaching Events to Form Fields
 -------------------------------
 
@@ -392,8 +409,8 @@
   ...     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 changed, we would like to get an alert:
+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)
@@ -406,7 +423,7 @@
   ...     def nameChangeEvent(self, event, selector):
   ...         return 'alert("The Name was Changed!");'
 
-We also need to register all the default form registrations:
+We also need to register all of the default ``z3c.form`` registrations:
 
   >>> from z3c.form.testing import setupFormDefaults
   >>> setupFormDefaults()
@@ -447,3 +464,168 @@
       </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.
+



More information about the Checkins mailing list