-
Notifications
You must be signed in to change notification settings - Fork 120
Views and Templates
Tower has built in support for every template framework using mint.js.
- HTML5 Bootstrap
- Twitter Bootstrap
The client doesn't have layouts (at least for now). In Ember there is the concept of layouts, so you can use them there.
- HTML5 tags
- formFor
- tableFor
- partial
- render
- yields
- contentFor
- hasContentFor
- Forms
- Tables
- Menus and lists
- Breadcrumbs
- Definition lists
- Generic "widgets"
A lot of thought was put into figuring out the conventions behind most of the HTML components we use. Where applicable, you can opt-into ARIA-roles as well. The structures all have sensible defaults, but they can be globally configured no problem.
Tower also includes several "meta-level" helpers, such as:
- Internet explorer hacks to build things like vertically/horizontally centered images (where the width of the image is unknown), and to apply CSS sprites to rich buttons for IE6. You need the structure
<div><span><span>[<input/>|<img/>]</div>
to do this. - User agent helpers (to perform human-readable checks on the browser, operating system, etc.).
- HTML "head" helpers (meta tags, titles, keywords, links, etc.).
- Fragment caching helpers, like conditional caching.
- An extension to the building in
t()
I18n method to allow past/present/future and zero/one/many keys and several more options.
Tower allows configuration of some components:
Tower.View.configure
headerClass: "header"
titleClass: "title"
subtitleClass: "subtitle"
defaultHeaderLevel: 3
formFor @user, (form) ->
form.fieldset 'Profile', (fields) ->
fields.field 'firstName'
fields.field 'lastName'
fields.field 'email'
form.fieldset 'Address', (fields) ->
fields.field 'lat', as: hidden
fields.field 'lng', as: hidden
fields.field 'street'
fields.field 'city', as: 'select', collection: ['CA']
<form class='form' data-method='post' method='post' novalidate='true' role='form'>
<fieldset class='fieldset' id='profile'>
<legend class='legend'>
<span>Profile</span>
</legend>
<ol class='field-list'>
<li class='field string optional validate'>
<label class='label' for='active-record-user-first-name-input'>
<span>First Name</span>
<abbr class='optional' title='Optional'></abbr>
</label>
<input accesskey='f' class='string first-name optional input validate' data-validate='presence' data-validates-presence-message="can't be blank" data-validates-presence='true' id='active-record-user-first-name-input' maxlength='255' name='activeRecordUser[firstName]' type='string' value='Lance' />
<output class='error'></output>
</li>
<li class='field string optional validate'>
<label class='label' for='active-record-user-last-name-input'>
<span>Last Name</span>
<abbr class='optional' title='Optional'></abbr>
</label>
<input accesskey='l' class='string last-name optional input validate' data-validate='presence' data-validates-presence-message="can't be blank" data-validates-presence='true' id='active-record-user-last-name-input' maxlength='255' name='activeRecordUser[lastName]' type='string' value='Pollard' />
<output class='error'></output>
</li>
<li class='field email optional validate'>
<label class='label' for='active-record-user-email-input'>
<span>Email</span>
<abbr class='optional' title='Optional'></abbr>
</label>
<input accesskey='e' class='email string optional input validate' data-validate='presence' data-validates-presence-message="can't be blank" data-validates-presence='true' id='active-record-user-email-input' maxlength='255' name='activeRecordUser[email]' type='email' value='[email protected]' />
<output class='error'></output>
</li>
</ol>
</fieldset>
<fieldset class='fieldset' id='address'>
<legend class='legend'>
<span>Address</span>
</legend>
<ol class='field-list'>
<li class='field hidden optional'>
<input accesskey='l' class='hidden lat optional input' id='active-record-user-lat-input' name='activeRecordUser[lat]' type='string' />
</li>
<li class='field hidden optional'>
<input accesskey='l' class='hidden lng optional input' id='active-record-user-lng-input' name='activeRecordUser[lng]' type='string' />
</li>
<li class='field string optional'>
<label class='label' for='active-record-user-street-input'>
<span>Street</span>
<abbr class='optional' title='Optional'></abbr>
</label>
<input accesskey='s' class='string street optional input' id='active-record-user-street-input' name='activeRecordUser[street]' type='string' />
<output class='error'></output>
</li>
<li class='field select optional'>
<label class='label' for='active-record-user-city-input'>
<span>City</span>
<abbr class='optional' title='Optional'></abbr>
</label>
<select accesskey='c' class='select city optional input' id='active-record-user-city-input' name='activeRecordUser[city]'>
<option value='CA'>CA</option>
</select>
<output class='error'></output>
</li>
</ol>
</fieldset>
</form>
The formFor
helper is pretty much just doing this:
form action: urlFor(@user), method: 'post', ->
fieldset class: 'fieldset', ->
ul class: 'fields', ->
li class: 'field', ->
label for: 'user-first-name-input', 'First Name'
input id: 'user-first-name-input', type: 'text', value: @user.get('firstName')
field 'firstName', as: 'string'
field 'email', as: 'email'
field 'password', as: 'password'
- If you need to divide your forms up into columns or vertical sections, use the
<fieldset>
tag. - Give your
<label>
element afor
attribute value. On one hand this is for accessibility, so the computer can tell blind people what the label is for the input in focus. On the other hand, it makes it so if you click on a label, the browser will focus on the input, so it kind of wires them together. - Use the
<legend>
tag to give your form, or a fieldset, a header. - If all inputs are required, don't mark them all with an asterisk
*
or whatever. Instead, mark the few ones that aren't required with(Optional)
. - Set
<form novalidate='true'>
, otherwise the browser will validate the input based on it's type (`') and it most likely won't be styled the way you style your custom validations.
Potential for something as simple as:
formFor '{{metadata.toParam}}', (f) ->
f.fields (fields) ->
hEach 'fields', ->
fields.field '{{name}}', as: '{{type}}'
form ->
p ->
label
input
label ->
input
In your .coffee
templates you have access to a few global variables:
Tower
_ # underscore library
App # your namespace
Tower has also defined a bunch of helpers, found in Tower's source in ./src/tower/view/helpers/
.
hEach 'App.postsController.all', ->
li '{{title}}'
You also have access to any helpers you've included in Tower.View
:
Tower.View.helper(App.ApplicationHelper)
doctype 5
html ->
head ->
partial "shared/meta"
body role: "application", ->
if hasContentFor "templates"
yields "templates"
nav id: "navigation", role: "navigation", ->
div class: "frame", ->
partial "shared/navigation"
header id: "header", role: "banner", ->
div class: "frame", ->
#if hasFlash()
# renderFlash()
partial "shared/header"
section id: "body", role: "main", ->
div class: "frame", ->
yields "body"
aside id: "sidebar", role: "complementary", ->
if hasContentFor "sidebar"
yields "sidebar"
footer id: "footer", role: "contentinfo", ->
div class: "frame", ->
partial "shared/footer"
if hasContentFor "popups"
aside id: "popups", ->
yields "popups"
if hasContentFor "bottom"
yields "bottom"
ul ->
li class: 'active', ->
a href: '/', 'Dashboard'
li ->
a href: '/users', 'Users'
li ->
a href: '/posts', 'Posts'
Here are some examples of how to do those richer list items like you'd see in a gallery.
li class: 'item', ->
header ->
div ->
footer ->
section id: 'user-page', class: 'page', ->
header class: 'header', ->
h2 'User'
div class: 'content', ->
dl ->
dt 'Email'
dd ->
span '[email protected]'
footer class: 'footer', ->
- set the
li
class toactive
(notcurrent
or whatever)
nav ->
header ->
ul ->
li ->
a ->
meta 'title', 'keywords', 'description', 'copyright'
link 'pagination'
<meta name='description' content='140 characters' />
<meta name='keywords' content='ruby, jquery, node.js' />
<meta name='copyright' content='© 2011 Lance Pollard. All rights reserved.' />
<meta name='robots' content='noodp,noydir,index,follow' />
<meta name='og:title' content='Storefront' />
<meta name='og:site_name' content='Storefront' />
<meta name='og:url' content='http://storefront.com' />
<meta name='og:image' content='http://storefront.com/images/logo.png' />
<meta name='og:description' content='2 sentences' />
<link rel="first" href="http://site.com/users" title="Users - Page 1" />
<link rel="prev" href="http://site.com/users?page=5" title="Users - Page 5" />
<link rel="next" href="http://site.com/users?page=7" title="Users - Page 7" />
<link rel="last" href="http://site.com/users?page=10" title="Users - Page 10" />
<ul>
<li {{App.postsController.isActive:active}}>
<a href="/posts">Posts</a>
</li>
<li {{App.commentsController.isActive:active}}>
<a href="/comments">Comments</a>
</li>
</ul>
tableFor 'users', (t) ->
t.head ->
t.row ->
t.header 'name', width: 400, sort: true
t.header 'createdAt', sort: true
t.body ->
for user in users
t.row ->
t.cell ->
linkTo(user.name, adminUserPath(user))
t.cell ->
time user.createdAt
t.foot ->
t.row ->
t.cell colspan: 2, ->
render partial: 'shared/pagination', locals: {collection: @users}
<table class='data-table' data-for='users' data-url='/admin/users' id='users-table' role='grid' summary='Table for Users'>
<thead>
<tr role='row' scope='row'>
<th abbr='name' aria-sort='none' class='sortable' id='users-header-1-0' role='columnheader' scope='col' width='400px'>
<a href="/admin/users?sort=name+">Name</a>
</th>
<th abbr='createdAt' aria-sort='none' class='sortable' id='users-header-1-1' role='columnheader' scope='col'>
<a href="/admin/users?sort=createdAt+">Created At</a>
</th>
</tr>
</thead>
<tbody>
<tr class='odd' role='row' scope='row'>
<td headers='users-header-1-0' id='users-cell-1-0' role='gridcell'>
<a href="/admin/users/2288">Lance Pollard</a>
</td>
<td headers='users-header-1-1' id='users-cell-1-1' role='gridcell'>
<time>5/29/2011 @ 03:44pm</time>
</td>
</tr>
<tr class='even' role='row' scope='row'>
<td headers='users-header-1-0' id='users-cell-2-0' role='gridcell'>
<a href="/admin/users/2287">John Smith</a>
</td>
<td headers='users-header-1-1' id='users-cell-2-1' role='gridcell'>
<time>5/29/2011 @ 03:40pm</time>
</td>
</tr>
</tbody>
<tfoot>
<tr role='row' scope='row'>
<td colspan='3' headers='users-header-1-0' id='users-cell-1-0' role='gridcell'>
<nav class='paginator' role='toolbar'>
<ul class='goto-pages'>
<li class='goto-search'>
<a href="#search" class="search-pages" title="Toggle Advanced Search">⌕</a>
</li>
<li class='goto-page'>
<a href="/admin/users?page=1" aria-disabled="true" class="first-page disabled" data-page="1" rel="first" title="Go to the first page">⇤</a>
</li>
<li class='goto-page'>
<a href="/admin/users?page=1" aria-disabled="true" class="prev-page disabled" data-page="1" rel="prev" title="Go to page 1">⇠</a>
</li>
<li aria-valuemax='109' aria-valuemin='1' aria-valuetext='1' class='goto-page current-page' role='spinbutton'>
<span>Page</span>
<input class='current-page-input' value='1'>
<span>of</span>
<span class='page-count'>109</span>
</li>
<li class='goto-page'>
<a href="/admin/users?page=2" aria-disabled="false" class="next-page yes" data-page="2" rel="next" title="Go to page 2">⇢</a>
</li>
<li class='goto-page'>
<a href="/admin/users?page=109" aria-disabled="false" class="last-page yes" data-page="109" rel="last" title="Go to the last page">⇥</a>
</li>
</ul>
<output class='record-count'>
<span>Viewing</span>
<span class='current-record-range'>1 - 20</span>
<span>of</span>
<span class='total-record-count'>2178</span>
</output>
</nav>
</td>
</tr>
</tfoot>
</table>
You can build a table that works for any model very easily (todo, since you can't totally do this yet in Handlebars):
hWith 'App.User', ->
tableFor '{{name}}', (t) ->
t.head ->
t.row ->
hEach 'fields', ->
t.header '{{name}}', sort: true
t.body ->
hEach 'all', ->
t.row ->
hEach 'fields', ->
t.cell '{{get(name)}}'
t.foot ->
section id: 'users-page', class: 'page', ->
header class: 'header', ->
h2 'Users'
div class: 'content', ->
tableFor 'users', (t) ->
t.head ->
t.row ->
t.header 'First Name'
t.header 'Last Name'
t.header 'Email'
t.body ->
for user in @users
t.row class: 'user', 'data-id': user.get('id').toString(), ->
t.cell class: 'first-name', -> user.get('firstName')
t.cell class: 'last-name', -> user.get('lastName')
t.cell class: 'email', -> user.get('email')
footer class: 'footer', ->
When you save a template, you're either saving a template, partial, or layout.
- If you save a layout, it will reload the html page.
- If you save a template, it will go to the url for that template. If you have the templates in view it will render without refreshing. If history.pushState is available then the url will change as well. This triggers the controller to run the render method again.
- If you save a partial, it will use jQuery to find/replace the partial in the cached version in the browser, and rerun the controller action if that partial was used in the last controller action.
handlebars 'each', 'posts', ->
h2 '{{title}}'
p '{{body}}'
{{#each posts}}
<h2>{{title}}</h2>
<p>{{body}}</p>
{{/posts}}
Headers rendered inside of a widget look like this:
<header class='header'>
<hgroup>
<h3 class='title'></h3>
<h4 class='subtitle'></h4>
</hgroup>
</header>
<header class='header'>
<h3 class='title'></h3>
</header>
widget "xyz", title: "I'm a title", # h3, or h#{defaultHeaderLevel}
widget "xyz", title: "I'm a title", subtitle: "I'm a subtitle", # h3, h#{defaultHeaderLevel + 1}
widget "xyz", headerHtml: {class: "my-header"}, titleHtml: {class: "menu-title"}, subtitleHtml: {class: "sub-title"}
widget "xyz", title: "I'm a title", level: 1 # h1
<nav>
<header class='header'>
<h3 class='title'></h3>
<h4 class='subtitle'></h4>
</header>
<ul class='content list'>
<li class='list-item'>
</li>
</ul>
</nav>