Personal tools
You are here: Home   Additional documentation   KSS  
Search
 
Document Actions

KSS

by Balazs Ree last modified 2006-10-19 15:02

KSS format

This is not yet a formal syntax description, but it tries to show all aspects of KSS via a set of examples taken from our demo cases.

Revision

This is revision 4 of the KSS format.

The changes from revision 3 only touch advanced usage:

  • formal special selector kss is now called behaviour
  • Other parameter producer functions implemented (form, formVar, nodeContent, stateVar and pass, and more)
  • Other html manipulation commands added to the core
  • parameter producer functions changed to camelCase

Simple cases

These are the simple scenarios that more or less contain our original use cases. Here the new format does not add extra functionality, but let's have a look at how this would look like.

Examples:

#title_save:click {
    action-server: saveTitle;
}

Another example:

#kukitportlet-main:timeout {
evt-timeout-delay: 5000;
action-server: refreshRecentPortletBody;
}

Complex cases

The complex cases define the full kss syntax, of which the base case is only a specialization.

The goal of the complex cases is that the plugin programmer can develop complex event types with abstract events that are not equivalent to browser events. A simple example is "timeout": this is not a browser event but it is relatively simple to bind it. In even more complex cases, the plugin writer can freely implement how the events should be bound: main point is that once they are triggered and then we will handle them within the system - like as if they were normal browser events.

As referring to a previous strategical argument of the azax team, this biases the framework to the direction that we do not necessarily bind the low level browser events directly from the kss format. In the kss format we always bind our conceptual events, which in some cases ("native") map to real browser events, in some other cases however not. In the case of the more conceptual events the binding (hijacking) of the real events goes in a blackboxed way from the plugin implementation.

Rules

The rules resemble css rules but at the last tag there must be a kss event selector attached with a comma to it.. The attributes are also alike but with different semantics than in css. We will see more about the parametrizing semantics a bit later. In outline, the rule contains different properties that define

  • parameters for the event binding
  • server and client actions, with parameters; there can be more of these
  • parameters for the "default action" (explained later)
[<CSS> <CSS> <CSS> ...] <CSS_SELECTOR>:<KSS_EVENT_SELECTOR> {

/* event parameters */
...

/* actions (server or client) */
...

/* default action */
...
}

Consider the following example:

div#thisnode a:timeout {

/* event parameters */
evt-timeout-delay: 3000;

/* actions */

action-server: updateInfo;
updateInfo-remark: 'Updating from timeout';
updateInfo-color: red;

action-client: log;
log-message: 'Updating from timeout';
}

Explanation: there is a "timeout" event bound, in case the "div#thisnode a" css selector selects any nodes. The event is bound to the selected nodes but in case of the timeout event the node itself does not matter. The event is bound with a parameter "delay=3000" specifying e 3000 ms timeout recurring tick. When the events are executed, two actions, updateInfo and log are executed, each one with the supplied parameters. One of the actions is a remote action that is executed on the server (and commanding back the client), the other one is a special local action that is implemented as a javascript action plugin, this particular one gives a log message. (There is no default action for this rule).

There is a special type of rule where there is no real css selector but instead of it a KSS special selector is used.

Remark: More css rules for the same description block, separated with comma, are not supported at the moment.

Event selectors

(the "input arrows" of the event node) This is a full-blown solution and remember, in most of the cases, you will only need the shortcut forms. I don't have enough use case examples to sufficiently demonstrate the use of this, this makes the concept more difficult to understand at first.

/documentation/kss/kss_nodes.png

Events are declared by different event classes, it is possible to declare new event classes in a plugin. An event class is implemented by a piece of javascript code that declares how the event is bound. Each event class has "incoming" arrows that are the kss events (not equal to browser events!) and outgoing arrows, the kss actions. It is an important principle that the names of events and actions are in the namespace of the event class.

The outgoing arrow in many case has the same name as the incoming arrow, this means that the event pass through the plugin unbothered.

The binding of kss actions to different nodes, and the specification of what a given action will execute (in other words the "wiring" of the events) is specified in the kss resource file. For this reason we first need to get acquainted to our event selectors.

General form:

:namespace-eventname(eventid)

example:

:dnd-drag(originals)
:dnd-drop(originals)
:dnd-drag(inchart)
:dnd-drop(inchart)

In this case, the event type binder is instantiated in two instances, with two ids (originals and inchart), and the drag and drop are selector classes that the event recognizes.

The (class) name of the event type binder cannot be specified and is not visible from the format. Instead of this, the namespace is specified. The namespace is defined by the event plugin: in a typical case more classes would be defined within the same namespace. The method names, obviously, may not conflict within the same namespace.

Notice that an "event instance" is never created when an event is triggered, but rather when an event is bound to a particular node or nodes. So event instance creation happens at page loading time (or when page content is replaced from an action, and events are rebound).

Remark: the dnd component is not implemented, the namespace shows it is not a component in the core. Iow, do not attempt to use these bindings at the moment.

Shortcut forms:

Id-s can be omitted. In this case each event class creates a singleton instance with a default id. That means that all events share the same object space. (Since most event binder lacks a logic that would require to share data between two consecutively triggered events, this is the expected behaviour anyway.)

:dnd-drag
:myproduct-eventname

There is no namespace. This is to be used for event names that are registered into the global namespace. Any plugin can register event names into the global namespace, in case they do not clash.

timeout
click

The full event rule

The event selectors follow the CSS selector.

div#recent-portlet:click { ... }
div.warehouse-item:dnd-drag(originals) { ... }

The kss event selector must be preceded by a normal css selector. Cannot stand in itself.

Special kss selectors

In addition to css style node selection, there are special kss selectiors that can select the event. The following ones are implemented: 

document
The rule will be matching exactly once for each document. (The methods will receive node=null as a parameter.) The match is only done at the initial pageload, and not when dome content is injected to a page.
behaviour
The rule will never be matched for any node. However, it is possible to call this event method programmatically from the plugin javascript code, and in this case the given actions and parameters are used.  If the event id is omitted, the rule will be in effect for the singleton event instance. You can think about these as fake events, that are generated not by the browser itself but are triggered automatically by other plugin components.

General form:

document:namespace-method
behaviour:namespace-method(eventid)

Eventids must be omitted but if present,. must be existing ones, or else the selected rule will never execute and no warning will be made.

Full example for an event method rule is later in this document.

Specifying parameters

The general schema for this:

.... {
evt-<eventname>-<key1>: <descriptor1>;
evt-<eventname>-<key3>: <descriptor3>;
...
default-<key1>: <descriptor1>;
default-<key2>: <descriptor2>;
...
...
action-[server|client]: <actionname1>;
<actionname1>-<key1>: <descriptor1>;
<actionname1>-<key2>: <descriptor2>;
...

}

The keys themselves cannot contain -, as this has a special semantics in kss. Also the action names cannot be "default" or "evt". Action names can be in camelcase and although original css disallows the usage or uppercase characters in the identifier, here they are allowed.

Event parameters mean that these are parameters to the event binding itself.

#thisnode:timeout {
evt-timeout-delay: 2000;
}

The repetition of the event name after evt- has no semantics, it just increases the readability.

Event actions can be defined with their parameters. An event action must be a server or a client action. There is a restriction: only one event action with the same name can be executed within one rule.

#thisnode:timeout {
action-server: updateInfo;
updateInfo-remark: 'Updating from timeout';
updateInfo-color: red;

action-client: log;
log-message: 'Updating from timeout';
}

For those events whose plugin programatically declares a so called default action, there is a way also to specify parameters to that. Only the parameters can be set: the default action is always the same for that given event name, and it is always on the client. The default action, if we think about the event binder as a class, corresponds to a method of the class so it can be thought of as an action that is bound to an event binder class, it can access its state.

#buttonupdate:bluekit-update {
    default-url:      kukitupdate.html;
    default-nodeid:   target;
}

The default methods are designated to do the task themselves but it is also possible to also specify local and global actions for the same node and in this case all these actions will be executed.

Parameter producer functions

A value simply represents a value. Can stand with or without quotes.

size:     12pt;
typeface: "Bitstream Vera Sans";
typeface: 'Bitstream Vera Sans';

There are possible extensions to acquire a parameter in another way as a constant, these are called parameter producer functions. These are preset functions that can take the eventruls, the state of the event and the page in consideration when calculating a value.

There is a possibility to send an attribute of the selected node.  If the second parameter is true, it also acquires the parameter from parent nodes recursively, until the attribute is found.

node_id: nodeAttr(id);
rownum: nodeAttr("kukit:rownum", true);

or the textual content of a node (in a non-recursive or recursive manner).

text: nodeContent();
text: nodeContent(true);

There is a possibility to send a given form variable. Parameters can optionally be quoted.

member: formVar(edit, member);
member: formVar("edit", "member");
member: formVar('edit', 'member');

member: currentFormVar(member);
member: currentFormVar();

data: form("edit");
data: currenForm("edit");

The parameter producer functions operate on the scope of the node, which triggered the current event.

A complete example rule follows here:

div#portlet-recent:timeout {
evt-timeout-delay:    2000;
action-server: replaceMacro;
replaceMacro-selector: #portlet-recent;
replaceMacro-macropath: portlet_recent/macros/portlet;
}

Here delay="2000" is used as a parameter for the binding of the timeout macro, and selector="div#portlet-recent", macropath="portlet_recent/macros/portlet" are used for calling the "replaceMacro" method.

Another example:

div.menu-item:load {
action-remote: replaceWithRenderedText;
replaceWithRenderedText-text: nodeContent();
replaceWithRenderedText-size: 12pt;
replaceWithRenderedText-typeface: "Bitstream Vera Sans";
replaceWithRenderedText-selector: sameNode();
}

Remark: nodeContent() and sameNode() are planned, but not yet implemented.

The full list of implemented parameter producer functions:

  • 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 it must be a form tag itself).
  • 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.
  • nodeAttr(attrname [, recurseParent]): Procudes the value of a given html attribute of the selected node. If the optional parameter is true, it also tries to recurse to the parents.
  • nodeContent(recursive): Procudes the textual content of the node. Newlines are converted to spaces. If the parameter is alse (default), then only the direct text nodes are considered, if the parameter is true, texts are fetched from the whole subtree.
  • stateVar(varname): Procudes the value of a state variable, that is, the same that can be set via the setStateVar command.
  • pass(attrname): To be described elsewhere. It is only used in advanced cases with specially developed stateful event plugins.

The load event

load is an example for a method that is executed on page loading time. There is a particular form of load:

document:load {
...
}

that would be executed unconditonally once a page is loaded. So to say "document" is a special selector that binds to each page, exactly once.

Implementation remark: On initial load events are executed after the document is completely loaded. However on insertion loads,  we do not know for sure if all parts (i.e. images, embedded documents) are loaded yet. We might find a better implementation for this as soon as becomes possible. There is an experimental implementation of onload events on iframe tags, this is yet work in progress.

How rule merging is done

Like in CSS, parameters with the same key can be overwritten in a selected node. So it is possible to say

div#portlet-recent:timeout {
evt-timeout-delay:    2000;
action-server: replaceMacro;
replaceMacro-selector: #portlet-recent;
replaceMacro-macropath: portlet_recent/macros/portlet;
}

#portlet-recent:timeout {
evt--timeout-delay:    3000;
}

in which case delay will be 3000 (on node(s) which are selected by both rules).

The merging of the rules is similar to how you would think if you think in terms of CSS: the  last secion, the kss selector counts too. Simply, all rules with the same action name and same event id are merged on the same node.

This means that rules are not merged

  • Rules for two different events are not merged: for example, "click" and "timeout" can be applied to the same node.
  • even if the two different events are in the same event binder class: for example, a "drag" and a "drop" rule of the dnd event binder can be applied to the same node.
  • if the event id is different

However the kss-action is not significant from the point of view of the merging, and can simply be overwritten in a following rule if applies to the same node.

As a consequence, for example you cannot specify two "timeout" events to the same node. They will be simply merged and there will be only one timeout event. However if you do deliberately want two timeout events triggered by the presence of the same node, you can specify explicit event ids and this will explicitely make two events. (You could apply the same approach on two "click" events too, but obviously it would not work: because they share the same physical event, so one event would hijack the other in this case, even if they are unmerged.)

Using default methods and method rules

Method rules are special cases of an "outgoing" arrow from the event node. They represent actions that can be triggered programmatically from an event plugin implementation. Method rules are selected by the special method selector kss. This means that the rule will never be triggered by any event, however it can be triggered automatically from the default method of other events.

So when an event has a default method, usually there are no outgoing arrows but the default method does what it needs to. Most importantly, it can decide to execute another "event method" of the same event instance. These rules are defined with the "kss" special event descriptor.

As an example, let us consider the annoyClicker event type plugin. The goal of the plugin is to annoy users. The click event of the plugin can be bound to any node just like an ordinary click event. However the event instantiates itself and starts counting the incoming clicks. The action specified in the "doit" kss rule is only executed every 5th time. For all the other clicks, the annoy action is executed.

/documentation/kss/kss_nodes2.png

The special selector in this example will be annoyclicker-click. This enables binding the action that will be carried out if our annoyclicker-click event decides so in its implementation. (The dashes in the event name show that the event is looked up from the annoyclicker plugin namespace instead of the global one.) The click rule does not specify actions since the default action is supposed to do its work. It could have parameters to the default action though, which is does not have now.

The special selectors kss:annoyclicker-annoy and kss:annoyclicker-doit will never select to any nodes by themselves; instead they are called up from the default action, when the click event is triggered.

The special selectors can be parametrized in the same way, as the event selectors, but obviously evt- keys (parameters for the event binder) are not allowed here. Also kss must stand in itself and not prefixed by some other css selector (in the latter case it would be actually counting as a regular css selector, not a special one)

.clickable:annoyclicker-click {
}

kss:annoyclicker-doit {
action-server: clickedButton;
clickedButton-id: nodeAttr(id);
action-client: log;
log-message: "Was here.";
}

kss:annoyclicker-annoy {
action-client: alert;
alert-message: "You are an idiot! Ha ha ha. (But just keep on trying...)";
}

In the previous example we did not use an event id, as it was not necessary. We just bound by event class. But let's see the same example in case we need to specify an event id. This may be possible, for example, if we want to instantiate different counters to different selectors, and count separately. So we need two different states, two different event binder instances. Both with a different id of our selection, annoyMe and annoyYou.

#button-one:annoyclicker-click(annoyMe) {
}

kss:annoyclicker-doit:annoyMe {
action-server: clickedButton;
clickedButton-id: nodeAttr(id);
action-client: log;
log-message: "Was here.";
}

kss:annoyclicker-annoy(annoyMe) {
action-client: alert;
alert-message: "Keep trying until you get there";
}

#button-two:annoyclicker-click(annoyYou) {
evt-click-count: 2;
}

kss:annoyclicker-doit(annoyYou) {
action-server: clickedButton;
clickedButton-id: nodeAttr(id);
}

kss:annoyclicker-annoy(annoyYou) {
action-client: alert;
alert-message: "Keep trying until you get there, from the second button";
}

Error handlers

It is also possible to attach error handlers to a server action. As an error handler, an error action can be specified. An error action is always a client action, as we cannot rely in error handling to the server..

#title_save click {
action-server: saveTitle;
saveTitle-error: handleError;
}

The error action can accept parameters and they are specified in the same way as for other actions.

Main reasons for an error to occur during the remote action execution are a server error or a timeout. It is also possible that the server method raises an exception on purpose, knowing the error handler will fallback the case on the client side in a designated way.

Cancelling an action with merging

It is now possible to cancel an action:

ul.contentViews li a:click {
action-client: alert;
}

thisId#a:click {
action-client: alert;
}

The above example binds the alert action to the click event for the nodes selected in the first rule. The second rule cancels the alert action for the nodes it selects, in this example for the node with the id thisId.

Action names

Action names are declared following the action-client or action-server keys in the definition.

An action name is either a server action name in case of action-server, or if action-client then a local action implemented as a javascript action plugin.

If an action key is missing from an event or method rule, means that no action (besides the default action) will be carried out. That could also be a use case that we did not cover here: an incoming-only event with no action to be taken.

In the definition there can be more client and server actions defined, but only one is accepted with the same name. More definitions with the same action name will result in overriding the parameters for the original action, and not attaching a second action with the same name.

Implementation notes

Worries about if KSS parsing would be slow

Comparing to the original XML resource parsing, the KSS parsing can take more CPU resources. This is happening in the client browser at page loading time and occasinally when new content is injected into the page. Comaring with the XML parser that is implemented as a native library in the various browser implementations, for the KSS parsing we use a parser implemented in javascript. The main reason for this is that at first we want to provide a library that is independent from the server side.

I might also add that the overhead of KSS parsing will not be substantial, in my opinion, in typical cases with a few or a dozen rules per page. However it is worth considering the possible implementation enhancements such as:

  1. Use a native CSS parser. Since KSS is valid CSS syntax and in fact only uses a subset of CSS, any CSS parser could be used to parse KSS. At this time I am not aware of browsers offering their internal CSS parser through an API, but situation can change.
  2. Use a "cooked" format. This way the KSS will get parsed on the server side and the browser will get a cacheable response containing pre-cooked data. We plan to implement this soon and the flexible JSON format will be used for data transfer. The JSON format requires in the client only a plain evaluation of the data by the browser's internal javascript parser, so it will be an effective speedup, however it requires a parser to be written for the server.
  3. Use the old-style XML resources, this is disabled at the moment but we will re-enable it.

As the library can accept and handle multiple format resources, the "raw" KSS, "cooked" KSS and old-style XML resource formats will be enabled simultaneously, even for use within the same page.

To shortcut additional considerations, the logs now include loading, parsing, and event binding times expressed in milliseconds.

Slowdown caused by ResourceRegistries

Remark: If you notice slow page loads, that's not kss parsing, the problem is that ResourceRegistries repacks all files each time. If you switch on browser side caching or otherwise have a cache between the client and the server, you will see the difference. Also look at the logs for time consumption during parsing and event binding.

The problem typically occurs if you run zope in debug mode and switch of caching in the browser. Not sure if it would be desirable to make ResourceRegistries respond correctly to the last modified headers, or we could say people install cacheFu as an upfront cache anyway or use they browser cache.

Hint: To make debugging more effective, look at the concatresource.zcml  file in azax, and change it how the comments say. It makes no big difference on my Linux station, but it is critical on Mac (probably has to do something with file access). This will not actually speed up ResourceRegistries but speeds up the way the ++resource++kukit.js is handled.

Features

The implementation is partial however tends to reach the goals.

  • embedded documents, iframe tags are not supported (i.e. documents in iframes cannot have their own kss stlesheet, this causes a clash at the moment.)