Begin with KSS, part 2

ree
.
This tutorial is the direct continuation of the first part. We will learn how to specify server side actions and finally complete our first useful AJAX example. Creating a server action
Until now, we called a client action (alert) from our event. This was good to test if our event was triggered. However, in a typical AJAX pattern, we want to call a server action.
Server actions are implemented as methods on the server. They could be any kind of callable methods, including python scripts, and this is what we will do first. Besides doing whatever is necessary for the business logic, the task of the server method is to assemble a sequence of commands. These commands are marshalled back to the client to be executed there. A command is a call to DOM manipulation client code. A command can (in most cases, should) have a selector, to set the scope of nodes on which the command is executed, and a set of parameters for execution.
Although existing components come with implemented server action methods, it is easy to create a custom one since it requires only python skills. Let's create a python script (response1) in the custom skin with the following content:
# 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) # add a command core = getKSSCommandSet('core') core.replaceInnerHTML('#portal-siteactions', '<h1>We did it!</h1>') # render the commands return renderKSSCommands()
After the imports, we initialize the view that will hold the KSS commands that we want to send back to the client.
Then we add a command. The name of the command is replaceInnerHTML. This is one of our most useful commands : it simply replaces the contents of the selected html nodes with some html string.
To specify which nodes will be selected, the command also needs a selector: in this example, a standard CSS selector. We choose to replace the portal actions of a Plone portal that are on the top of the page - but we could choose any other element as well.
The replaceInnerHTML method is accessed through a command set. Since we have a pluggable system, we need to refer to the component that defines the methods, in this case, the 'core' command set.
In the last line, the renderKSSCommands call is mandatory : it will generate the response payload from the accumulated commands. To look at this payload, let's access this method directly from the browser: http://localhost:8080/plone/front-page/response1. We will see We did it! on the screen, but let's have a more careful look at the source of the response:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:kukit="http://www.kukit.org/commands/1.0"><body> <kukit:commands> <kukit:command selector="#portal-siteactions" name="replaceInnerHTML" selectorType=""> <kukit:param name="html"><h1>We did it!</h1></kukit:param> </kukit:command> </kukit:commands> </body></html>
Note:
In KSS version 1.4, the payload looks slightly different, but structurally it is the same.
This is an XML response, where we can see how commands and parameters are actually marshalled. When the response is interpreted by the KSS engine, it will execute the commands with the given parameters.
Calling the server action
Now, we have finished to build our server action; we want to call it from our kss style sheet. We replace our previous KSS event rule with the following one:
a.navTreeCurrentItem:click { evt-click-preventdefault: True; action-server: response1; }
The action-server line specifies the name of the remote method to call: response1 (since this is how we named our python script). The script will be called on the context url of the page we are at.
Let's reload the page so that the new kss comes into effect. Open the loggingpane. Then press the "Home" line in the navtree portlet. It works! We can see the site actions replaced with our text. Also notice that a few things have been logged to the kss console:
RequestManager notifies server http://127.0.0.1:8080/demo11/folder1/page1/response1, rid=0 (RQ: 1 OUT, 0 WAI). POST http://127.0.0.1:8080/demo11/folder1/page1/response1?kukitTimeStamp=1191846467877 (109ms) ... RequestManager received result with rid [0] (RQ: 0 OUT, 0 WAI). Parsing commands. Number of commands : 1. Selector type [default (css)], selector [#portal-siteactions], selected nodes [1]. [replaceInnerHTML] execution.++resource++kukit... 1 nodes inserted.++resource++kukit... ...
This gives a lot of information about what happened in the client:
- the server is notified,
- the response is received,
- it is parsed successfully,
- it contains one command,
- the command selects 1 node to act on.
Now let's change our command response in the following way:
... from DateTime import DateTime # add a command core = getKSSCommandSet('core') content = '<h1>We did it!</h1><span>%s</span>' % DateTime() core.replaceInnerHTML('#portal-siteactions', content) ...
This way, the current time is sent back by the server on each click and we can see that something happens.
It is interesting to note that we did not need to reload the page in order to see the effect of this change. Because we only made changes on the server, we did not need to load anything new on the client side. So we can continue to debug from the already loaded page and this will work even through server restarts.
What happens if the server-side script has an error, or the client does not get a correct response for some reason? For example, changing DateTime to DateTimeX in our code, results in the following:
- Request failed at url http://127.0.0.1:8080/demo11/folder1/page1/response1,
- rid=1, server_reason="ImportError: import of "DateTimeX" from "DateTime" is unauthorized. The container has no security assertions. Access to 'DateTimeX' of (module 'DateTime' from '/usr/local/lib/Zope210/lib/python/DateTime/__init__.pyc') denied."
The error Request failed indicates that we have to turn to the server to debug the problem. Our best friend, the zope error log will tell us about the actual problem. However in this case KSS also logged us the server side error message.
Server action parameters
Like client actions, server actions can also accept parameters. The parameters will be sent to the server as form variables. Zope publisher can then pass them as usual keyword parameters to our python script. Let's render a parameter coming from the client. We add parameter mymessage to the python script. Then modify the script:
... # add a command core = getKSSCommandSet('core') content = '<h1>We did it!</h1><span><b>%s</b> at %s</span>' % (mymessage, DateTime()) core.replaceInnerHTML('#portal-siteactions', content) ...
We modify our kss rule to actually send the parameter from the client:
a.navTreeCurrentItem:click { evt-click-preventdefault: True; action-server: response1; response1-mymessage: "Hello Plone!"; }
The key response1-mymessage is built identically to how we did it with the client action. We use the name of the action first and then, following the dash, the name of the parameter. This time, because we change the stylesheet, we need to reload the page before testing by clicking on the bound node.
To understand better how all this is working, let's enter a second rule in the kss:
ul#portal-globalnav li a:click { evt-click-preventdefault: True; action-server: response1; response1-mymessage: "clicked on global nav"; }
This shows some new things. First, you can see that you can use any css selector in a rule. In this case, the selector will select all globalnav tab links. If you reload the page, you will notice that if you click on any of those links, different content is replaced because different parameter are passed to the server.
If you take a look at the log after the page reload, you can see something like this:
Loading KSS engine. ... Initializing rule sheets. Count of KSS links: 1 Start loading and processing http://127.0.0.1:8080/demo11/portal_kss/Plone%20Default/tutorial1.kss of type kss GET http://127.0.0.1:8080/demo11/portal_kss/Plone%20Default/tutorial1.kss (22ms) EventRule #0: a.navTreeCurrentItem EVENT=click EventRule #1: ul#portal-globalnav li a EVENT=click Finished loading and processing http://127.0.0.1:8080/demo11/portal_kss/Plone%20Default/tutorial1.kss resource type kss in 33 + 36 ms. Setup of events for document starts. Using original cssQuery. EventRule [#0-@@0@@click] selected 1 nodes. EventRule [#1-@@0@@click] selected 5 nodes. 0 special rules bound in grand total. Instantiating event id [@@0], className [0], namespace [null]. 6 nodes bound in grand total. ... Setup of events for document finished in 52 ms. ...
This shows that the second rule is also in effect now. Moreover, it has selected 5 nodes (or however many globalnav tabs you have). A lot of other information is also logged, it should not worry you at the moment.
Different command selector
Until now, in our command, we used the default css selector. It is possible to use other types of selectors, like a html id selector. Let's modify our command in the following way:
... # add a command core = getKSSCommandSet('core') content = '<h1>We did it!</h1><span><b>%s</b> at %s</span>' % (mymessage, DateTime()) selector = core.getHtmlIdSelector('portal-personaltools') core.replaceInnerHTML(selector, content) ...
What an HTML id selector selects should be obvious. Reload the page and exercise!
Commands can also select multiple nodes:
... # add a command core = getKSSCommandSet('core') content = '<h1>We did it!</h1><span><b>%s</b> at %s</span>' % (mymessage, DateTime()) selector = core.getCssSelector('dt.portletHeader a') core.replaceInnerHTML(selector, content) ...
The CSS selector dt.portletHeader a selects all portlet headers in the page, so the replacement will be executed not on one node but on many nodes (as can also be seen in the logs). Try clicking the Home link in the navtree, or any of the globalnav tabs to see the effect.
You can also add multiple commands: each of them will be executed, in the order they have within the payload.
One thing is important to note. If a command selects no nodes, it is not an error: the behaviour designed in this case is that nothing happens. This is in line with the usual logic of CSS selectors in style sheets. Using a different event
So far we have only used the click event: let's try with another one, timeout. The timeout event does not directly map to a browser event but it is a (conceptual) KSS event. This shows that in KSS anything can be an event and how an event binds itself is freely implementable.
Let's add the following rule to the end of our kss file (altogether we will have 3 rules then):
document:timeout { evt-timeout-delay: 8000; action-server: response1; response1-mymessage: "from timeout"; }
The timeout event implements a recurring timeout tick. It has a delay parameter that specifies the time in milliseconds. In this case, the event will be triggered each 8 seconds over and over again. The event calls the server action that we already have but with a different parameter.
The timeout event does not really need a node as binding scope. This is why we use document instead of a CSS selector as we did until now. This is a special KSS selector that is an extension to css and simply means : bind this event exactly once when the document loads, with a scope of no nodes but the document itself.
If you reload the page you will notice that clicks work as before, however, every 8 seconds, the timeout event will trigger and do a replacement on the required nodes.
There are some more advanced issues that this example opens and we will show more about them in the next tutorials.
Congratulations!
You have completed your first KSS tutorial, learned the basics and now you are able to start some experimentation on your own. Or, just sit back and relax.
Server-side commands - the zope3 way
A python script may not be the most proper implementation of a server method. Plone community is moving towards zope3 style development: the suggested way is to use a browser view (multiadapter). Previously, you have created a demo product, now create a python module demoview.py in the product root directory on the filesystem:
from kss.core import KSSView, kssaction from datetime import datetime class DemoView(KSSView): @kssaction def response1(self, mymessage): # build HTML content = '<h1>We did it!</h1><p><b>%s</b> at %s</p>' date = str(datetime.now()) content = content % (mymessage, date) # KSS specific calls core = self.getCommandSet('core') core.replaceInnerHTML('#portal-siteactions', content)
We inherit our view from KSSView. It further inherits from Five's BrowserView.
It is maybe time to explain how the ttwapi uses those views.
- startKSSCommands does the instantiation of a KSSView.
- getKSSCommandSet is the call equivalent to self.getCommandSet.
- renderKSSCommands calls self.render(), which actually returns the KSS commands payload to the client. The self.render() is implicitly arranged on return of methods decorated with @kssaction. This is why we do not need to finish our method that way.
To be able to use the server side method, you need to add the following to your configure.zcml file:
<browser:page for="plone.app.kss.interfaces.IPortalObject" class=".demoview.DemoView" attribute="response1" name="response1" permission="zope2.View" />
The interface that the view is bound to is one setup by kss.core on all portal objects. You could also use directly the interfaces defined by Plone 2.5 directly if you wanted, however the advantage of using the marker interface defined by plone.app.kss.interfaces, like in the above example, is that the same code will work in older Plone versions as well.
If you still have the response1 python script from the begin of this tutorial, do not forget to rename it. Now it is time to restart Zope. If everything goes well, the page functions as previously but you can see from the replacement message that the new method is operating on your page.
Remember, when you are working with browser views, you must restart Zope each time you want to test the changes made in the method code come.