Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

Template feature: content_for block type #192

Closed
w0rm opened this Issue Nov 25, 2012 · 7 comments

Comments

Projects
None yet
2 participants

w0rm commented Nov 25, 2012

In complex apps, there is a need to pass bits of content from child to parent templates. I know this could be done through var statement, but it is not as flexible as my proposal.

I suggest implementing content_for code block, that captures block content, saves it in web.ctx under specified name, so it can be output later in parent templates.

Below is a dirty implementation of what I want.

import web

class WithNode(web.template.BlockNode):
    pass

web.template.STATEMENT_NODES["with"] = WithNode

class content_for:

    def __init__(self, name, locals=None):
        self.name = name
        self.value = web.ctx.get(self.name)
        if self.value is None:
            self.value = []
            web.ctx[self.name] = self.value
        self.locals = locals

    def __call__(self, value):
        self.value += value

    def __enter__(self):
        # Remember extend_ and replace it with self
        self.extend_ = self.locals["extend_"]
        self.locals["extend_"] = self

    def __exit__(self, type, value, traceback):
        # Restore extend_
        self.locals["extend_"] = self.extend_

    def __str__(self):
        return u"".join(self.value)

template = """
$with content_for("header", locals()):
    <p>Text header</p>

$#Then later in this or another template
$:content_for("header")
"""

with_template = web.template.Template(
    template,
    globals={
        "content_for": content_for,
        "locals": locals,
    })

# Uncomment the line below to see generated code
#print with_template.generate_code(template, "")

print with_template()

What I dislike about my solution, is the need to pass in locals() to patch extend_. I'd like to know if there is a better way to implement the same functionality perhaps by deeper integration into web.py template.

w0rm commented Nov 25, 2012

So I've managed to implement a slightly better solution, I'm not sure it should be included in web.py, but maybe somebody finds this useful.

import web


class ContentForNode(web.template.BlockNode):

    def __init__(self, stmt, block, begin_indent=''):
        self.original_stmt = stmt[:-1]
        stmt = 'with ' + self.original_stmt + ' as extend_:'
        web.template.BlockNode.__init__(self, stmt, block, begin_indent)

    def emit(self, indent, text_indent=''):
        out = web.template.BlockNode.emit(self, indent, text_indent)
        out += indent + "extend_ = self.extend" + "\n"
        return out

    def __repr__(self):
        return "<content_for: %s, %s>" % (repr(self.original_stmt),
                                          repr(self.suite))

web.template.STATEMENT_NODES["content_for"] = ContentForNode


class content_for:

    def __init__(self, name):
        self.value = web.ctx.get(name)
        if self.value is None:
            self.value = []
            web.ctx[name] = self.value

    def __call__(self, value):
        self.value += value

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        pass

    def __str__(self):
        return u"".join(self.value)

template = """
$content_for("header"):
    <p>Text header</p>

$#Then later in this or another template
$:content_for("header")
"""

with_template = web.template.Template(
    template,
    globals={
        "content_for": content_for,
        "locals": locals,
    })

# Uncomment the line below to see generated code
#print with_template.generate_code(template, "")

print with_template()
Contributor

aaronsw commented Nov 26, 2012

Can you give an example of how you use this?

w0rm commented Nov 26, 2012

For example, if page needs to include separate css and js and topbar submenu.

$def with(page)
$#layout.html
<html>
<head>
    <link href="/static/style.css" rel="stylesheet">
    $:content_for("head")
</head>
<body>
    <header>
        $:render.topbar()
    </header>
    <section>
        $:page
    </section>
</body>
</html>

#topbar.html
<header>
    <nav>
        <ul>
            <li><a href="#">link 1</a></li>
            <li><a href="#">link 2</a></li>
            $:content_for("topbar")
        </ul>
    </nav>
</header>


#page.html
<h1>Page title</h1>
<p>Paragraph of text.</p>
$content_for("head"):
    <link href="/static/custom.css" rel="stylesheet">
    <script src="/static/custom.js"></script>
$content_for("topbar"):
    <li>
        <span>page submenu</span>
        <ul>
            <li><a href="#">Item #1</a></li>
            <li><a href="#">Item #2</a></li>
        </ul>
    </li>
Contributor

aaronsw commented Nov 26, 2012

See, I'd just do this as:

layout.html

$def with (page)

$:page.head $:render.topbar(page) $:page

topbar.html

$def with (page)
$:page.topbar

page.html

$var head: ...
$var topbar: ...

Or am I missing something?

w0rm commented Nov 26, 2012

This is what I was doing before. But $var statement is a one liner, means you either have to render its value from subtemplate or define separate function in page.html. $content_for is more convenient to use and also more flexible.

It also appends content for the specified name, means for example if page.html had sub-templates of gallery and a map, they could specify their own js and css files.

This is not my idea, it is borrowed from rails http://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html#method-i-content_for

Contributor

aaronsw commented Nov 26, 2012

I'm pretty sure you can use multiple-line $var statements. The append is a
real difference, but I'm not sure it's worth confusing the control flow
like this.

w0rm commented Nov 26, 2012

Oh, it is possible to have a $var that captures output from multiline block. I didn't know that.

import web
template = """
$var header:
    <p>Header text line 1</p>
    <p>Header text line 2</p>
<p>Other text</p>
"""
page = web.template.Template(template)
print page().header

It really works, thanks. Then I agree with you, content_for feature is not needed in web.py code.

@w0rm w0rm closed this Nov 26, 2012

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment