Curly
=======
Free your views!
Curly is a template language that completely separates structure and logic.
Instead of interspersing your HTML with snippets of Ruby, all logic is moved
to a presenter class, with only simple placeholders in the HTML.
While the basic concepts are very similar to [Mustache](http://mustache.github.com/)
or [Handlebars](http://handlebarsjs.com/), Curly is different in some key ways:
- Instead of the template controlling the variable scope and looping through
data, all logic is left to the presenter object. This means that untrusted
templates can safely be executed, making Curly a possible alternative to
languages like [Liquid](http://liquidmarkup.org/).
- Instead of implementing its own template resolution mechanism, Curly hooks
directly into Rails, leveraging the existing resolvers.
- Because of the way it integrates with Rails, it is very easy to use partial
Curly templates to split out logic from a presenter. With Mustache, at least,
when integrating with Rails, it is common to return Hash objects from view
object methods that are in turn used by the template.
Installing
----------
Installing Curly is as simple as running `gem install curly-templates`. If you're
using Bundler to manage your dependencies, add this to your Gemfile
```ruby
gem 'curly-templates'
```
How to use Curly
----------------
In order to use Curly for a view or partial, use the suffix `.curly` instead of
`.erb`, e.g. `app/views/posts/_comment.html.curly`. Curly will look for a
corresponding presenter class named `Posts::CommentPresenter`. By convention,
these are placed in `app/presenters/`, so in this case the presenter would
reside in `app/presenters/posts/comment_presenter.rb`. Note that presenters
for partials are not prepended with an underscore.
Add some HTML to the partial template along with some Curly variables:
```html
```
The presenter will be responsible for filling in the variables. Add the necessary
Ruby code to the presenter:
```ruby
# app/presenters/posts/comment_presenter.rb
class Posts::CommentPresenter < Curly::Presenter
presents :comment
def body
BlueCloth.new(@comment.body).to_html
end
def author_link
link_to(@comment.author.name, @comment.author, rel: "author")
end
def time_ago
time_ago_in_words(@comment.created_at)
end
end
```
The partial can now be rendered like any other, e.g. by calling
```ruby
render 'comment', comment: comment
render comment
render collection: post.comments
```
Examples
--------
Here is a simple Curly template -- it will be looked up by Rails automatically.
```html
{{title}}
{{author}}
{{description}}
{{comment_form}}
```
When rendering the template, a presenter is automatically instantiated with the
variables assigned in the controller or the `render` call. The presenter declares
the variables it expects with `presents`, which takes a list of variables names.
```ruby
# app/presenters/posts/show_presenter.rb
class Posts::ShowPresenter < Curly::Presenter
presents :post
def title
@post.title
end
def author
link_to(@post.author.name, @post.author, rel: "author")
end
def description
Markdown.new(@post.description).to_html.html_safe
end
def comments
render 'comment', collection: @post.comments
end
def comment_form
if @post.comments_allowed?
render 'comment_form', post: @post
else
content_tag(:p, "Comments are disabled for this post")
end
end
end
```
Caching
-------
Caching is handled at two levels in Curly – statically and dynamically. Static caching
concerns changes to your code and templates introduced by deploys. If you do not wish
to clear your entire cache every time you deploy, you need a way to indicate that some
view, helper, or other piece of logic has changed.
Dynamic caching concerns changes that happen on the fly, usually made by your users in
the running system. You wish to cache a view or a partial and have it expire whenever
some data is updated – usually whenever a specific record is changed.
### Dynamic Caching
Because of the way logic is contained in presenters, caching entire views or partials
by the data they present becomes exceedingly straightforward. Simply define a
`#cache_key` method that returns a non-nil object, and the return value will be used to
cache the template.
Whereas in ERB you would include the `cache` call in the template itself:
```erb
<% cache([@post, signed_in?]) do %>
...
<% end %>
```
In Curly you would instead declare it in the presenter:
```ruby
class Posts::ShowPresenter < Curly::Presenter
presents :post
def cache_key
[@post, signed_in?]
end
end
```
Likewise, you can add a `#cache_duration` method if you wish to automatically expire
the fragment cache:
```ruby
class Posts::ShowPresenter < Curly::Presenter
...
def cache_duration
30.minutes
end
end
```
### Static Caching
Static caching will only be enabled for presenters that define a non-nil `#cache_key`
method (see "Dynamic Caching.")
In order to make a deploy expire the cache for a specific view, set the version of the
view to something new, usually by incrementing by one:
```ruby
class Posts::ShowPresenter < Curly::Presenter
version 3
def cache_key
# Some objects
end
end
```
This will change the cache keys for all instances of that view, effectively expiring
the old cache entries.
This works well for views, or for partials that are rendered in views that themselves
are not cached. If the partial is nested within a view that _is_ cached, however, the
outer cache will not be expired. The solution is to register that the inner partial
is a dependency of the outer one such that Curly can automatically deduce that the
outer partial cache should be expired:
```ruby
class Posts::ShowPresenter < Curly::Presenter
version 3
depends_on 'posts/comment'
def cache_key
# Some objects
end
end
class Posts::CommentPresenter < Curly::Presenter
version 4
depends_on 'posts/comment'
def cache_key
# Some objects
end
end
```
Now, if the version of `Posts::CommentPresenter` is bumped, the cache keys for both
presenters would change. You can register any number of view paths with `depends_on`.
Thanks
------
Thanks to [Zendesk](http://zendesk.com/) for sponsoring the work on Curly.
Copyright and License
---------------------
Copyright (c) 2013 Daniel Schierbeck (@dasch), Zendesk Inc.
Licensed under the [Apache License Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
{{author_link}} posted {{time_ago}} ago.
{{body}}