whoward / cadenza

parser and renderer library for liquid-like templates
13 stars 6 forks source link

Template inheritance while iterating #2

Closed joefiorini closed 11 years ago

joefiorini commented 12 years ago

I'm working on an app for designers (http://static.ly) and I'm currently using liquid with the liquid-inheritance gem to provide what Cadenza has built in. I'm considering switching over to Cadenza. There's one feature I'd like to see in template inheritance that I haven't seen anywhere yet, I'm curious to get your thoughts on it.

I'd like to provide some base templates that abstract away more complicated code, then allow users to provide the content for each iteration via blocks. As an extremely simple example, imagine you're coding your blog and want to write an archive page. I would have a file like _templates/all-posts.cadenza that looks like:

{% for post in posts %}
  {% block post %}
{% endfor %}

and then the user could create a file, archive.html.cadenza to define the look for this list:

{% extends _templates/all-posts %}

<ol class="posts-list">
{% block post %}
  <li class="post">
    <a href="{{ post.permalink }}">{{ post.title }}</a>
    {{ post.summary }}
  </li>
{% endblock %}
</ol>

then it would use the content inside the block for iteration in the loop. I'd be happy to take a stab at it; is this something that would be useful for Cadenza? Am I missing some totally obvious way to do this?

Thanks!

whoward commented 12 years ago

First off it's awesome to hear that you are thinking of using cadenza in your app! I'd love to hear anything you have to say (good or bad) about cadenza if you end up working with it!

Now on to your topic, I honestly had not considered this use case before :) so I had to try it out myself which led me to find and patch a bug 8046c3fb in Cadenza::TextRenderer which you'll likely need for whatever solution you come up with (I will release this as Cadenza 1.7.1 soon)

So to answer your question is...sort of :)

Cadenza will (as of 1.7.1) let you define a block in your template which will override content in the base template and you should be able to nest your blocks inside of control structures (if not, let me know, it is a bug)

<!--- base.cadenza --->
{% for item in collection %}
   {% block item_content %}default content goes here...{% endblock %}
{% endfor %}
{% extends "base.cadenza" %}

{% block item_content %}
   <div class='custom'>{{ item.content }}</div>
{% endblock %}

The problem is it looks like you want to wrap your collection in an <ol>...</ol> and when rendering a document with a {% extends %} block Cadenza will ignore anything that isn't inside a {% block %} tag

So what you need is something a bit more custom, I think defining a custom block will do the trick, we could call it 'collection' or something - naming is up to you

{% extends "_templates/all-posts" %}

{% block post %}
  <li class="post">
    <a href="{{ post.permalink }}">{{ post.title }}</a>
    {{ post.summary }}
  </li>
{% endblock %}

{% collection "posts" %}
   <ol>{{yield}}</ol>
{% end %}

PostLogic = Cadenza::Parser.new.parse <<-EOS
   {% for post in posts %}
      {% block post %}default content here...{% endblock %}
   {% endfor %}
EOS

Context = Cadenza::BaseContext.dup
Context.define_block :collection do |context, nodes, parameters|
   # ???
end

I still need to think on it a bit but maybe this can start getting you on your way?

joefiorini commented 12 years ago

Thanks for the reply @whoward!

Can you nest blocks in Cadenza? What if I could say:

<!--- base.cadenza --->
{% block collection %}
<ol class="posts-list">
{% for post in posts %}
{% block item_content %}
  <li class="post">
    <a href="{{ post.permalink }}">{{ post.title }}</a>
    {{ post.summary }}
  </li>
{% endfor %}
{% endblock %}
</ol>
{% endblock %}

then do:

{% extends "base.cadenza" %}

{% block item_content %}
   <div class='custom'>{{ item.content }}</div>
{% endblock %}

but if I override {% block collection %} it would print the default {% block item_content %} unless I specified otherwise.

Is this already possible?

whoward commented 12 years ago

You absolutely should be able to nest blocks within each other, though I will admit I haven't tested this behaviour myself. If this doesn't work be sure to let me know as it is a bug that will have to be fixed!

The problem you may run into is that overriding blocks in your extended template will completely override the content in the base template, so the user will have to copy all of the {% for x in y %}...{% endfor %} logic into the redefinition of {% block collection %}

joefiorini commented 12 years ago

I'll have to think on this some more. I think this is a problem worth solving, since it would make for a very elegant way to define default templates and then allow users to override them without having to mess around with awkward partials and whatnot. Thanks for the help so far!

whoward commented 12 years ago

Yeah it would be great if we could figure out a nice way to do this and make it generalized enough that anyone would be able to apply it to their system.

Here's a thought, in place of the {{yield}} magic variable I suggested above you could instead use the render function to do much of the same. Your users will still have to remember to put in the call to "render" but at least they won't have to copy logic if they want to redefine the collection.

<!--- index.html --->
{% extends "_templates/all-posts.html" %}

{% block post %}
  <li class="post">
    <a href="{{ post.permalink }}">{{ post.title }}</a>
    {{ post.summary }}
  </li>
{% endblock %}

{% collection "posts" %}
   <ol>{{render "_templates/each_post.html"}}</ol>
{% end %}
<!--- _templates/each_post.html --->
{% for post in posts %}
   {% block post %}default content here...{% endblock %}
{% endfor %}
<!--- _templates/all-posts.html --->
<html>
   <head></head>
   <body></body>
      <h1>All Posts</h1>
      {{ render_collection "posts" }}
   </body>
</html>
# when setting up your context

Context = Cadenza::BaseContext.dup
Context.define_block :collection do |context, nodes, parameters|
   # this code assigns the parsed nodes to the _collections hash on the topmost scope (the global scope)
   globals = context.stack.first
   globals[:_collections] ||= {}
   globals[:_collections][parameters[0].identifier] = nodes # you'll probably want to validate the parameters first :)
end

Context.define_functional_variable :render_collection do |context, collection|
   nodes = context.stack.first[:_collections][collection] # best to do some validation here first

   if nodes
      Cadenza::TextRenderer.render(nodes, context)
   else
      ""
   end
end

Be warned, I haven't tested this so you might need to play around with it a bit :)

Maybe you could even improve upon this by using the {{yield}} variable I suggested above to magically figure out how to call {{render "template_name"}} (a case...when block maybe)

whoward commented 11 years ago

out of curiosity have you had a chance to try the idea above? hows it work?

I'm starting to think of a more generalized feature to cover this functionality but I'll probably open a separate issue for it so we can take a lot of the noise out of this one

Any other suggestions you might have? I'll probably continue along with the standard release plan (https://www.pivotaltracker.com/projects/211737) otherwise but I much prefer to work on something I know people will actually use :smiley:

joefiorini commented 11 years ago

I haven't yet. I have a number of other features I need to get through first. I'll close it until I get to that point.