Liquid Forms
August 25th, 2007
A few weeks back I wrote about using liquid. In the article I mentioned that soon I would write about how we integrated forms.
I’ve been purposely stalling for two reasons. One, I’ve been extremely busy at work, and two, I’ve been wanting to clean up some code and provide some helpers, but that doesn’t look likely at least for some time.
Before I start, I need to give kudos to the Mephisto team for giving me a good launch pad in creating forms.
Here’s an example tag.
##
# In some other file
##
class MyForm < Liquid::Block
# I typically include these to allow most of the html tag helpers
include ActionView::Helpers::TagHelper
include ActionView::Helpers::FormTagHelper
include ActionView::Helpers::FormOptionsHelper
def render(context)
# In the controller, pass the model along with the registers to be accessed
# from the tag
@model = context.registers[:model]
# this will contain side affects.
context.stack do
context["form"] = {
"first" => text_field_tag("model[first_field]", @model.first_field, :id => "model_first_field"),
"second" => text_field_tag("model[second_field]", @model.second_field, :id => "model_second_field"),
# This is some helper method used to generate an html output of
# the validation errors
"errors" => generate_validation_errors(@model),
"submit" => submit_tag("Submit Form")
}
end
result << %(
<form method="post" action="#{context.url_for(:controller => "some_controller", :action => "some_action")}"
#{render_all(@nodelist, context)}
</form>
)
# this gets spit out to the view
result
end
end
##
# Somewhere in your config you need to register the tag
##
Liquid::Template.register_tag(:myform, MyForm)
The first thing to do is to create a tag that inherits from Liquid::Block, and override the render. Next notice add a hash to the context. This directly defines what the templater can use. Also notice render_all inside the generated form. This allows the tag to pass the buck further on whatever it wraps, and obviously is extremely important.
For the most part that’s it. The templater can then template a form similar to the following.
<div>
{% my_form %}
{{ form.errors }}
<p>
<label for="model_first_field">First Field</label>
{{ form.first }}
</p>
<p>
<label for="model_second_field">Second Field</label>
{{ form.second }}
</p>
<p>
{{ form.submit }}
</p>
{% end_my_form %}
</div>
I think this is a pretty slick way of enabling templaters customization over the way forms are laid out structurally. Obviously there is a lot of room for improvement. Extracting more out of the tag to keep it dry. I’ve also thought about allowing filters to allow designers to manipulate all the other attributes of the elements like:
...
{{ form.first | html id:"new-id" class:"class_one class_two" style:"border: 1px solid"}}
...
I just haven’t had any time to devote to liquid since we closed the last project that used it 3 weeks ago.
Finally you’ll notice above that I’m calling context.url_for. That’s not someting that’s baked into liquid, but something I monkey patched to provide the tags with Rails routes. It only expects that the controller gets passed as a register.
class Liquid::Context
def url_for(*options)
@registers[:controller].send(:url_for, *options)
end
end
Liquids leaking from a Developer
August 2nd, 2007
After eyeing Liquid for quite some time, we decided to use it on a project to allow a customer template his app from the admin side. After seeing a lot of documentation for Designers and Templaters, I felt there was something needed from a developer’s perspective.
I want to extend a gracious thank you to Jaded Pixel the creators behind Shopify for taking to the time to extract this library from their own work, and provide it publically.
Trying to jump into the libary was a little difficult, and I learned a lot from just reading the source and the documentation, as well as the code for Mephisto, one of the few open source projects that I know uses Liquid.
Using Liquid
Using liquid is pretty straightforward. You have Liquid parse a template, and then render it giving the appropriate information
Liquid::Template.parse(some_content_as_a_string).render(assigns, :registers => registers)
You notice that I pass Liquid two other items. assigns is a hash of available variables, objects or drops that the template can reference. registers is a hash of variables that are accessible from Drops, Tags, and Filters. Think of assigns as exposed to the template, and registers only used within the back-end processing of the template.
Liquid::Template.parse(some_content_as_a_string).render({"foo" => "bar"}, :registers => { "something" => "only in the backend"})
... in the template ...
{{ foo }} # => "bar"
{{ something }} # => ""
{{ something_else_wacky }} # => ""
When passing an object in the assigns, it will check either the to_liquid of the object, or check to see if it’s a Drop
Object#to_liquid
Most objects are expected to provide a to_liquid method that will convert itself into a hash which will permit the template access information from the object. No methods will be exposed to the outside, which is convenient for the sake of security.
Keep in mind, you don’t need to provide a 1 to 1 mapping to liquid exposed methods to real methods. You can create additional items for the view.
class Dog
def bark
"woof"
end
def something_secure
"don't touch"
end
def to_liquid
{ "bark" => "woof", "fetch" => "some newspaper"}
end
end
Liquid::Template.parse(some_content_as_a_string).render({"dog" => Dog.new})
... in template ...
{{ dog.bark }} # => "woof"
{{ dog.something_secure}} # => ""
{{ dog.fetch }} # => "some newspaper"
Drops
Sometimes creating to_liquid is either more work than necessary, or you notice a lot of code regarding opening an object. Drops will help in these situations. A drop is an object that will expose all public methods to the template. I could easily rewrite the previous code as a drop (without having to write a to_liquid method)
class DogDrop < Liquid::Drop
def initialize(dog)
@dog = dog
end
def bark
@dog.bark
end
def fetch
"some newspaper"
end
end
Liquid::Template.parse(some_content_as_a_string).render({"dog" => DogDrop.new(Dog.new)})
... in the template ...
{{ dog.bark }} # => "woof"
{{ dog.something_secure}} # => ""
{{ dog.fetch }} # => "some newspaper"
{{ dog.something_wacky }} # => ""
Filters
Filters are a way of manipulating the output the input passed to it. They can be chained in any order without serious harm (though obviously you can write them in that fashion, I wouldn’t suggest it).
module MyFilters
def uppercase(text)
text.upcase
end
def reverse(text)
text.reverse
end
def replace_chars(text, item_to_replace, substitute)
text.gsub(item_to_replace, replace_with)
end
end
... in template ...
{{ dog.bark | uppercase }} # => "WOOF"
{{ dog.bark | reverse }} # => "foow"
{{ dog.bark | uppercase | reverse }} # => "FOOW"
{{ dog.bark | replace_chars: 'w', 'b' }} # => "boof"
To use filters within the templates, you need to register them with Liquid
Liquid::Template.register_filter MyFilters
Tags
These are another core piece of liquid that are generally wrapped in {% %}. You can see the implementation of control structures like “if”, “for”, “while” blocks. As well as other useful tags such as “capture”, “include” and “assign”. I suggest looking at the source for each of those to see how they are implemented, and how to pass parameters.
Later I will post on how to use tags to implement forms that are templatable by designers. (All credit goes to Mephisto for this one).
Creating forms with tags will essentially allow you to write a form like the following, which places a lot of needed control in the designer’s hands.
{% myform %}
<p>
<label for="some_item">First Item</label> {{ form.first_item }}
</p>
<p>
<label for="some_other_item">Second Item</label> {{ form.second_item }}
</p>
<p>
<input type="submit" name="commit" value="Submit this Form" />
</p>
{% endmyform%}
Filesystems
These are a neat concept that is only used for the include tags. You set a “filesystem” with Liquid by setting it to an object.
Liquid::Template.file_system = MyFileSystem.new
Liquid will then pass all {% include 'some_include' %} to the filesystem specifed, which allows you to customize where the partial actually resides. For instance, to allow users to create templates from an interface and retrieve them from the database, you can implement a similar filesystem.
class MyFileSystem
def read_template_file(template_name)
do_something_to_retreive_a_string_from_db(template_name)
end
end
Nothing extraordinary, but very useful. And of course the database isn’t the only place you could store the includes.
All in all, a great library, and I plan to integrate templates with Eventable soon, since it was very successful with our previous project. Thanks Jaded Pixel.
Mocha and Forcing Verification
August 2nd, 2007
I switched to Mocha about 5 months ago, and after getting over my preference for strict ordering, really enjoyed the library.
One problem I continue to have with the library is the silent failure of mocks if an assertion fails prior to the verfication.
Looking at the code:
class Car
def initialize(parts = [])
@parts = parts
end
def start
started = true
@parts.each do | part |
# commenting out for failure
# started = started && part.start
end
started
end
end
class SomeTest < Test::Unit::TestCase
def test_start
engine_mock = mock("engine_mock")
car = Car.new([engine_mock])
engine_mock.expects(:start).returns(false)
assert !car.start
end
end
I am left with a failing test that only provides information regarding the assertion failing. Now with this trivial case, I can look into the method to discover where my problem is, but generally I have to resort to inserting logging in order to discover the crux of my problem.
What I have typically done is in every TestCase I write is include a common teardown
def teardown
mocha_verify
end
This will still error, but it will provide information about the mock failing as well as the assertion failing, which generally provides me quicker turn around in grokking why the test is failing in the first place. The only thing I don’t like about this method, is that when you have a mock verification failure but all your assertions pass, you end up having duplicate Errors in the log. But I’m practical, and willing to live with that.
Getting tired of constantly having a common teardown in each test, I decided to crack open the hood of the library, and submit a patch1. (Patches always seem to be the best conversation starter in open source projects.) However talking with James Mead we seem to have a different points of view in terms of fast failing of the tests.
I respect the choice to not include the functionality into the core, but until I am convinced otherwise, I need more information from my tests. If your brain works anything like mine (which may be an insult), you can use this snipbit of code to monkey patch your TestCases to force the verification, and still ensure the execution of your teardown code.
I must put up some disclaimers, I did NOT engineer this code, and I take no credit for the elegance in providing a AOP-like advice. This was completely ripped off from Hardmock written by the solid developers at Atomic Object
require "mocha"
require "test/unit"
class Test::Unit::TestCase
def mocha_force_verify
mocha_verify
end
if method_defined?(:teardown) then
alias_method :old_teardown, :teardown
define_method(:new_teardown) do
begin
mocha_force_verify
ensure
old_teardown
end
end
else
define_method(:new_teardown) do
mocha_force_verify
end
end
alias_method :teardown, :new_teardown
def self.method_added(method) #:nodoc:
case method
when :teardown
unless method_defined?(:user_teardown)
alias_method :user_teardown, :teardown
define_method(:teardown) do
begin
new_teardown
ensure
user_teardown
end
end
end
end
end
end
Toss this bit of a code into a file and include it. This still has problems with the duplicate errors, but at least you don’t have to remember to implement each test’s teardown.
1 I do have to commend all the maintainers of Mocha for a quality job in the library and the supporting test suites.
Update 8/10/07
I noticed a problem using this with rails 1.2.2 fixtures. Since both seem to use the same methodology. I’ll try and provide an updated snipbit soon.
Update 8/12/07
After banging my head trying to have both the Fixtures source and my code to sit on top of method_added, I decided to just monkey patch mocha. Just plop this into a file and require it before your test.
require 'mocha'
require 'mocha/expectation_error'
module Mocha
module ForceVerifyTestCaseAdapter
def self.included(base)
base.class_eval do
def run(result)
yield(Test::Unit::TestCase::STARTED, name)
@_result = result
begin
mocha_setup
begin
setup
__send__(@method_name)
mocha_verify { add_assertion }
rescue Mocha::ExpectationError => e
added_mocha_failure = true
add_failure(e.message, e.backtrace)
rescue Test::Unit::AssertionFailedError => e
add_failure(e.message, e.backtrace)
rescue StandardError, ScriptError
add_error($!)
ensure
begin
teardown
rescue Test::Unit::AssertionFailedError => e
add_failure(e.message, e.backtrace)
rescue StandardError, ScriptError
add_error($!)
end
end
ensure
unless added_mocha_failure
begin
mocha_verify
rescue
add_error($!)
end
end
mocha_teardown
end
result.add_run
yield(Test::Unit::TestCase::FINISHED, name)
end
end
end
end
end
class Test::Unit::TestCase
include Mocha::ForceVerifyTestCaseAdapter
end