Views written in Coffee-Script 08 Mar 2012
Using an unobtrusive syntax in combination with an expressive language allows the development of Domain specific languages (DSL) for specialized problems.
On problem that can be solved with a DSL is creating templates.
A lot of different template engines are available. While these template engines mature more and more language concepts are added to satisfy the users. And a lot of template engines try to recreate the languages they are created in. An great example of this is smarty which tries to replicate features of php. PHP is a document oriented programming languages (the code is embedded into a regular html document), which can be seen as a template engine, too. ;)
This tutorial motivates the subject of writing template code in the currently used programming language itself.
Coffee-Script, by chance, has a Libraries that allow to develop views in the same language.
The obvious advantages of this are:
As the template code is written in the applications language of choice the developers have to know one language less. As views are code the whole discipline of software engineering can be applied to the view development, too.
Aim of this tutorial
- Creating an decoupled view api
- Creating an easy to use api for views
- Explaining how one can structure the code of the views with coffee-script
Available libraries
I started developing with drykup. I have used this library in a few projects and had no complaints that far. I recommend to use this library for starters. As the code of the library is pretty awful, I implemented the same library myself. The result is plainkup, this library should be working, too. But plainkup is not field tested, yet.
Usage
To create a view one has to create an instance of the builder
The builder
has all available html elements (builder.a
, builder.div
, …).
Executing a method of the builder appends it to the internal model.
The internal model can be retrieved with builder.htmlOut
builder.a href: '/login', 'Go to Login'
// appends <a href="/login">Go to Login</a> to `builder`
The first parameter is an object with all attributes of the element
builder.div a: 'x', b: 'y', 'text'
// output: <div a="x" b="x">text</div>
The last parameter is either a text which is filled between the tags. If the last parameter is a function the function is evaluated within the context of the tag.
builder.div ->
builder.p 'foo'
builder.p 'bar'
// <div><p>foo</p><p>bar</p></div>
Nesting is unlimited
div -> div -> div -> div 'hey'
// <div><div><div><div>hey</div></div></div></div>
The above gives us the technical knowledge of how to write view code in coffee-script. Now we need to think of a way of structuring our view code. Our aim is to reduce code duplication and make software engineering a bit more pleasant.
View Interface
All views (partial or not) should have the same signature:
greeting = (builder, params) ->
name = params.name
{div, p} = builder
div ->
p "hello #{name}"
p "how are you?"
The function greeting
receives two parameters: An initialized builder
(builder = drykup()
) and an object which contains the parameters.
We inject the builder in each view, because this gives us nesting for free:
# params.names = ['foo', 'bar', 'baz']
greetPeople = (builder, params) ->
_.each params.names, (name) -> greeting builder, name: name
# hello foo, hello bar, hello baz..
I structured my views directory in the following way
/view/:controller/:action.coffee
For example:
/view/user/all.coffee # view for the list of all users
/view/user/edit.coffee # view for editing a user
/view/user/foo.coffee # random partial which is used by the `user` views
All action have the same signature (builder, params)
.
Adding a Layout
Most of the time web applications have a default layout.
A Layout is the view code that is wrapped around each page
- header
- includes
- footer
This layout takes a menu and a view and creates a complete view.
This is an example of a default layout:
layout = (view, menu) -> (builder, params) ->
{head, script, ...} = builde
head ->
script ...
...
body ->
div class: container', ->
#initializing the injected `menu` in the default layout
menu builder, params
# add a flash message
alert builder, params.flash
# print the default page
view builder params
The layout has the same signature as all views and partials.
Wrapping it together
The most convenient way of accessing the views is the following
params = getAllRequiredParamsForTheView()
result = view.user.edit params
# result is a string containing the complete rendered view
If we implement this interface, all implementation details are hidden behind the interface and the view code does only depend on the submitted parameters. This allows us to implement a clean and decoupled view interface.
As all views have the same interface and we have the layout, we just need to wrap everything together:
view-api.coffee:
view = require './view' // all views
layout = require './layout' // the default layout signature:
addLayout = (view) -> (params) ->
builder = drykup()
layout(view, menu) builder, params
builder.htmlOut
module.exports =
login: addLayout view.login
logout: addLayout view.logout
user:
all: addLayout api.user.all
...