Curly
The Curly template language allows separating your logic from the structure of your HTML templates.
Install / Use
/learn @zendesk/CurlyREADME
Curly
[!WARNING] This gem is not used in Zendesk services anymore and won’t be updated. Curly 4.0.0 will be the final released version.
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.
Table of Contents
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
gem 'curly-templates'
Curly can also install an application layout file, replacing the .erb file commonly
created by Rails. If you wish to use this, run the curly:install generator.
$ rails generate curly:install
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 components:
<!-- app/views/posts/_comment.html.curly -->
<div class="comment">
<p>
{{author_link}} posted {{time_ago}} ago.
</p>
{{body}}
{{#author?}}
<p>{{deletion_link}}</p>
{{/author?}}
</div>
The presenter will be responsible for providing the data for the components. Add the necessary Ruby code to the presenter:
# app/presenters/posts/comment_presenter.rb
class Posts::CommentPresenter < Curly::Presenter
presents :comment
def body
SafeMarkdown.render(@comment.body)
end
def author_link
link_to @comment.author.name, @comment.author, rel: "author"
end
def deletion_link
link_to "Delete", @comment, method: :delete
end
def time_ago
time_ago_in_words(@comment.created_at)
end
def author?
@comment.author == current_user
end
end
The partial can now be rendered like any other, e.g. by calling
render 'comment', comment: comment
render comment
render collection: post.comments
Curly components are surrounded by curly brackets, e.g. {{hello}}. They always map to a
public method on the presenter class, in this case #hello. Methods ending in a question mark
can be used for conditional blocks, e.g. {{#admin?}} ... {{/admin?}}.
Identifiers
Curly components can specify an identifier using the so-called dot notation: {{x.y.z}}.
This can be very useful if the data you're accessing is hierarchical in nature. One common
example is I18n:
<h1>{{i18n.homepage.header}}</h1>
# In the presenter, the identifier is passed as an argument to the method. The
# argument will always be a String.
def i18n(key)
translate(key)
end
The identifier is separated from the component name with a dot. If the presenter method has a default value for the argument, the identifier is optional – otherwise it's mandatory.
Attributes
In addition to an identifier, Curly components can be annotated with attributes. These are key-value pairs that affect how a component is rendered.
The syntax is reminiscent of HTML:
<div>{{sidebar rows=3 width=200px title="I'm the sidebar!"}}</div>
The presenter method that implements the component must have a matching keyword argument:
def sidebar(rows: "1", width: "100px", title:); end
All argument values will be strings. A compilation error will be raised if
- an attribute is used in a component without a matching keyword argument being present in the method definition; or
- a required keyword argument in the method definition is not set as an attribute in the component.
You can define default values using Ruby's own syntax. Additionally, if the presenter
method accepts arbitrary keyword arguments using the **doublesplat syntax then all
attributes will be valid for the component, e.g.
def greetings(**names)
names.map {|name, greeting| "#{name}: #{greeting}!" }.join("\n")
end
{{greetings alice=hello bob=hi}}
<!-- The above would be rendered as: -->
alice: hello!
bob: hi!
Note that since keyword arguments in Ruby are represented as Symbol objects, which are not garbage collected in Ruby versions less than 2.2, accepting arbitrary attributes represents a security vulnerability if your application allows untrusted Curly templates to be rendered. Only use this feature with trusted templates if you're not on Ruby 2.2 yet.
Conditional blocks
If there is some content you only want rendered under specific circumstances, you can
use conditional blocks. The {{#admin?}}...{{/admin?}} syntax will only render the
content of the block if the admin? method on the presenter returns true, while the
{{^admin?}}...{{/admin?}} syntax will only render the content if it returns false.
Both forms can have an identifier: {{#locale.en?}}...{{/locale.en?}} will only
render the block if the locale? method on the presenter returns true given the
argument "en". Here's how to implement that method in the presenter:
class SomePresenter < Curly::Presenter
# Allows rendering content only if the locale matches a specified identifier.
def locale?(identifier)
current_locale == identifier
end
end
Furthermore, attributes can be set on the block. These only need to be specified when opening the block, not when closing it:
{{#square? width=3 height=3}}
<p>It's square!</p>
{{/square?}}
Attributes work the same way as they do for normal components.
Collection blocks
Sometimes you want to render one or more items within the current template, and splitting out a separate template and rendering that in the presenter is too much overhead. You can instead define the template that should be used to render the items inline in the current template using the collection block syntax.
Collection blocks are opened using an asterisk:
{{*comments}}
<li>{{body}} ({{author_name}})</li>
{{/comments}}
The presenter will need to expose the method #comments, which should return a collection
of objects:
class Posts::ShowPresenter < Curly::Presenter
presents :post
def comments
@post.comments
end
end
The template within the collection block will be used to render each item, and it will
be backed by a presenter named after the component – in this case, comments. The name
will be singularized and Curly will try to find the presenter class in the following
order:
Posts::ShowPresenter::CommentPresenterPosts::CommentPresenterCommentPresenter
This allows you some flexibility with regards to how you want to organize these nested templates and presenters.
Note that the nested template will only have access to the methods on the nested presenter, but all variables passed to the "parent" presenter will be forwarded to the nested presenter. In addition, the current item in the collection will be passed, as well as that item's index in the collection:
class Posts::CommentPresenter < Curly::Presenter
presents :post, :comment, :comment_counter
def number
# `comment_counter` is automatically set to the item's index in the collection,
# starting with 1.
@comment_counter
end
def body
@comment.body
end
def author_name
@comment.author.name
end
end
Collection blocks are an alternative to splitting out a separate template and rendering that from the presenter – which solution is best depends on your use case.
Context blocks
While collection blocks allow you to define the template that should be used to render items in a collection right within the parent template, context blocks allow you to define the template for an arbitrary context. This is very powerful, and can be used to define widget-style components and helpers, and provide an easy way to work with structured data. Let's say you have a comment form on your page, and you'd rather keep the template inline. A simple template could look like:
<!-- post.html.curly -->
<h1>{{title}}</h1>
{{body}}
{{@comment_form}}
<b>Name: </b> {{name_field}}<br>
<b>E-mail: </b> {{email_field}}<br>
{{comment_field}}
{{submit_button}}
{{/comment_form}}
Note that an @ character is used to denote a context block. Like with
collection blocks, a separate presenter class is used within the
block, and a simple convention is used to find it. The name of the context component
(in this case, comment_form) will be camel cased, and the current presenter's namespace
will be searched:
class PostPresenter < Curly::Presenter
presents :post
def title; @post.title; end
def body; markdown(@post.body); end
# A context block method *must* take a block argument. The return value
# of the method will be used when rendering. Calling the block argument will
# render the nested template. If you pass a value when calling the block
# argument it will be passed to the presenter.
def comment_form(&block)
form_for(Comment.new, &block)
end
# The presenter name is automati
