= MasterView - Rails-optimized (x)html friendly template engine
MasterView is a ruby/rails optimized HTML/XHTML friendly template engine.
It is designed to use the full power and productivity of rails including
layouts, partials, and rails html helpers while still being editable/styleable
in a WYSIWYG HTML editor.
MasterView is distributed as a gem or a plugin. You may install it as a gem and then generate a lightweight plugin which mainly refers to the gem or you can simply install as a plugin which is self contained. I personally prefer installing as a gem for ease of management, however if you are running at a shared hosting environment you might not have authority to install this gem so you may install as a self contained plugin.
If you are interested in the background story behind all this, it is at the end of this page.
Author:: Jeff Barczewski
Email:: jeff.barczewski @ gmail.com
Rubyforge project:: masterview
Website:: http://masterview.org
License:: MIT open source license like Rails
== Goals
- Create/extend a template engine for rails that would be XHTML friendly and thus could be edited/styled with a WYSIWYG HTML editor even late in development without breaking template.
- Keep it simple. DRY. No extra config files, simple syntax with ruby flavor.
- Design it specifically for ruby and rails. Use the full power and not be limited in its capabilities over what can be done with ERb
- Work nicely with layouts, partials, and rails html helpers.
- Reduce complexity, work with existing rails code, no extra view logic or hashes than what is used by ERb. Scaffold generate initial templates or work from existing html prototype.
- Use one master file to drive all related sections, simplifying editing.
- Preview in browser without running an app. Allow for dummy data in the template so that the page can be viewed and styled independently of the application.
- Performance equal to ERb
== Prerequisites
Requires::
No external dependencies
Optional::
tidy (gem) and tidy library - if these are installed you can use tidy to cleanup html into valid xhtml for use by MasterView
log4r (gem) - if this gem is installed then MasterView will use it for logging otherwise it defaults to using built in Logger.
== Installation
Install in one of the two following ways depending on whether you can use gems or not.
=== Installation using gems (if you are able to use gems)
If you can use gems you may simply do the following
gem install masterview
Now your gem is installed and you can skip that step in the future. After creating your rails directory, change directory to it and run the following to create a very lightweight plugin instance for this application mainly consisting of an init.rb file which will get loaded at runtime. This init.rb refers to the gem for everything but allows you to override any constants or setup that has been provided. See MasterView module masterview.rb for a list of the available constants.
script/generate masterview_plugin
After this MasterView is ready for use. Skip down to the Usage section for more details.
=== Installation without using gems, install as plugin
script/plugin install masterview
This will copy entire MasterView system into your vendor/plugin/masterview directory. You may tweak its init.rb to override any MasterView constants at runtime. See MasterView module masterview.rb for a list of available constants. You may also retrieve the plugin package (.tgz and .zip) from Rubyforge.org searching for the project masterview_complete.
== Usage
You may add MasterView attributes to existing (x)html or you may use the masterview generator to create a complete working application. The generator can create controllers, models, and the MasterView template file similar to how the built-in generator works. Simply change directory to your rails application and run the following
script/generate masterview YourModelName
Once it is done generating, the generated MasterView template file will be created in app/views/masterview/YourModelName.html. This file is html and can be edited with any standard html editor. The rails specific logic is contained in simple attributes which are ignored by html editors. The syntax for these attributes is heavily derived from the rails helper tags themselves so it should feel natural to the rails developer.
Another interesting thing to know is that while all of the pages for this Model have been bundled up into one html file for ease of editing, at runtime this template gets rendered into the exact same layouts and partials that you would use if you were building from scratch. Its jsut that now you can see what your pages will render like in your wysiwyg html editor and change and layout accordingly. Additionally MasterView supplies some javascript to show only one action view at time (list, new, show, edit, delete) so you can view in your browser without running in Rails. Dummy html can be included to improve the accuracy of the page which can be easily removed at runtime.
MasterView is designed to be easy for a developer and designer to work together. By keeping the template in an html friendly format, designers can apply style, layout changes, wording changes, without causing havoc on the rails view code. The designer can be involved at anytime during the development cycle including being able to change style and layout after the system is built. This is great for allowing design or wording changes without reinvolving the developers. One can even start from a designer created prototype and add MasterView tags to make it become real. Whichever way you prefer to work, MasterView accomodates you.
== MasterView attribute directive syntax
These attribute directives are provided by MasterView and you can create your own custom attributes for even more power.
mv:generate="layouts/product.rhtml"
When the generate directive is encountered it creates a new output rhtml file where this element and its
children will be rendered. It also will suspend outputting to any previous output file until it is done with this
one (nested). MasterView will not output any rhtml files unless it encouters one of these or mv:gen_render directive
below. Typically you will have one of these in the very top html element to generate the layout, and you will have
additional directives further inside to output different view partials and forms. MasterView takes this one html
file and generates all the other layout, forms, and partials that you would do manually. for example...
<html mv:generate="layouts/product.rhtml">
<title>Hello</title>
<body>
<div mv:replace="@content_for_layout">
<div mv:generate="product/new.rhtml">
<form></form>
</div>
</div>
</body>
</html>
outputs to two files
app/views/layouts/product.rhtml
<html>
<title>Hello</title>
<body>
<%= @content_for_layout %>
</body>
</html>
app/views/product/new.rhtml
<div>
<form></form>
</div>
mv:gen_render=":partial => 'product/form'"
This directive does two things it creates a partial and it outputs the appropriate code to render this
partial in the parent output file before beginning output of the partial. It calculates the file name from the
:partial value and appends .rhtml for example...
<html mv:generate="layouts/product.rhtml">
<body>
<div mv:gen_render=":partial => 'product/show'">
Name: <span class="static" mv:content="h @product.send(:name)">product Name dummy text</span>
Description: <span class="static" mv:content="h @product.send(:description)">product description dummy text</span>
</div>
</body>
</html>
outputs two files
app/views/layouts/product.rhtml
<html>
<body>
<%= render :partial => 'product/show' %>
</body>
</html>
app/views/product/_show.rhtml
<div>
Name: <span class="static"><%= h @product.send(:name) %></span>
Description: <span class="static"><%= h @product.send(:description) %></span>
</div>
You can also use collections with your partials like
<div mv:gen_render=":partial => 'shared/product', :collection => @products">
which will create the partial shared/_product.rhtml and will render this partial over a collection
mv:gen_replace="whatToLeave"
When used in conjunction with mv:generate directive it causes the value of the attribute to be erb
executed output in the parent file before transferring to the child (nested) output file. for example...
<html mv:generate="layouts/product.rhtml">
<title>Hello</title>
<body>
<div mv:generate="product/new.rhtml" mv:gen_replace="@content_for_layout>
<form></form>
</div>
</body>
</html>
outputs two files
app/views/layouts/product.rhtml
<html>
<title>Hello</title>
<body>
<%= @content_for_layout %>
</body>
</html>
app/views/product/new.rhtml
<div>
<form></form>
</div>
mv:block="rubyBlockCodeHere"
At runtime this expands to the equivalent rhtml (erb) block code around this element, for example...
<div mv:block="products.each do |product|">foobar</div>
becomes
<% products.each do |product| %>
<div>foobar</div>
<% end %>
Note how MasterView automatically put in the 'end' after the closing tag.
Another example using the brace form of a block
<div mv:block="products.each { |product|">foobar</div>
becomes
<% products.each { |product| %>
<div>foobar</div>
<% } %>
Its all based on the ruby and rails syntax while keeping things xhtml and designer friendly.
mv:content="foo"
Replaces the content of the tag with the erb output of foo, for example...
<div mv:content="foo">some dummy html here</div>
becomes
<div><%= foo %></div>
mv:form=":action => 'new'"
Replaces this tag and closing tag with the form_tag and end_form_tag helpers. It also pulls the
method and multipart attributes from the element and merges that into the form_tag options, for example...
<form mv:form=":action => 'new'">foobar</form>
becomes
<% form_tag :action => 'new' %>
foobar
<% end_form_tag %>
<form method="GET" mv:form=":action => 'new'">foobar</form>
becomes
<% form_tag { :action => 'new' }, :method => 'GET' %>
foobar
<% end_form_tag %>
mv:hidden_field="object, method"
Replaces this tag with an hidden input tag using the rails hidden_tag helper, for example...
<input mv:hidden_field="product, desc" type="hidden" value="dummy text"/>
becomes
<%= hidden_field product, desc %>
mv:if="yourIfConditionHere"
Wraps the tag with an if block using the attribute contents for the condition, for example...
<div mv:if="@flash[:notice]">foo</div>
becomes
<% if @flash[:notice] %>
<div>foo</div>
<% end %>
And combining if and content directives we can do the following
<div mv:if="@flash[:notice]" mv:content="@flash[:notice]" class="messages">
dummy notice message here
</div>
becomes
<% if @flash[:notice] %>
<div>
<%= @flash[:notice] %>
</div>
<% end %>
mv:elsif="yourElseConditionHere"
Used in conjunction with an if directive allows you to create if, elsif blocks, for example...
<div mv:if="foo()">
hello
</div>
<div mv:elsif="bar()">
world
</div>
becomes
<% if foo() %>
<div>
hello
</div>
<% elsif bar() %>
<div>
world
</div>
<% end %>
Note that the elsif directive element needs to follow the if element's closing tag
mv:else=""
Used in conjunction with if and elsif, allows you to create if, elseif, else blocks, for example...
<div mv:if="@foo">
hello
</div>
<div mv:else="">
world
</div>
becomes
<% if @foo %>
<div>
hello
</div>
<% else %>
<div>
world
</div>
<% end %>
mv:javascript_include="prototype.js"
Replaces the tag with a javascript_include_tag helper, for example...
<script type="text/javascript" src="../../../public/javascripts/prototype.js"
mv:javascript_include="prototype.js"></script>
becomes
<%= javascript_include_tag 'prototype.js' %>
This is useful to allow scripts to be used for demos/prototypes and then at runtime
use the appropriate asset using the rails helper.
mv:link_to="linkToOptions"
Replaces the tag with a link_to helper, it uses the contents of the tag for the name of the link,
so the name of the link can be changed by the html designer and it gets used in the appropriate place
in the helper, for example...
<a mv:link_to=":action => 'new'">New product</a>
becomes
<%= link_to 'New product', :action => 'new' %>
mv:link_to_if="linkToIfConditionAndOption"
Replaces the tag with a link_to_if helper, uses the contents of the tag for the name of the
link so the name of the link can be changed by the html designer and it gets used in the appropriate
place in the helper, for example...
<a mv:link_to_if="@product_pages.current.previous, { :page => @product_pages.current.previous }"Previous page"</a>
becomes
<%= link_to_if @product_pages.current.previous, 'Previous page', {:page => @product_pages.current.previous } %>
mv:link_to_remote="linkToRemoteOptions"
Replaces the tag with a link_to_remote helper, uses the contents of the tag for the name of the
link so the name of the link can be changed by the html designer and it gets used in the appropriate
place in the helper, for example...
<a mv:link_to_remote=":action => 'new'">New product</a>
becomes
<%= link_to_remote 'New product', :action => 'new' %>
mv:password_field="user, password"
Replaces the tag with a password_field helper using the attribute as well as merging any html
attributes that have been set by the designer, for example...
<input type="password" mv:password_field="user, password"/>
becomes
<%= password_field user, password %>
and similarly
<input type="password" mv:password_field="user, password" size="10" maxlength="15" class="pwdStyle"/>
becomes
<%= password_field, user, password, :class => 'pwdStyle', :maxlength = 15, :size => 10 %>
mv:replace="foo bar"
Replaces the tag and contents with the erb executed output of the attribute. If the attribute
value is an empty string then the tag is simply removed and nothing is output, for example...
<span mv:replace="h product.name">dummy product name</span>
becomes
<%= h product.name %>
and similarly, you can use this to remove dummy html which was there for prototyping/demoing
<table>
other rows here
<tr mv:replace="">
<td>foo</td>
<td>bar</td>
</tr>
</table>
becomes
<table>
other rows here
</table>
Passing in an empty attribute simply eliminated the tag, children, and contents
mv:stylesheet_link="style"
Replaces the tag with a stylesheet_link_tag helper, for example...
<link rel="stylesheet" type="text/css" href="../../../public/stylesheets/scaffold.css" mv:stylesheet_link="scaffold"/>
becomes
<%= stylesheet_link_tag "scaffold" %>
This is useful to allow style to be used for demos/prototypes and then at runtime
use the appropriate asset using the rails helper.
mv:submit="submitOptions"
Replaces the tag with a submit_tag helper, it uses the html value attribute to set the text
in the helper, and appends any options after, for example...
<input type="submit" mv:submit="" value="Save product"/>
becomes
<%= submit_tag 'Save product' %>
and
<input type="submit" mv:submit=":foo => 'bar'" value="Save product"/>
becomes
<%= submit_tag 'Save product', :foo => 'bar' %>
mv:text_area="object, method"
Replaces the tag with a text_area helper, it uses the html attributes like rows, cols, disabled,
readonly, class, style, tabindex, accesskey and merges them with the object and method specified
in the attribute value, for example...
<textarea mv:text_areas="product, desc">dummy text</textarea>
becomes
<%= text_area prouct, desc %>
or a more complex example
<textarea mv:text_area="product, desc" rows="4" cols="60", class="mystyle">dummy text</textarea>
becomes
<%= textarea product, desc, :class => 'mystyle', :cols => '60', :rows => 4 %>
mv:text_field="object, method"
Replaces the tag with a text_field helper, it uses the html attributes like size, maxlength,
disabled, readonly, class, style, tabindex, accesskey and merges them with the object and
method specified in the attribute value, for example...
<input type="text" mv:text_field="product, name" value="dummy text"/>
becomes
<%= text_field product, name %>
and
<input type="text" mv:text_field="product, name" value="dummy text" class="mystyle" size="15" maxlength="20"/>
becomes
<%= text_field product, name, :class => 'mystyle', :maxlength => 20, :size => 15 %>
Inline substitutions
Since <%= foobar %> and <% foo %> are invalid xhtml values, MasterView has an inline substitution to
replace these for the cases when you simply want to inject something. {{{ gets replaced with <%
and {{{= gets replaced with <%= also }}} gets replaced with %> If you instead want to use the
old <% syntax instead, MasterView by default is setup to perform a substitution on those values before
parsing. However to be safe you should use the xhtml safe syntax otherwise html editors might end
up changing things on you.
{{{= foobar }}}
becomes
<%= foobar %>
and
{{{ foo }}}
becomes
<% foo %>
== Background story
I came from a Java Struts environment when I was introduced to Ruby. I
was just in the process of evaluating Tapestry and JSF when I learned about Rails.
Ruby and Rails changed my world forever. I had to however drop many of my bad
habits aquired from years of Java and C++ work. I had to embrace the new ideas
and approach things now with a different mindset. (If you ever have a chance to
hear Dave Thomas speak, you will know what I mean. He has a wonderfully entertaining
yet enlightening way of illustrating this.)
After learning the Ruby and Rails way, (Thank you Matz and DHH for such wonderful
gifts to the world) I was amazed at how productive this environment is. Programming was
fun once again, and everything you do in Ruby or Rails is easier than any other
language or framework I have ever seen. Even taking into account having to learn
something new, I was still far more productive than ever before. The only thing
that I felt that could be done better, was to have a nice html friendly templating
engine so that designers and developers could work collaboratively together on a project.
The designers could style and layout the pages with their WYSIWYG HTML editors and
developers would have the full power of Ruby and Rails without having to jump
through extra hoops to use them. I looked at the available html template engines
to see if anything fit with my style. I was disappointed with each of them, mainly because
they all made it harder than straigt ERb (rhtml) and were not able to use layouts
and partials easily.
After all the hard work so many people have put into Ruby and Rails, I wanted to see
if there was anyway I could contribute as well, maybe extending one of the existing
template projects to have the ideal functionality I was striving for. I wanted
something to make my own web development even more productive. However after
reviewing each of them and even getting into the source code, the closest ones seemed
to be Amrita2, Liquid, and Kwartz. Unfortunately it was not going to be easy to add
the functionality and usability I desired.
During my research I learned that many of the rails gurus were putting down html
template engines because they felt that they handicapped the developer severely,
namely by not taking advantage of rails layouts, partials, and html helpers.
After doing some projects with ERb (rhtml) I realized that they were right. I too
did not want to give up any of that power either. I also didn't want to create a
bunch of extra code or config to use the templates (extra view code, or data
structures to pass to the templates) as so many of the other engines relied on.
I expanded my search outside of Ruby for the best html template technology I could find.
After reviewing all the front runners I still didn't find anything that quite met with my
goals for developing with ruby and rails. I also didn't find any technology that was
close enough that I could adapt it. However I did come across features in a variety of
systems that inspired me, and helped me shape the ideas that are presented here.
After putting much thought into all the possible options I could not see extending
an existing template engine to meet my needs. And since I wanted this engine to have
all the power of ruby and rails, it seemed that I would be best served by developing
a html template syntax that was a thin layer above ERb. That way it would be intuitive
to use (not having to learn yet another syntax) yet very powerful. Once I was able to
come up with the desired syntax and approach it became apparent that this would also
be fairly straightforward to build.
I ran the concept by the guys at the St. Louis Ruby Group http://stlruby.org and
they were excited about the project. With their help I was able to simply some of
the confusing or complicated areas before beginning development. They also sparked
some ideas about how to make this infinitely extensible such that developers could
easily add their own attribute directives. This would give them the power to create
custom extensions that would encapsulate some of their development work. This would
serve a couple needs, it would not only make this engine extensible, it would also
allow this engine to grow easily being composed of small pluggable attribute directives.
Many thanks to the members of this group, I continue to enjoy discussing the power of
Ruby and learning from each of them on a monthly basis.
While I am on the subject of thanks, I would like to thank my wife and daughter for
their support as I took time away from them to work on this project and I want to
thank God for bringing the right people into my life just when I needed them and for
bringing me on this wonderful journey, I am so very blessed. May I be able to give
back some of what has been given to me.
So that's the story, I hope that many of you will find MasterView to be a useful
tool for your development. I am continuing to refine MasterView by using it daily for
Genesis Ministry sites and all my web projects. I know that there is much more work needed
in documentation, examples, test cases, additional directives, etc., so I would
appreciate any feedback and ideas that you have. Thanks for taking the time to
review MasterView!
Jeff Barczewski (jeff.barczewski @ gmail.com)