Accessing DOM information, part 2

ree
In the previous lesson we learned how to use parameter producer functions with KSS, and faced with the most common issues in dealing with parameters on the server side. We continue where we finished. Merging (or cascading) rules again
Originally we had a single kss rule matching the current node in the navtree only. Now we have a rule that matches all navtree nodes. Would there be a way to have both ? Consider entering a second kss rules after the current one:
li.navTreeItem a:click { evt-click-preventdefault: True; action-server: response1; response1-mymessage: 'clicked on navtree item'; response1-href: nodeAttr("href"); response1-value: nodeContent(); } a.navTreeCurrentItem:click { response1-mymessage: 'clicked on the CURRENT navtree item'; }
After reloading the page we see this section in the kss console logs:
... 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 (36ms) EventRule #0: li.navTreeItem a EVENT=click EventRule #1: a.navTreeCurrentItem EVENT=click EventRule #2: 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 667 + 80 ms. Setup of events for document starts. Using original cssQuery. EventRule [#0-@@0@@click] selected 2 nodes. Merged rule [0,1-@@0@@click]. EventRule [#1-@@0@@click] selected 1 nodes. EventRule [#2-@@0@@click] selected 5 nodes. 0 special rules bound in grand total. Instantiating event id [@@0], className [0], namespace [null]. 7 nodes bound in grand total. Setup of events for document finished in 69 ms. ...
What happens here? Eventrule #0 matches up with 2 nodes as previously. Eventrule #1 matches up with 1 node - however this node is one of the four nodes rule #0 has matched to. What happens? Rule #1 would not make much sense in itself, but in this case the two rules will merge on the common node. We can also see from the log that this happens.
The result will be as expected (for those that are familiar with CSS cascading): if we click on the current navtree node, the text from rule #1 will be displayed on the screen.
There are a few important things to notice.
- Rule lines are merged similarly to css.
- Merging is not done based on the selectors but on the nodes that the selectors actually select. So, merging depends on the content on which the rules are matched.
- Order is important: consequent rules override the earlier rules.
- Different eventnames do not match, for example a :click event and a :timeout event will not be merged.
- Actions with different names will be added after each other (as an example, place an alert in rule #1), actions with the same name will merge to one action with all their parameters merged.
KSS attributes (HTML markup)
As the next task, we want to extend our navtree so that we can also access the UID of the folder. The first thing that comes to our mind is that we could use another node attribute to hold this information. However, there is a problem. We could use the id attribute of the node but we may want to keep that for different purposes (besided, a node can have only a single id.) We could use a different attribute but using an attribute unknown to HTML would result in an invalid HTML content, even if the browser would not shout against it. So we want to be able to attach attributes to the nodes when we generate the template, and we want them to be accessible from KSS, but we want it to be invisible for HTML.
As a solution, namespace attributes could be used. We could make attributes like kukit:attrname="attrvalue". Unfortunately we cannot use this in Plone. Because Plone uses transitional XHTML at the moment and this would only be valid in real XHTML with mimetype text/xhtml.
To explore this feature, let us create a customized version of the portlet_navtree_macro. In this case, do not copy the template code from here, but make the necessary changes on your customized version instead.
We will path the information "Is this item the current item?" to our server action. That is already available at the desired point of the template in a variable called is_current.
The changes need to be done on the filesystem, because plone portlets cannot be customized through the ZMI in Plone 3. Edit the file plone/app/portlets/portlets/navigation_recurse.pt in lib/python of your instance:
... <div tal:define="item_class python:is_current and item_class + ' navTreeCurrentItem' or item_class"> <a tal:attributes="href python:link_remote and remote_url or item_url; title node/Description; class string:$item_class kssattr-iscurrent-${is_current}"> <img tal:replace="structure item_icon/html_tag" /> <span tal:replace="node/Title">Selected Item Title</span> </a> </div> ...
Note:
You may need to restart Zope after you have made changes on the filesytem, to have your changes rendered.
Look at the emphasized part. What we do is that instead of using real namespace attributes, we use an emulation that stores attributes into the html class attribute. The advantage is that we can have more attributes encoded into the same class after each other, and even preserve the existing classes on the node. The class entry must have the form of kssattr-key-value. The kssattr- prefix assures that we recognize the entry and do not mix with real classes.
We only added the kssattr at the end of the existing classes on the node and did not change anything else on the template. After rendering the page we can see that all the navtree nodes have kss attributes, for example consider the following rendered html section:
<li class="navTreeItem visualNoMarker"> <div> <a class="state-private kssattr-iscurrent-False" title="" href="http://l12/demo11/folder1/page1"> </a> </div> </li>
To access this attributes from KSS, we use the kssAttr value provider. This works very similarly to nodeAttr, only in this case it looks for the special kss parameters, and it is able to understand the encoding of kss attributes into classes, as described above.
To use it, let us extend our kss rules in the following way:
li.navTreeItem a:click { evt-click-preventdefault: True; action-server: response1; response1-mymessage: 'clicked on navtree item'; response1-href: nodeAttr("href"); response1-value: nodeContent(); response1-iscurrent: kssAttr(iscurrent); } a.navTreeCurrentItem:click { response1-mymessage: 'clicked on the CURRENT navtree item'; }
And, expand our response1 script too. Also add iscurrent=None to the end of the parameter list:
... 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, ) if iscurrent is not None: txt += '<br><span>path=<b>"%s"</b>' % (iscurrent, ) ...
Let us test the results now. All navtree items now print if they are current or not (except for Home which comes from a different template with which we did not deal now).
Homework:
Modify the navtree.pt template too, to make the information available on the "Home" folder as well. This will only have a visible result if the navtree portlet becomes enabled on the portal root, too.
Starting on this path, we could arrive to an ajaxified navtree portlet. We do not do that now, however we can conclude this. The ajaxification of existing components may require the customization of templates. The customization can be done by attaching simple KSS attributes to the required nodes. Why do we need to do this? For the same reason we need class attributes in order to make css work.
Accessing form variables
Another common task is to access the form variables in the page We need this if we want to implement in-place field validation, for example. When we click out of a field, we want to execute a server action.
We will use the portal front page's edit view for our experiment. This contains a stringfield (the title), a textarea field (the description), and a rich widget (text) controlled by the kupu editor as default. We will start only with the title now.
Note:
Although I asked you in the first tutorial, I find it important to stress it again at this point: Before going on with this example, make sure again that Plone's builtin plone.kss and at.kss resources are made inactive. This is important before a similar functionality is already implemented in Plone, and not disabling it would contradict with our example, confusing the results.
Let us use FireBug to look at the html part responsible for rendering the title field:
<div id="archetypes-fieldname-title" class="field ArchetypesStringWidget kssattr-atfieldname-title"> <span/> <label for="title">Title</label> <span class="fieldRequired" title="Required"> (Required) </span> <div id="title_help" class="formHelp"/> <div class="fieldErrorBox"/> <input id="title" class="blurrable firstToFocus" type="text" maxlength="255" size="30" value="Welcome to Plone" name="title"/> </div>
The CSS rule that would bind to the input node is div#archetypes-fieldname-title input and the event that we will use is blur. This event is a native browser event and gets triggered when the control looses focus. So let's add the following rule to our kss:
div#archetypes-fieldname-title input:blur { action-client: alert; }
First we use the alert action to see if the event activates. Indeed, if we enter the field and then leave it, the alert popup appears.
After this we can call a real server action. Let us name it validateField and create it as a python script from the custom skin. At first make it just to display a message to the upper right corner just like our response1 method did:
# 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>In validation.</h1><span>at %s</span>' % (DateTime(), ) # add a command core = getKSSCommandSet('core') core.replaceInnerHTML('#portal-siteactions', txt) # render the commands return renderKSSCommands()
And change the kss to call it:
div#archetypes-fieldname-title input:blur { action-server: validateField; }
This is good so far, but now we want to pass the value of the input form to the field. We have a few possibilities for this:
- formVar('edit_form', 'title') fetches the value by form name and field name.
- currentFormVar('title') fetches the value by field name, and from the current form. The "current" form is selected by the node on which the event has been executed, in this case the input node.
- currentFormVar() supposes that the node on which the event has been triggered is itself a form control element, and uses its value.
You can try all the three possibilities, but for our purposes the third one seems to be the best, since the input node is the same on which the blur event is triggered. So our kss rule will be:
div#archetypes-fieldname-title input:blur { action-server: validateField; validateField-value: currentFormVar(); }
Then we change our validateField script as follows (and we also add value as a parameter to the script):
# import Through-The-Web(TTW) API from kss.core.ttwapi import startKSSCommands from kss.core.ttwapi import getKSSCommandSet from kss.core.ttwapi import renderKSSCommands from kss.core.ttwapi import force_unicode # start a view for commands startKSSCommands(context, context.REQUEST) # use the parameters value = force_unicode(value, 'utf') txt = '<h1>In validation.</h1><span>at %s</span>' % (DateTime(), ) 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 can see now that the script really passes the value that is in the title field, when we leave it. How would we validate it? Well, we use Archetypes to do it. This method will return an error in case the field does not validate, or None if it is all right:
instance = self field = instance.getField('title') error = field.validate(value, instance, {})
In addition if there is an error, we want the message to appear as a portal status message (orange). (Normally the message would appear as a field status message, but it's ok for us now.) Fortunately this is already implemented in KSS, so we don't have to implement it ourselves! This can be invoked in the following way:
plone = getKSSCommandSet("plone") plone.issuePortalMessage(error)
This calls something we call a command. It can be thought of a complex command but in fact it can emit a more complex series of commands for sending back to the client. It also does other things, like decides if there is an error at all (and if there is not, it disables the status message), and also does a translation of the thing.
The full action script will be this in the end:
# import Through-The-Web(TTW) API from kss.core.ttwapi import startKSSCommands from kss.core.ttwapi import getKSSCommandSet from kss.core.ttwapi import renderKSSCommands from kss.core.ttwapi import force_unicode # start a view for commands startKSSCommands(context, context.REQUEST) # use the parameters value = force_unicode(value, 'utf') txt = '<h1>In validation.</h1><span>at %s</span>' % (DateTime(), ) txt +='<br><span>value=<b>"%s"</b>' % (value, ) # add a command core = getKSSCommandSet('core') core.replaceInnerHTML('#portal-siteactions', txt) # do validation and send back result instance = context field = instance.getField('title') error = field.validate(value, instance, {}) plone = getKSSCommandSet("plone") plone.issuePortalMessage(error) # render the commands return renderKSSCommands()
To test it in working, let us clear the title. Leave the field: we see the orange message appear. Enter something again, leave the field, and see the message disappear. At the same time, we can see the status updated in the upper right corner too, since we left that part in as well. For a complete experience, we are happy to observe that we can also click on the navtree items and even to the global tabs! In other words: we have not broken our previous programs.
Naturally to implement in-place validation with all the archetypes fields, more things are needed. Most importantly, we need a way to be able to bind not only to a single field, but to all the fields (of the same type) at once. In addition we don't only have text inputs but checkboxes, dropdowns and more complex fields like the richtext editor. They all require tedious work on more Archetypes templates. However do not despair: this is already implemented in Plone. To look how it works, you can take a deeper look into the archetypes.kss product, together with the at.kss resource file that is contained by Products/Archetypes.
In a following tutorial we will also learn how to use the kssAttr parameter producer function and a single node markup thatr becames effective for entire page regions. For now, however, we conclude the current lesson and enjoy the fruit of our efforts. Because by now we have learned what we need to know if we want to add simple dynamicity to our custom products if we wish to do so.
The Zope export of the final results of this tutorial can be downloaded from here. Before importing, rename your existing custom skin to avoid conflicts. Also after importing if the modified template breaks for you, you might want to customize the template from the exact Plone version you use, instead of the one supplied here.
Doing it the Zope3 way (homework)
We leave this as homework for the kind Reader. One useful hint: in the script we used instance = context, and now in the view we have to use:
instance = self.context.aq_inner
The aq_inner is in some cases necessary to provide the exactly right acquisition context for the Archetypes scripts.
Also when we use the commandset adapters, the parameter for the interface that does the adaptation should be self, not commands (exactly how we did in the previous lessons).