close Warning: Can't synchronize with repository "(default)" (/project/ucw/svn does not appear to be a Subversion repository.). Look in the Trac log for more information.

Nested Components (:|,) containers and tabbed panes <- back | toc | forward -> Advanced form handling

Html templating: TAL

Ucw's template attribute language is loosely based on Zope's TAL. It also uses xml to express it's intentions and it uses a few of it operators. Moreover it uses its idea: a way to embed a few programming constructs in xml so that the xml can be parsed back into the language as a design template with some flexibility. Programmers can easily modify the html/xml that designers present to make it fit for code. As can the designers themselves up until a point.

How it works

Tal code is confined in .tal files. Those files are called from lisp code to render a page on a component, much like a render method. You can either replace a render method or embed tal files in a render method. You can also embed render methods in tal files, tal files in render methods and you can embed tal files in other tal files. Tal files can harbor control structures, variables and lisp code. When a component is rendered, the the tal control structures, variables and lisp code in a tal file are evaluated and the specified tal files are transformed into yaclml code, which on it's turn is transformed into xml/html.

An example. (yes you know, it's to be found in the supplied sources. zzz) First add the tal-generator key-value pair to the make-instance of our *ucw-intro-application* in config.lisp and add two entry-point to our dispatcher list. With the stuff added in previous chapters it should look something like this:

(defparameter *ucw-intro-application*
  (make-instance 'cookie-session-application
                 :url-prefix "/ucw-intro/"
                 :www-roots (list *www-root*)
                 :tal-generator (make-instance 'yaclml:file-system-generator
                                               :cachep t
                                               :root-directories (list *www-root*))
                 :dispatchers (list (action-dispatcher)
                                    ;; hello world
                                    (url-dispatcher "hello-world.ucw"
                                      (call 'hello-world))
                                    ;; basics
                                    (regexp-dispatcher "^(basics.ucw|)$"
                                      (call 'basics))
                                    (regexp-dispatcher "^(get.ucw|)$"
                                      (with-request-params ((cow "moo"))
                                          (context.request *context*)
                                        (let ((cow (concatenate 'string "mighty " cow)))
                                          (call 'basics :cow cow))))
                                    ;; containers
                                    (regexp-dispatcher "^(containers.ucw|)$"
                                      (call 'office))
                                    ;; tabbed-pane
                                    (regexp-dispatcher "^(pane.ucw|)$"
                                      (call 'pane))
                                    ;; tal
                                    (regexp-dispatcher "^(tal.ucw|)$"
                                      (call 'tal-component))
                                    (regexp-dispatcher "^(more-tal.ucw|)$"
                                      (call 'more-tal)))
                 :debug-on-error t))

Then we create the first part of tal.lisp:

(in-package :ucw-intro)
(defcomponent tal-component (template-component)
  ((page-name :initarg :page-name :accessor page-name)
   (contents :initarg :contents :accessor contents))
  ;;  (:entry-point "^(|tal.ucw)$" (:application *ucw-intro-application* :class regexp-dispatcher))
  (:default-initargs :template-name "intro.tal" :page-name "Que tal!" :contents "<blink>Qual blink!!!</blink>"
                     :flag t))

Observe the :tal-generator bit in the application constructor, which specifies the root directories of the search paths to the tal files. Furthermore we let tal-component inherit from template-component, which adds the :template-name slot to standard component, next to some custom methods that do undercover work. In default-initargs you specify your tal file to render. Let's make that file:

<html xmlns:tal=""
    <title tal:content="$page-name">page name goes here</title>
    <link href="sheet.css" rel="stylesheet" type="text/css"/>
    <h1 tal:content="(content $component)" tal:escape-html="nil">content goes here</h1>
    <h1 tal:replace="$contents" tal:escape-html="t">content goes here</h1>

In the html tag you see the xmlns bits. They are xml name spaces. You can look them up in your browser but they lead to nowhere. But inside lisp they are mapped to different packages where custom defined tags, attributes and parameters reside. In this case :it.bese.yaclml.tal. tal:in-package of course maps to in-package. We for example reach the tal tag and attribute definitions by putting tal: in front of them. They are:

  • tags: tal, lisp, include
  • attributes: content, replace, when, dolist, in-package

When a tag (a tal tag, or a html/xml tag) is parsed, tal checks, from left to right, if the tag and/or any of it's attributes are part of a namespace which begs them to be evaluated by tal. A tag can contain multiple tal attributes and they can be mixed with html/xml attributes. If so tal determines which handler is fit and hands it the tag to be operated upon. (For your understanding, when it concerns an attribute, tal usually eats that attribute, performs the specified operation, and processes the tag again without it.)

To continue the treatment of our example, we examine the attributes content and replace. Content replaces the content of the tag it's a part of (in this case "content goes here") with what's contained in it's variable (the result of "(contents $component). Replace replaces the complete tag with what's in it's variable. They both check for the value of escape-html which doesn't escapes html if the value is nil and which does escape html whenever the value is non-nil. If escape-html isn't supplied it defaults to t.

For completeness: we could also have had our tal-component class inherit from simple-component and leave out everything except for the contents of the body, and ucw would have added the code around it.

But where are variables defined? How can tal interact with the class it renders? And what about those other tags and attributes? Enter the next section:

Tal and lisp

Basically where a tag or an attribute accepts some sort of parameter you usually have a window into lisp. This is the case with lisp, content, replace, when and dolist. The text inside " and " is evaluated, except for tal:lisp which evaluates the text inside its beginning- and end-tag. Variables start with the character $ and are matched against values inside the so-called tal environment. This environments equates to a collection of supplied classes, alists and hash-tables in which tal tries to match the variable. When it does it returns the value. In our example some methods on template-component make sure the component that the tal file is rendered on is included and can be referenced with $component. As you can see you can either use the slot function of contents or you can refer to contents directly by pasting a $ to its front, as you can refer directly to the key of a cons in an a-list or the key in a hash table. Later on you will see ways to extend the tal environment.

Sometimes you want to use a variable, or the output of some lisp code in an ordinary attribute. You can do that by putting "${..your lisp code...}" after the attribute (see the next example). If you for some reason want to splice the contents of a list after an attribute you can replace the $ with a @.

Let's review the rest of the tal tags and attributes. We start of with another example. This time we create a component called more-ucw, two simple classes and a hash table for use with the tal file:

(defcomponent more-tal (template-component)
  ((flag :initarg :flag :accessor flag)
   (da-var :initform "da-var from more-tal class" :accessor da-var)
   (tal-name :initform "more-wrapper.tal" :accessor tal-name)
   (another-var :initform "another-var from more-tal class" :accessor another-var)
   (a-href :initform "" :accessor a-href)
   (a-class :initform (make-instance 'in-list) :accessor a-class)
   (spliced-href :accessor spliced-href :initform '("http://" "planet." "lisp." "org"))
   (my-list :accessor my-list
            :initform `((,(make-instance 'in-list)
                         ,(make-instance 'another-in-list))
                        (((da-var . "da-var from a-list") (another-var . "another-var from a-list")))
                        (,(make-instance 'in-list :da-var "da-var from class in another bullet")
                         ((da-var . "da-var from another a-list") (another-var . "another-var from another alist")))
  ;;  (:entry-point "^(|more-tal.ucw)$" (:application *ucw-intro-application* :class regexp-dispatcher))
  (:default-initargs :template-name "more-tal.tal" :flag nil))
(defclass in-list ()
  ((da-var :initarg :da-var :initform "da-var from in-list class" :accessor da-var)))
(defclass another-in-list ()
  ((another-var :initform "another-var from in-list class" :accessor another-var)))
(defparameter *tal-table* (make-hash-table))
(setf (gethash 'another-var *tal-table*) "another-var from hash")

and we make a .tal file called more-tal.tal:

<html xmlns:tal=""
    <title>more tal</title>
    <link href="sheet.css" rel="stylesheet" type="text/css"/>
  <tal:include tal:name="more-wrapper.tal" param:little-class="(a-class $component)">
      <div tal:when="$flag" tal:content="$another-var">
        flag is set?
        <li tal:dolist="$my-list">
          <b tal:content="$da-var"></b><br/>
            <tal:tal tal:content="$another-var">another var</tal:tal>
      <p><a href="${(a-href $component)}">attribute insertion</a></p>      
      <p><a href="@{(spliced-href $component)}">spliced attribute insertion</a></p>
        (setf (a-href $component) "")
      <a href="${(a-href $component)}">new value of href</a>      

We leave tal:include and the param: tag and attribute alone for a moment because they need some extra code to explain. We explain the rest first:

  • tal:when - an attribute - which is of course not dissimilar to lisps when. When the value produced by the expression inside the quotation marks equates to nil, attributes after the when tag are not processed. If this means that no tal attribute will supply the tag with content, the whole tag, and it's contents are removed. If a previous attribute already supplied the tag with content through tal:content the tag will not be removed and that content will be shown. If content was previously supplied with tal:replace the tag will be replaced by the content of tal:replace, as this is the behaviour of tal:replace
  • tal:dolist - an attribute - This is a rather cool and nifty tag. It works not dissimilar to dolist. The code between the quotation marks of dolist should equate to a list of lists containing alists, classes hash-tables or a combination of the former. Dolist iterates over the list of lists. On each iteration the tag dolist is a part of and it's children are evaluated and added to the yaclml tree in the pending tal environment extended with the list which is at that point the list dolist is iterating over. See the initform of the my-list slot of the more-tal component to get a feel for it.
  • tal:tal - a tag - In the dolist tag tree in more-tal.tal you see tal:tal which is simply a placeholder for attributes that doesn't show up in the html/xml code you want to produce.
  • the $ AND @ ones after a normal attribute - as discussed earlier these are used when you want to use the result of some lisp code output or a list of some lisp code output respectively after an ordinary attribute.
  • tal:lisp - a tag - The text enclosed within a tal:lisp tag is processed for side effects. No output is passed back into the tag tree.

Now to explain tal:include, which basically does what you expect. Tal:include extends the current tal environment with the parameters that are in the param name-space and that are enclosed in the include tag (so everything with param: in front of it). This environment is passed to the calling tal file which it uses to evaluate it's own tal tags and attributes. You can pass param: tags and param: attributes, which behave differently. Param tags (you may call them whatever you wish) stuff all the evaluated content enclosed in them in a variable. (in the case of our example this will be in $contents). All the tags enclosed in the include tag are evaluated together with their attributes and the attributes of the param: tag before they are passed to the included tal file. So the content of $contents will be a big (or a little) string. By using attributes on the other hand, you can pass variables that refer to lisp objects. The value of the variable is the value returned by the code enclosed within the quotations that stick to it via the equal sign. The template that tal:include calls can be specified by either a tal:name or tal:name-expression attribute. Tal:name simply maps to the specified tal file. Name-expression evaluates the expression typed inside it's quotation marks, which should equate to a valid tal-file path. With this information we look at the .tal file we call from more-tal, called more-wrapper:

  <tal:tal tal:content="$contents" tal:escape-html="nil">here goes more-tal</tal:tal>
    (setf (da-var $little-class) "this is the new and quite boring contents of little class")
  <p tal:content="(da-var $little-class)">contents of little-class</p>

Should be pretty clear. For some more clarity, you could also have specified the file to include with:

tal:name-expression="more${(print (string-downcase '-wrapper.tal))}"

Now granted, this looks rather useless and hackerish, but the point is to show how you can use arbitrary expressions enclosed in ${} to get to your filename.

But when would you want to use name-expression? Well, imagine you want to reference a tal file based on the a component you have in a certain slot of the component you are rendering. Then you could write:

<tal:include tal:name-expression="details/${ (class-name (class-of (datum $component))) }" param:datum="(datum $component)"/>

This will include a different file in the details directory, depending on the class of the datum slot, and pass the datum object itself to the included file. (example and explanation from Marco Baringer)

Note that the code inside ${} will only return the name of a class. tal:name-expression automagically appends .tal to the end if it's not already present, as does tal:name.

Ucw-specific tal

Ucw also has a couple of custom defined tal attributes and tags. They are:

  • ucw:select - tag
  • ucw:option - tag
  • ucw:action - attribute
  • ucw:accessor - attribute
  • ucw:render-component - tag

Which simply map to their yaclml equivalent, except for render-component which doesn't have one. Surprisingly enough it allows you to render a component from within tal. Typical well-formed tal strings using ucw would and actually do look like the tags below because they're straight from tal files found in the source code:

<a href="" ucw:action="(respond $component $value)" tal:content="$text">text</a>
<input type="file" ucw:accessor="(file-upload-example.file $component)"/>
<ucw:render-component ucw:component="$selected-component"/>

Well, it should all be self-explanatory. Only render-component is slightly out of context, but even my obsessive over-explaining-gene doesn't feel the need to explain it.

Rendering tal files from within render methods

Sometimes you want to render tal files from within render methods. You can do this by using the render-template method, which is also used under the cover for children of template-component. A render-template method expects a request-context class, the location of a tal file and a tal environment. A basic one looks like this:

(render-template ucw:*context* "tal-file.tal" nil)

In this example the method grabs the standard ucw context, but what is a context? From the source code:

A request-context object contains all the information associated with one particular request/response loop. The request context is usually accessed via the special variable *context* and contains the currently relevant application, session, session-frame, request and response object.

So you should almost always use ucw:*context*, unless you let the code do things advanced enough you don't need to read this tutorial to explain this particular aspect.

Furthermore our example above specifies a tal file and a request context of nil. So if you want to render that particular tal file, all variables you want the file to look up in the request context will equate to nil.

For explanations sake, we change the tal-component component so it doesn't inherit from template-component anymore, eg change:

(defcomponent tal-component (template-component)


(defcomponent  tal-component ()

Then we define a render method on the component:

(defmethod render ((component tal-component))
        (render-template ucw:*context* "intro.tal" nil))

Now this one will actually report an error. Since our tal environment is nil, intro.tal tries to get to the slot component of an inexistent class. Let's add a class to our environment:

(defmethod render ((component tal-component))
  (render-template ucw:*context* "intro.tal"  `(((component . ,component)))))

Since a tal-environment consists of lists of alists, components and hash tables intro.tal now finds our component by referencing the specified alist in the tal-environment. The $contents variable in the tal file however equates to nil, because the component itself isn't listed as an object the tal-environment references to look up values. To add both the component itself and use it as a variable we might as well use the function make-standard-environment which is meant for this purpose:

(defmethod render ((component tal-component))
  (render-template ucw:*context* "intro.tal" (make-standard-environment `((component . ,component)) component)))

And now tal-component renders like before.

Nested Components (:|,) containers and tabbed panes <- back | toc | forward -> Advanced form handling

Last modified 12 years ago Last modified on 07/25/06 11:32:45