Code Partitioning With Scopes

TDI allows you to bind nodes or groups of nodes to scopes. This is the counterpart to overlays on the python side. Scopes add the capability to edit the template with different model instances.

It looks like this:

<html>
<body>
    <h1 tdi="doctitle">doc title goes here</h1>
    <ul tdi:scope="menu">
        <li tdi="item"><a tdi="link">some menu item</a></li><li tdi=":-item">
        </li>
    </ul>
    <p tdi="intro" class="edit-intro">Intro goes here.</p>
    <div class="list" tdi="list">
        ...
    </div>
</body>
</html>

Everything within the node marked with tdi:scope (including the node itself) belongs to the “menu” scope. Each scope eventually refers to its own model object, which is looked up as scope_name within the current model. Here’s the code for our menu example:

class Menu(object):
    def __init__(self, menu, page):
        self._menu = menu
        self._page = page

    def render_item(self, node):
        node.repeat(self.repeat_item, self._menu)

    def repeat_item(self, node, (href, menuitem)):
        node.link.content = menuitem
        if (node.ctx[0] + 1 == self._page):
            node.link.hiddenelement = True
        else:
            node.link['href'] = href


class Model(object):
    def __init__(self, menu, page):
        self.scope_menu = Menu(menu, page)

The essence is, that scopes allow you to split your model code into logical pieces. Again, which piece of code belongs to which scope is part of the logic, not part of the template.

More notes on scopes:

  • They can be nested and work cumulative. A scope menu inside a scope left_bar is looked up as model.scope_left_bar.scope_menu.
  • dotted scope names are allowed (for example header.menu), which means that two scopes (first header and then menu inside the header model) are looked up (model.scope_header.scope_menu)
  • scopes can be combined with overlays, of course (you can specify both tdi:scope and tdi:overlay within the same node)

The output of the example:

<html>
<body>
    <h1>doc title goes here</h1>
    <ul>
        <li><a href="/some/">Some Menu Item</a></li>
        <li>Editing Content &amp; Attributes</li>
        <li><a href="/third.html">Another Menu Item</a></li>
    </ul>
    <p class="edit-intro">Intro goes here.</p>
    <div class="list">
        ...
    </div>
</body>
</html>

Distributed Scopes

Simple Scope Scattering

Scopes do not need to be contained by one block. You can easily scatter them over the template. If they occur within the same parent scope, the same scope model instance will be asked for rendering. That’s very handy for distributed but logically associated content, for example:

from tdi import html
template = html.from_string("""
<html>
<body>
    <ul tdi:scope="navigation">
        <li tdi="breadcrumb"><a tdi="*link"></a></li><li tdi=":-breadcrumb">
        </li>
    </ul>
    ...
    <ul tdi:scope="navigation">
        <li tdi="menu"><a tdi="*link">some menu item</a></li><li tdi=":-menu">
        </li>
    </ul>
</body>
</html>
""".lstrip())


class Navigation(object):
    def __init__(self, breadcrumb, menu, page):
        self._breadcrumb = breadcrumb
        self._menu = menu
        self._page = page

    def render_breadcrumb(self, node):
        node.repeat(self.repeat_nav, self._breadcrumb,
            len(self._breadcrumb) - 1
        )

    def render_menu(self, node):
        node.repeat(self.repeat_nav, self._menu,
            self._page - 1
        )

    def repeat_nav(self, node, (href, menuitem), active):
        node.link.content = menuitem
        if (node.ctx[0] == active):
            node.link.hiddenelement = True
        else:
            node.link['href'] = href


class Model(object):
    def __init__(self, breadcrumb, menu, page):
        self.scope_navigation = Navigation(breadcrumb, menu, page)


menu = [
    (u'/some/', u'Some Menu Item'),
    (u'/other/', u'Editing Content & Attributes'),
    (u'/third.html', u'Another Menu Item'),
]
breadcrumb = [
    (u'/', u'Hompage'),
    (u'/other/', u'Editing Content & Attributes'),
]
model = Model(breadcrumb=breadcrumb, menu=menu, page=2)
template.render(model)

Also note, how the repeater code is reused.

<html>
<body>
    <ul>
        <li><a href="/">Hompage</a></li>
        <li>Editing Content &amp; Attributes</li>
    </ul>
    ...
    <ul>
        <li><a href="/some/">Some Menu Item</a></li>
        <li>Editing Content &amp; Attributes</li>
        <li><a href="/third.html">Another Menu Item</a></li>
    </ul>
</body>
</html>

Parametrized Overlays

There’s another, more tricky use case. You can extend or parametrize an existing scope from within the template. That’s useful when you have overlays, which provide standard constructions. Examples are layouted boxes, flash embedding code and so on. The idea is to place a parameter block near the overlay target (usually before it, so it can trigger code, before the overlay is actually rendered). The parameter block gets the same scope as the overlay and voilà – the scope model will see it. The following code outlines the priniciple:

from tdi import html
from tdi.tools.html import decode

# This is some typcial flash embedding code.
# It serves as a "layout widget", placed via overlay
flash = html.from_string("""
<!-- <tdi> is used as neutral dummy element (removed due to the - flag) -->
<tdi tdi:scope="-flash" tdi:overlay="<-flash">
    <object tdi="object_ie"
        classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width=""
        height="">
        <param tdi="url" name="movie" value="" />
        <param tdi="param" />
        <![if !IE]><object tdi="object_other"
            type="application/x-shockwave-flash" data="" width="" height="">
            <param tdi="param" />
        <![endif]>
        <p tdi="alternative">You need to enable <a
          href="http://www.adobe.com/go/getflashplayer">Flash</a> to view this
          content.</p>
    <![if !IE]></object><![endif]></object>
</tdi>
""")

# page template, using the flash layout widget, passing parameters
template = html.from_string("""
<html>
<body>
    <h1>some flash</h1>
    <tdi tdi:scope="-flash.param" tdi="-init"
            file="flashfile.swf" width="400" height="300">
        <tdi tdi="param" name="bgcolor" value="#ffffff" />
        <tdi tdi="param" name="wmode" value="transparent" />
        <tdi tdi="param" name="quality" value="high" />
        <img tdi="alternative" src="replacement-image.png" alt="some cool text" />
    </tdi>
    <tdi tdi:overlay="->flash" />
</body>
</html>
""".lstrip()).overlay(flash)


class FlashParam(dict):
    def render_init(self, node):
        self.clear()
        encoding = node.raw.encoder.encoding
        self['file'] = decode(node[u'file'], encoding)
        self['width'] = decode(node[u'width'], encoding)
        self['height'] = decode(node[u'height'], encoding)
        self['param'] = []
        node.render()
        node.remove()

    def render_param(self, node):
        encoding = node.raw.encoder.encoding
        self['param'].append((
            decode(node[u'name'], encoding),
            decode(node[u'value'], encoding),
        ))

    def render_alternative(self, node):
        self['alt'] = node.copy()


class Flash(object):
    def __init__(self):
        self.scope_param = FlashParam()

    def render_object_ie(self, node):
        p = self.scope_param
        node[u'width'] = p['width']
        node[u'height'] = p['height']

    def render_url(self, node):
        p = self.scope_param
        node[u'value'] = p['file']

    def render_param(self, node):
        p = self.scope_param
        for pnode, (name, value) in node.iterate(p['param']):
            pnode[u'name'] = name
            pnode[u'value'] = value

    def render_object_other(self, node):
        p = self.scope_param
        node[u'width'] = p['width']
        node[u'height'] = p['height']
        node[u'data'] = p['file']

    def render_alternative(self, node):
        p = self.scope_param
        if 'alt' in p:
            node.replace(None, p['alt'])


class Model(object):
    def __init__(self):
        self.scope_flash = Flash()


template.render(Model())
<html>
<body>
    <h1>some flash</h1>
    
    
    <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="400" height="300">
        <param name="movie" value="flashfile.swf" />
        <param name="bgcolor" value="#ffffff" /><param name="wmode" value="transparent" /><param name="quality" value="high" />
        <![if !IE]><object type="application/x-shockwave-flash" data="flashfile.swf" width="400" height="300">
            <param name="bgcolor" value="#ffffff" /><param name="wmode" value="transparent" /><param name="quality" value="high" />
        <![endif]>
        <img src="replacement-image.png" alt="some cool text" />
    <![if !IE]></object><![endif]></object>

</body>
</html>

This code probably needs some explanations:

  • it uses a <tdi> element as “neutral” container for some stuff. This is often useful if you need such a semantic-less element. It also fits everywhere (which a <span> cannot contain everything and a <div> cannot be contained by everything).
  • it uses directly a subscope for the parameters. That’s not exactly neccessary, but it’s useful sometimes – in this case, because it separates the parameters and the (flash-)container rendering on the code side. Note how the FlashParam scope model inherits from dict.
  • FlashParam.render_init uses a subtle trick to render its children and the remove itself: subrendering. This may seem like a cheap trick, but it’s not. The generated result string (which is thrown away) is very small and it keeps the code very clear.
  • However, we just invented our own parameter language here. That’s not a good thing per se. Keep these things small and restrict them to the absolutely neccessary cases. They are the source of confusion and error.

Now, while it looks like a lot of effort to build such a code – remember you’re going to create that once and use often. It will save time and nerves directly afterwards.

Absolute Scopes

You can declare absolute scopes by flagging them with =. This means, they break out of the current scope nesting. You should avoid using this feature regularily. It is, however, useful for:

  • generated templates, in order to ensure a certain structure (it’s used by the prerendering-mechanism, for example)
  • truly absolute scopes, which are always located at the root model (like statistics snippets, parametrized layout widgets, or, say, the main navigation)