Does Haml have template inclusion mechanism?

28 views
Skip to first unread message

Adam Strzelecki

unread,
Apr 15, 2009, 8:58:48 AM4/15/09
to ha...@googlegroups.com
Hello,

I two Haml templates that share the same chunk of code.

published.haml
--
%h1 Published
- links.each do |link|
.link
%h3
%a{ :href => bare ? link.permalink : link.url }= link.title
.actions
%a{ :href => link.permalink+'/'+_('edit') } Edit
%a{ :href => link.permalink+'/'+_('delete'), :onclick =>
"javascript:return confirm('"+_('Are you sure you want to delete this
item?')+"');" } Delete
%p= link.description

link.haml
--
.link
%h3
%a{ :href => bare ? link.permalink : link.url }= link.title
.actions
%a{ :href => link.permalink+'/'+_('edit') } Edit
%a{ :href => link.permalink+'/'+_('delete'), :onclick =>
"javascript:return confirm('"+_('Are you sure you want to delete this
item?')+"');" } Delete
%p= link.description
- comments.each do |comment|
.comment= comment.content


Is there any way to turn them into?? :

published.haml
--
%h1 Published
- links.each do |link|
+ link_body

link.haml
--
+ link_body
- comments.each do |comment|
.comment= comment.content

link_body.haml
--
.link
%h3
%a{ :href => bare ? link.permalink : link.url }= link.title
.actions
%a{ :href => link.permalink+'/'+_('edit') } Edit
%a{ :href => link.permalink+'/'+_('delete'), :onclick =>
"javascript:return confirm('"+_('Are you sure you want to delete this
item?')+"');" } Delete
%p= link.description

With haml and keep DRY within templates?
I know I can call in Hamle template with Sinatra using:
- haml_concat haml :link_body, :layout => false, :locals => {:link =>
link}
But this is pretty, and slow too.

Would be cool then if Haml::Engine#new took some :lookup option
parameter that is a symbol of scope object method that returns Haml
source for specified name:

class MyScopeObject
def lookup(name)
# some code goes here ...
template_src
end
end

So + action call can be customized by framework embedding Haml such as
Sinatra that would be:
class Sinatra::Base
def lookup(name)
lookup_template(name, :haml)
end
end

Regards,
--
Adam

Chris Eppstein

unread,
Apr 15, 2009, 12:09:41 PM4/15/09
to ha...@googlegroups.com
I think an opportunity does exist for haml to provide a framework agnostic inclusion mechanism but I'm not sure this is quite it. I like the correlation between Sass Mixins by using + to include content. But I'm not a big fan of shared locals -- that's a huge amount of burden to place on the calling template.

Like sass, I wonder if a special declaration of partials and importing (partially delegated to the app framework) makes sense with a naming convention for the common case.

_link_body.html.haml

=link_body(link, description = link.description)
  %h3
    %a{ :href => bare ? link.permalink : link.url }= link.title
  .actions
    %a{ :href => link.permalink+'/'+_('edit') } Edit
    %a{ :href => link.permalink+'/'+_('delete'), :onclick =>
"javascript:return confirm('"+_('Are you sure you want to delete this
item?')+"');" } Delete
  %p= description

Everything after the equals sign is valid ruby syntax and could be compiled directly to a ruby function for the calling semantics.

Using it would look like:
.link
  +link_body(link)

If the haml mixin isn't found it could be auto imported by assuming there's a local partial of the same name or it could be imported directly if you wanted to have a mixin library, etc:

- haml_import("shared/links")

Additionally, this syntax allows partials to be declared inline without the need for additional files.

Introducing two new control character (+,=) would require some fore-thought and deprecation warnings, etc.

Chris

Chris Eppstein

unread,
Apr 15, 2009, 2:22:13 PM4/15/09
to ha...@googlegroups.com
I realized that using = as a control character is obviously out of the question. We'd need something else. For now, I'll suggest "+=" as a placeholder.

Also, another syntax nicety that could fall out from this approach:

+=form_field(label)
  .field
    %label= label
    %br
    - yield

- form_for @user do |form|
  +form_field("Name")
    = form.text_field :name

Note how the indented content is passed as a block to the mixin function. This would accomplish much of what the w2tags project is trying to accomplish.

Chris

Nathan Weizenbaum

unread,
Apr 15, 2009, 3:49:49 PM4/15/09
to ha...@googlegroups.com
This has been discussed before and decided against. Partial inclusion
is something that's really the domain of the Ruby framework built up
around Haml; there's too much craziness involved in figuring out the
location of the partials and so forth to be able to handle it in Haml
with any consistency. In addition, the functionality simply isn't
useful in light of the possibility of defining this in Ruby code.
Finally, we're trying to avoid major syntactic changes in Haml at this
point.

Adam Strzelecki

unread,
Apr 15, 2009, 5:02:50 PM4/15/09
to ha...@googlegroups.com
> This has been discussed before and decided against. Partial inclusion
> is something that's really the domain of the Ruby framework built up
> around Haml; there's too much craziness involved in figuring out the
> location of the partials and so forth to be able to handle it in Haml
> with any consistency.

Maybe you can reconsider my proposition, where you specify symbol of
the method of scoped object that takes String name and returns Haml
source.
So:
+ included_template_name
or whatever in place "+" would use a callback defined outside of Haml
that returns the Haml source for specified name.

> In addition, the functionality simply isn't
> useful in light of the possibility of defining this in Ruby code.

Yes, it is possible to do some kind of inclusion now. We could have a
call in Sinatra:


- haml_concat haml :link_body, :layout => false, :locals => {:link =>
link}

This is "includes" link_body into the output. But there's a serious
problem of performance.
When this haml_concat haml call is rendered in loop of 20 iterations
rendering takes ~4x time than if I just have put the link_body content
in place of the - haml_concat ....

And of source it is far less readable and clear than:
+ link_body

Regards,
--
Adam

Nathan Weizenbaum

unread,
Apr 15, 2009, 6:35:05 PM4/15/09
to ha...@googlegroups.com
First of all, your use of "- haml_concat ..." is really confusing;
what's wrong with just "= ..."? Second, if the partial-rendering call
takes too much time, that suggests that there's a performance problem
in Sinatra's Haml rendering code, like that I mentioned at
http://nex-3.com/posts/81-more-haml-benchmark-issues#comment_561 .

Adam Strzelecki

unread,
Apr 16, 2009, 5:03:36 AM4/16/09
to ha...@googlegroups.com
> First of all, your use of "- haml_concat ..." is really confusing;
> what's wrong with just "= ..."?

Good question ;) Well it works now fine with =..., hmm I don't
remember what was wrong there.

> Second, if the partial-rendering call
> takes too much time, that suggests that there's a performance problem
> in Sinatra's Haml rendering code, like that I mentioned at
> http://nex-3.com/posts/81-more-haml-benchmark-issues#comment_561 .

Seems this is Sinatra's issue then. I see one potential problem then
with Sinatra against running it through
render_proc, def_method -> scope object instance does change on every
request. So I cannot really cache
render_proc, def_method... but I'll try to cache it during one
request. So may subcalls to = haml :template should be faster.

Thanks for your suggestions,
--
Regards

Adam Strzelecki

unread,
Apr 16, 2009, 6:20:30 AM4/16/09
to ha...@googlegroups.com
Here's some benchmark of calling "ab -n 500 http://localhost:4567/"
driven by Sinatra+Thin+Sequel and Ruby 1.8.6 Mac (1.9.1 stable) that
renders:

index.haml
---


%h1 Published
- links.each do |link|

= haml :link, :layout => false, :locals => {:link => link}
---

which calls 10 items via "= haml :link" (links.each has 10 iterations)

link.haml
---
.link
%h3
%a{ :href => link.url }= link.title
.actions
%a{ :href => link.permalink+'/edit' } Edit
%a{ :href => link.permalink+'/delete', :onclick =>
"javascript:return confirm('Are you sure you want to delete this
item?');" } Delete
%p= link.description
---

Complete requests: 500

(1) With caching engine = Haml::Engine#new(src ...) across whole
application, caching proc = engine.to_proc ... during request, and
calling proc.call
Time per request: 7.314 [ms] (mean) (RB 1.9 = 7.498)

(2) With caching engine = Haml::Engine#new(src ...) across whole
application, and calling engine.render
Time per request: 8.575 [ms] (mean) (RB 1.9 = 9.622)

(3) Without caching at all (bare Sinatra)
Time per request: 24.377 [ms] (mean) (RB 1.9 = 31.148)

Having "= haml :link" replaced with actual content of link.haml

(4) Same caching (1) but this time we don't call "= haml :link"
Time per request: 6.228 [ms] (mean) (RB 1.9 = 6.560)

(5) No caching at all (bare Sinatra) ...
Time per request: 11.019 [ms] (mean) (RB 1.9 = 12.558)

My conclusions are:
1. Sinatra w/o caching precompiled templates is a riot in
production 2x-5x slower (1)-(3), (4)-(5)!
2. using engine.render or proc.call (cached from engine.to_proc)
brings us ~20% performance boost (1)-(2) (Ruby's parser, compiler is
really fast then too)
3. theoretically using template inclusion could give us another
~20% boost with caching (1)-(4), or 2x boost with bare sinatra (which
should use caching anyway) (3)-(5)
4. Ruby 1.9 is slower here! probably due slower compiler, string
manipulation and not really using virtues of YARV here

Final question is if this 20% (1 ms) is worth this template inclusion?

Regards,
--
Adam

Nathan Weizenbaum

unread,
Apr 16, 2009, 1:04:11 PM4/16/09
to ha...@googlegroups.com
I believe the way Rails caches templates is that it has a module into
which it defines methods for each of the templates, and then includes
that module into each scope object.

Adam Strzelecki

unread,
May 21, 2009, 2:52:13 PM5/21/09
to ha...@googlegroups.com
Nathan,

After thinking twice I found out that the `Haml::Engine.def_method`
can be called with `self.class` rather than `self`.
So the we got application class instance method that stays for life-
time for every request.

Thanks to your suggestions I think I got perfect Sinatra Haml cache,
here's the original msg from Sinatra group:
http://groups.google.com/group/sinatrarb/msg/2c5896c9430f2439

The trick is just the method defined via `def_method` in our
Application class is the "cache", which is actual Ruby bytecode. No
extra class variables, hashes or whatever is needed (as in my old
code). And it works fast & great!

Reading your blog... I'm looking forward 2.2 release so all together
we gonna run faster than ERB with :ugly and caching on production :)

Regards,
--
Adam | nanoant.com

Reply all
Reply to author
Forward
0 new messages