Personal tools
You are here: Home   Additional documentation   Tutorials   Accessing DOM information  
 
Document Actions

Accessing DOM information

by Balazs Ree last modified 2007-11-20 14:01

In this lesson we will learn how we can access information from our page and pass it to the server actions. We will also familiarize ourselves with the most common errors we encounter with parameters passing, and learn some useful debugging tricks.

The tutorial has been updated for kss.core v1.2. Updated version

In our previous lessons we learned how to use KSS to invoke a server action and how to pass different parameters to them. To start with, we will further improve the code that we used in the previous examples.  You can import this code from the ZMI: go to your portal_skins, delete or remove the custom folder, and import the code. If you have just finished the previous lesson,  you do not need to do this. Also remember to disable the kss files already registered from the installed products and add your new kss file into portal_css tool, as described earlier.

Preface

Or, why is this important at all?

We have the following rule in our kss resource:

a.navTreeCurrentItem:click { 
    evt-click-preventdefault: True;
    action-server: response1;
    response1-mymessage: "Hello Plone!";
}

This rule, combined with the response1 method, writes a message in the upper right corner of the page, if in the navigation portlet we click to the line representing our current folder location. (For the portal home page, this will be the "Home" line.)

This works fine. We also learned how we can invoke the same server action with different parameters, resulting in different display texts in the result. Now we would like to bind the rule to all the navtree items. After having looked in the FireBug inspector we can see that the corresponding css selector will be "li.navTreeItem a". Change the rule accordingly. (Do not make a duplicate for reasons to be described later.)

li.navTreeItem a:click { 
    evt-click-preventdefault: True;
    action-server: response1;
    response1-mymessage: "Hello Plone!";
}

Save the rules and reload your page. If done right, you can see in the loggingpane that the rule is now correctly binding to the required number of nodes (for example, I have 4 folders appearing in the navtree):

DEBUG: EventRule #0 mergeid @@0@@click selected 4 nodes

Also you can check that clicking of any of the navtree items will activate the replacement.

Now, I would like to see in the result, which line I clicked on. However I would then need to pass different parameters depending on the node. If I programmed in javascript, I would look up the correct information from the DOM. How can I do this with KSS? Obviously making 4 different eventrule is not an option, as the point is exactly to have one matching rule.

The solution is using parameter producer functions that can be specified instead of the strings. The functions may take a number of parameters, and based on this they look up some information from the page. This results in dynamic parameter values passed to the remote request. As an example. consider we want to pass the href attribute of the clicked node. Then we modify the rule as follows:

li.navTreeItem a:click { 
    evt-click-preventdefault: True;
    action-server: response1;
    response1-mymessage: nodeAttr("href");
}

Reload the page to see the effect!

Overview of the builtin parameter producers

The kukit core provides all the parameter producers that areuseful to cover most of the use cases. In addition to the parameters you must supply to them, they operate on the node selected by the event. The most important ones are:

For fetching content:

Our first objective is to be able to access the html content of the node, most of all the html attributes.

  • nodeAttr(attrname): Produces the value of a given html attribute of the selected node.
  • nodeContent(): This produces the textual content of the node. We do not encourage using this, as we should use these functions for identifying the node on which the event happened, however it may come handy for debugging or for some cases - like in our following example.
  • kssAttr(attrname): This is for reading (namespace) attributes holding information for kss only, and otherwise irrelevant for the html content. We will talk about this later.

For fetching form data:

The second important task is to fetch form values from the page. The following functions fetch a single form variable:

  • formVar(formname, varname):  Produces the value of a given variable within a given form.
  • currentFormVar(varname): Produces the value of a given variable within the current form, which is the one in which the selected node is. The parameter varname is optional, and if it is ommitted, the current node will be used (in this case the node must be a form variable itself).

Functions to fetch all variables of a form at once:

  • form(formname):  Produces the values of all the variables in a given form.
  • currentForm(): Produces the values of all the variables in the form that contains the current node.

The latter ones will pass a dictionary of values to the server action. The usage of all the other producer functions will result in passing a string.

For a complete and updated list, you can check the cheatsheet. All these commands are implemented in the kukit core and the rule is to keep them simple: if you need something more complicated, then a plugin extension will be needed. However the current use cases are all covered by the existing set.

Combining more parameters, and encoding

Let us have some more useful information displayed as a result. First let us pass 3 parameters: a string (like earlier), the current nodeAttr function, combined with nodeValue that yields the textual value of the node, in this case the visible title of the navtree item.

li.navTreeItem a:click { 
    evt-click-preventdefault: True;
    action-server: response1;
    response1-mymessage: 'clicked on navtree item';
    response1-href:      nodeAttr("href");
    response1-value:     nodeContent();
}

We also need to change the response1 python scrript to accomodate the new parameters:

# import Through-The-Web(TTW) API
from kss.core.ttwapi import startKSSCommands
from kss.core.ttwapi import getKSSCommandSet
from kss.core.ttwapi import renderKSSCommands

# start a view for commands
startKSSCommands(context, context.REQUEST)

# use the parameters
txt = '<h1>We did it!</h1><span><b>%s</b> at %s</span>' % (mymessage, DateTime())
txt += '<br><span>href=<b>"%s"</b>' % (href, )
txt += '<br><span>value=<b>"%s"</b>' % (value, )

# add a command
core = getKSSCommandSet('core')
core.replaceInnerHTML('#portal-siteactions', txt)

# render the commands
return renderKSSCommands()

We also need to change the parameter list of the string to: "mymessage, href, value" .

Let's click on the different lines in the navtree and enjoy the results. However I am using an internationalized site, and when I click on any line where there are non-ascii characters in the folder name, I receive an error in the loggingpane. The client cannot tell us what happened, so we look into the Zope event log and we see: (XXX put full traceback here)

AzaxUnicodeError: Content must be unicode or ascii string, original exception: 'ascii' codec can't decode byte 0xc5 in position 193: ordinal not in range(128)

The explanation is this. When we build up the commands, the basic rule is that the supplied command parameters must be either unicode or plain ascii. We do not care autoconverting anything else: azax has no information of what encoding is used in a string, and we have learned by now that magic can do more harm than help. So we must do explicit conversion of the parameters:

from kss.core import force_unicode
value = force_unicode(value, 'utf')

The force_unicode method converts the input to unicode, from the encoding given as a second parameter. Why did we use utf? Actually kss is handling charsets properly on the client side, and passes the parameters with utf encoding. The ZPublisher of Zope2 is dumb enough to marshall this to parameters as utf-8 encoded (non-unicode) strings. Hence the need for the conversion. On the other hand the twisted publisher of Zope3 is passing proper unicode values. So, if the force_unicode method meets an unicode, it just lets it pass. This is a way of thinking about future compatibility: we target to write code usable on Zope3 as is.

Notice that to have the server side change in effect there was no need to reload the page in the browser! This is one convenience: the browser needs a reload only if we made changes in the client side code, in this case, the kss.

Missing parameters

This works fine now. But we used to have a second kss rule too (inherited from the previous lesson):

ul#portal-globalnav li a:click {
    evt-click-preventdefault: True;
    action-server: response1;
    response1-mymessage: "clicked on global nav";
}

This comes into effect when we click on the global navigation tabs.

But now, this gives an error in the loggingpane. Turning to the Zope log we see a traceback:

2006-10-18 16:02:44 ERROR Zope.SiteErrorLog http://localhost:9777/testing/front-page/response1
Traceback (innermost last):
  Module ZPublisher.Publish, line 115, in publish
  Module ZPublisher.mapply, line 83, in mapply
  Module ZPublisher.Publish, line 46, in missing_name
  Module ZPublisher.HTTPResponse, line 683, in badRequestError
BadRequest:   <h2>Site Error</h2>
  <p>An error was encountered while publishing this resource.
  </p>
  <p><strong>Invalid request</strong></p>


  The parameter, <em>href</em>, was omitted from the request.<p>Make sure to specify all required parameters, and
try the request again.</p>
  <hr noshade="noshade"/>

What happened here? One of the mandatory parameters were missing for the response1 method. solution: we need to change the list of parameters to "mymessage, href=None, value=None". In addition we need to make the call to force_unicode conditional, and then for niceity, also make the text output better.

...
if value is not None:
    value = force_unicode(value, 'utf')

txt = '<h1>We did it!</h1><span><b>%s</b> at %s</span>' % (mymessage, DateTime())
if href is not None:
    txt += '<br><span>href=<b>"%s"</b>' % (href, )
if value is not None:
    txt += '<br><span>value=<b>"%s"</b>' % (value, )
...

When we are at debugging, let's look at some other important cases as well.

Nonexistent node attributes

What would happen if nodeAttr wants to acquire an attribute that does not exist on the node? To test that, change corresponding line in kss to

   response1-href:      nodeAttr("hrefXXX");

If we test it (this time don't miss to reload the page in the browser), it works well and we see the href informational line omitted from the output. This means that if there is a nonexistent value to be passed, the parameter will be missing from the request.

If we had not put the None default value to the href parameter in the script, we would have received again the "parameter missing" error in Zope. Lucky we were smart in advance... In fact this is an important issue. The reason for the implementation is, that we do not have a representation of the value "None" that is passable to the ZPublisher as a method. So we have chosen to omit the value in this case, but this makes the default value for the parameter inevitable.

As a second annoyance, currently we have no cross-browser compatible implementation that could distinguish between a nonexistent attribure and an attribute that equals to an empty string. So we take the latter case to be the same as a nonexistent attribute.

We might polish this implementation in the future.

Before continuing, don't forget to change back the kss line as used to be originally.

No error in zope event log

Sometimes it may happen that we see a request failed error in the loggingpane, but we see nothing in the Zope event log. One way to simulate this easily is to call a server action that does not have a corresponding server method. This would give NotFound and would not be logged by default. One way to try this is to temporarily remove the response1 method and see what happens. We could of course enable logging for this exceptions too, but we have an alternate way that gives us more control.

This is the time to look at the actual communication between client and server. There are two ways for this: use the firebug extension of FireFox with the show XMLHttpRequests option switched on, or use a proxy application like tcpwatch of zope3 that can be used to tap the full client-server communication. It is advisable to get acquainted with both methods but for its simplicity we use firebug now.

If we look into the response of the failing request we can see that instead of the kukit command response we received a normal Plone error page. Inside the long long page we will see the actual error message.

Congratulations! You can move to the next part  at this point, or optinally, have a look at the next chapter.

Doing it the Zope3 way (optional)

If you are familiar with Zope3 style development, you can easily transform the code into a method in a browserview yourself. Enter the file demoview.py into the product root in the filesystem:

from kss.core import KSSView
from kss.core import force_unicode
from datetime import datetime

class DemoView(KSSView):

    def response1(self, mymessage, href=None, value=None):

if value is not None:
value = force_unicode(value, 'utf')

txt = '<h1>We did it!</h1><span><b>%s</b> at %s</span>' % (mymessage, DateTime())
if href is not None:
txt += '<br><span>href=<b>"%s"</b>' % (href, )
if value is not None:
txt += '<br><span>value=<b>"%s"</b>' % (value, )

# KSS specific calls
core = self.getCommandSet('core')
core.replaceInnerHTML('#portal-siteactions', txt)
return self.render()

As usual,  you also need to add the following to your configure.zcml:

 <browser:page
      for="plone.app.kss.interfaces.IPortalObject"
      class=".demoview.DemoView"
      attribute="response1"
      name="response1"
      permission="zope2.View"
      />

In the next tutorial lesson we look at some more usage of parameter producer functions.