Skip to content

Views and Templates

Lance Pollard edited this page Sep 25, 2012 · 4 revisions

Tower has built in support for every template framework using mint.js.

  • HTML5 Bootstrap
  • Twitter Bootstrap

Client

The client doesn't have layouts (at least for now). In Ember there is the concept of layouts, so you can use them there.

Helper Methods

  • HTML5 tags
  • formFor
  • tableFor
  • partial
  • render
  • yields
  • contentFor
  • hasContentFor

Components

  • 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.

Configuration

Tower allows configuration of some components:

Tower.View.configure
  headerClass:        "header"
  titleClass:         "title"
  subtitleClass:      "subtitle"
  defaultHeaderLevel: 3

Form Builder

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>

Form Internals

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')

Form fields

field 'firstName', as: 'string'
field 'email', as: 'email'
field 'password', as: 'password'

Form Best-Practices

  • If you need to divide your forms up into columns or vertical sections, use the <fieldset> tag.
  • Give your <label> element a for 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.

Dynamic Admin Forms

Potential for something as simple as:

formFor '{{metadata.toParam}}', (f) ->
  f.fields (fields) ->
    hEach 'fields', ->
      fields.field '{{name}}', as: '{{type}}'

Don't use these patterns

form ->
  p ->
    label
    input
  label ->
    input

View Helpers

Global Helpers

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/.

Handlebars (Ember) Helpers

hEach

hEach 'App.postsController.all', ->
  li '{{title}}'

Asset Helpers

javascripts

stylesheets

Component Helpers

formFor

tableFor

Meta Helpers

appleMetaTags

appleViewportMetaTag

openGraphMetaTags

Custom Helpers

You also have access to any helpers you've included in Tower.View:

Tower.View.helper(App.ApplicationHelper)

Layouts

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"

Lists and Menus

Lists

ul ->
  li class: 'active', ->
    a href: '/', 'Dashboard'
  li ->
    a href: '/users', 'Users'
  li ->
    a href: '/posts', 'Posts'

List Items

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 ->

Definition Lists

section id: 'user-page', class: 'page', ->
  header class: 'header', ->
    h2 'User'
  div class: 'content', ->
    dl ->
      dt 'Email'
      dd ->
        span '[email protected]'
  footer class: 'footer', ->

Navigation

  • set the li class to active (not current or whatever)
nav ->
  header ->
  ul ->
    li ->
      a ->

Meta Tags

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='&copy; 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" />

View State

Navigation

<ul>
  <li {{App.postsController.isActive:active}}>
    <a href="/posts">Posts</a>
  </li>
  <li {{App.commentsController.isActive:active}}>
    <a href="/comments">Comments</a>
  </li>
</ul>

Popups

Content

Table Builder

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">&#8981;</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">&#8676;</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">&#8672;</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">&#8674;</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">&#8677;</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>

Dynamic Admin Tables

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 ->

Tables

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', ->

Templates

CoffeeKup

Automatic Template Refreshing in Browser

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.

Examples

Coffeekup + Mustache

handlebars 'each', 'posts', ->
  h2 '{{title}}'
  p '{{body}}'
{{#each posts}}
  <h2>{{title}}</h2>
  <p>{{body}}</p>
{{/posts}}

Widgets

API

Headers

Overview

Headers rendered inside of a widget look like this:

With a subtitle:
<header class='header'>
  <hgroup>
    <h3 class='title'></h3>
    <h4 class='subtitle'></h4>
  </hgroup>
</header>

Without a subtitle:
<header class='header'>
  <h3 class='title'></h3>
</header>

Configuration Options

Usage

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

Examples

List Widget

<nav>
  <header class='header'>
    <h3 class='title'></h3>
    <h4 class='subtitle'></h4>
  </header>
  <ul class='content list'>
    <li class='list-item'>
  
    </li>
  </ul>
</nav>
Clone this wiki locally