Using Viewlets

Viewlets are a mechanism for creating pluggable themes.

They are broadly similar to macros, in that they allow the contents of a particular part of a template to be defined elsewhere. Moreover, they are also registered based on the context, request and view, allowing specific viewlets to be used for specific types of objects. There are some important differences, though:

  • Multiple viewlets can create content for one spot. (If a only a single viewlet is defined, it’s functionally very much like a macro with no slots.)
  • They must be registered in ZCML. There is currently no support for automatically registering viewlets from the filesystem.
  • They have a hierarchy in naming: a template references a viewlet manager, and the viewlets are attached to the viewlet manager.

They have an extra level of flexibility because they can be written partly or entirely in Python. However, this document will only focus on how they can be used with template files.

Viewlet Managers and the provider: Expression

We’ll jump right in with an example from the base-chameleon theme. First, here is a (simplified) template called generic_post_list.pt:

 <html xmlns:tal="http://xml.zope.org/namespaces/tal"
       xmlns:metal="http://xml.zope.org/namespaces/metal">
    ...
    <body>
     <article tal:replace="structure provider:post_list_article" />
    </body>
 </html>

The provider: expression is new here. That will look up and invoke a content provider.

Viewlet Managers

In the context of nti.nikola_chameleon, that’s almost always going to be a viewlet manager. A viewlet manager is responsible for finding all the applicable viewlets attached to it, finding out which ones are available at the given time, sorting them, and then rendering them. This package provides one default viewlet manager, nti.nikola_chameleon.view.ConditionalViewletManager that does all of those steps. It sorts viewlets based on their weight attribute, and will disable viewlets that have their available attribute (if one exists) set to a false value.

So that’s the first step in using viewlets (after deciding where in the template viewlets might be useful, of course): define your viewlet manager. This is done in ZCML:

<configure xmlns="http://namespaces.zope.org/zope"
          xmlns:i18n="http://namespaces.zope.org/i18n"
          xmlns:zcml="http://namespaces.zope.org/zcml"
          xmlns:z3c="http://namespaces.zope.org/z3c"
          xmlns:browser="http://namespaces.zope.org/browser" >

 <include package="zope.viewlet" file="meta.zcml" />
 <include package="nti.nikola_chameleon" file="meta.zcml" />

 <browser:newViewletManager id="IPostListArticle" />
 <browser:viewletManager
     name="post_list_article"
     provides=".viewlets.IPostListArticle"
     class=".view.ConditionalViewletManager"
     permission="zope.Public"
     />
 </configure>

The line <browser:newViewletManager id="IPostListArticle" /> creates a new kind of viewlet manager. This is important because, as you’ll see, individual viewlets are attached to the kind of the viewlet manager (not its name). In advanced cases, this allows for a hierarchy of viewlet managers to be defined, but in most cases each individual viewlet manager should use its own type.

Note

There are a handful of pre-defined kinds of viewlet managers in nti.nikola_chameleon.interfaces: IHtmlHeadViewletManager, IHtmlBodyContentViewletManager, IHtmlBodyContentHeaderViewletManager, IHtmlBodyContentFooterViewletManager and IHtmlBodyFooterViewletManager. These are very generic and in complex situations it would be easy to accidentally register too much on any given kind of viewlet manager, so only use them with caution. They can work well for simple themes or when well documented, though.

The next line, beginning with <browser:viewletManager actually creates and registers a viewlet manager. We give it a name (post_list_article) by which a provider: expression can find it, we set it up to be the kind of viewlet manager we just defined (provides=".viewlets.IPostListArticle"), and we tell it what sort of class will implement the viewlet functionality.

Note

Using class=".view.ConditionalViewletManager" is always recommended.

Note

Setting permission="zope.Public" is unfortunately required at this time. In the future, a directive might be created that doesn’t require this.

Note

The viewletManager directive does allow the for, layer and view attributes to register the viewlet manager for a particular context, request and view, respectively. Usually registering individual viewlets for particular contexts, requests and views themselves is enough and we can leave these attributes out. However, in advanced cases, where you want to use a completely different kind of viewlet manager (IPostListArticle in this case) of the same name (post_list_article) this may be helpful. It can also be helpful to disable an entire viewlet manager when extending a theme.

Viewlet Manager Templates

Not seen here is the template argument. This allows you to provide a template into which the viewlets attached to the manager will be rendered. The template is passed the attached viewlets in its viewlets option. If you do not specify a template, viewlets default to being rendered one after the other, something like this:

<tal:block xmlns:tal="http://xml.zope.org/namespaces/tal"
           tal:repeat="viewlet options/viewlets">
    <tal:block tal:replace="structure viewlet" />
</tal:block>

Viewlets

Now that we have a viewlet manager, we can add individual viewlets to it. For our purposes, a viewlet is a template that gets run to fill in a piece of content for its viewlet manager. Recall that viewlets are associated with viewlet manager via the viewlet manager’s kind, or interface. Again, this is done in ZCML:

<configure xmlns="http://namespaces.zope.org/zope"
          xmlns:i18n="http://namespaces.zope.org/i18n"
          xmlns:zcml="http://namespaces.zope.org/zcml"
          xmlns:z3c="http://namespaces.zope.org/z3c"
          xmlns:browser="http://namespaces.zope.org/browser" >

 <include package="zope.viewlet" file="meta.zcml" />

 <browser:viewlet
     name="default_article"
     manager=".viewlets.IPostListArticle"
     template="v_generic_article_post_list.pt"
     layer=".interfaces.ITagPageKind"
     permission="zope.Public"
     article_class="tagpage"
     />

 <browser:viewlet
     name="default_article"
     manager=".viewlets.IPostListArticle"
     template="v_generic_article_post_list.pt"
     layer=".interfaces.IAuthorPageKind"
     permission="zope.Public"
     article_class="authorpage"
     />
</configure>

Here we have defined two viewlets and attached them to the IPostListArticle viewlet manager with their manager attribute. Each of them has specified a template to run and a name (as well as the boilerplate permission attribute). They have been specified for different request layers with the layer attribute. Because the layers are orthogonal (a request provides only one page kind), at most only one of these two viewlets will be found for any given request. (We could also specify view and context attributes to further narrow it down, if necessary.)

Passing Parameters

Notice that they both use the same template argument. And what’s that article_class attribute for? Let’s take a look at that template file, v_generic_article_post_list.pt:

 <article class="${view/article_class}"
        xmlns:tal="http://xml.zope.org/namespaces/tal"
            xmlns:metal="http://xml.zope.org/namespaces/metal">
   <header>
       <tal:block replace="structure provider:content_header" />
   </header>

   <ul metal:use-macro="macro:html_posts_postlist">
       <li>A post</li>
   </ul>
 </article>

It turns out that all the extra attributes you specify in ZCML get turned into attributes on the view object when the viewlet runs. So we are able to “parameterize” the template in this way. It turns out the only thing we want to do differently when we are listing posts for a tag and posts for an author is use a different CSS class name (class="${view/article_class}").

There is a subtlety here: The view object while the viewlet template renders is not the same as the view object when the template was called.

Caution

Because ICommentKind is currently based on the view, access to the type of comment system in use is lost when rendering a viewlet.

Overwriting, Extending, and Multiple Viewlets

We’ve seen an example where only one viewlet at a time is expected to match. What if we may actually want more than one viewlet to render? For example, we may have some standard styles that we always want to put into the HTML <head> (while allowing themes that extend us to modify or replace those), while also allowing for additional styles to be added when rendering particular types of pages (again, context/request/view discriminators).

This is where the name attribute becomes useful.

  • A more specific registration with the same name will replace the less specific registration.
  • Registrations with a different name will extend the set of applicable viewlets.
  • Equally specific registrations with the same name aren’t allowed, but an extending theme can replace them.

Let’s look at an example. Suppose our main template defines the <head> to use a content provider:

<html>
  <head>
     ...
    <!--! The extensible viewlet manager; anyone can add things based
         on view, context and request/layer to it. -->
    <tal:block content="structure provider:extra_head" />
  </head>
</html>

We can then define this viewlet manager and add some specific viewlets to it in ZCML:

<configure>
  ...
  <!-- Extra head -->
  <!-- The normal extra head for a page is called 'default_extra_head' -->
  <browser:viewletManager
    name="extra_head"
    provides=".interfaces.IHtmlHeadViewletManager"
    class="zope.viewlet.manager.WeightOrderedViewletManager"
    permission="zope.Public"
    />

  <!-- posts, pages, stories and books all have the same extra header -->
  <browser:viewlet
    name="default_extra_head"
    manager=".interfaces.IHtmlHeadViewletManager"
    template="v_post_extra_head.pt"
    permission="zope.Public"
    layer=".interfaces.IPostPageKind"
    weight="0"
    />
  <!-- books get extra -->
  <browser:viewlet
    name="book_extra_head"
    manager=".interfaces.IHtmlHeadViewletManager"
    template="v_book_extra_head.pt"
    permission="zope.Public"
    layer=".interfaces.IBookPageKind"
    weight="1"
    />
   ...
</configure>

We register a viewlet called default_extra_head for each page kind. (For posts, pages and stories we rely on the fact that all the specific page kinds extend the generic IPostPageKind interface to only write that down once.) We then use a new name to define the extra style that books get when we render IBookPageKind; if we had used the same name again, we would have hidden the default head.

This is also an example of sorting viewlets: we specify the weight attribute to be able to ensure what order the styles are added in.

Supplied Viewlets

This package supplies one viewlet for producing links to feeds in HTML bodies. See the documentation for nti.nikola_chameleon.feeds.HTMLFeedLinkViewlet for more information.