Write applications using the latest HTML5 and JavaScript technologies, serving them as static HTML to legacy browsers.
mkdir project_name
cd project_name
npm install view
node_modules/view/bin/bootstrap
node app.js
Run this command from your project:
cake watch
This will compile the following stylus and coffeescript files:
- app/stylesheets -> public/stylesheets
- app/models -> public/javascripts/models
- app/collections -> public/javascripts/collections
- app/controllers -> public/javascripts/controllers
- app/views -> public/javascripts/views
- app/templates -> public/javascripts/templates.js
Note that you need to restart the cake watch process when you create new files, this will be automatic in the future.
The View class represents a DOM Element, a set of behaviors associated with it (event handlers, custom initializers, custom methods, etc) and possibly a Model or Collection that is bound to the view. All views are mixins and can be extended and sub classed at any time.
Find a view by name:
ContactView = View 'ContactView'
Passing in name: callback pairs will call the callback with the view as it's context when the view is available:
View
ContactView: ->
@initialize()
Create a named view with the given mixins. You can pass a single mixin object, or an array of mixins. Nested arrays will be flattened.
View.create ParentView:
render: 'parent.eco'
on:
render: ->
console.log 'render event triggered'
View.create BuilderView: [Builder,
render: ->
@div()
]
Calling create on a named view creates a child view that will process all of the mixins of the parent view (effectively cloning it) in addition to the mixins passed into create:
{ChildView} = ParentView.create ChildView:
render: 'child.eco'
on:
render: ->
console.log 'render event triggered'
#render now has two event handlers
Some mixin directives like render will overwrite an existing callback, while others like on will bind additional events without unregistering old events.
Calling create on a named view with no arguments creates an unnamed clone of the view.
anonymous_view = ChildView.create()
All mixins passed to create() are then passed to extend. @extend looks for directives to process the attributes in the mixin (render,initialize,on, etc are all directives). If none are found the attribute becomes an attribute of the view.
{PostView} = View.create PostView:
render: 'post.eco' #processed with render directive
key: 'value'
PostView.key is 'value'
Extend itself can be extended to process new directives. If you view the ViewJS source code you will see this is how ViewJS itself is constructed. The "on" directive is implemented roughly like this:
View.extend extend:on: (events) ->
for event_name, callback of events
@bind event_name, callback
#now "on" can be passed to extend
View.extend
on:
render: ->
console.log 'Called when any view renders'
A discard callback is always passed as the second argument to a directive processor. Call this when a directive should only be processed once and not when child views are created.
View.extend extend:routes: (routes,discard) ->
#process routes once then
discard()
Add an asynchronous initialize callback to the view. The callback must call next or the view will never finish initializing. Note that initialize callbacks are added to a stack of existing callbacks. Child views specifying an initialize callback will not overwrite the parent's callback.
{PostCollectionView} = View.create PostCollectionView:
render: 'post_preview.eco'
collection: PostCollection
initialize: (next) ->
@collection.fetch success: next
Initializes a given view. Optional attributes will be passed to set() and an optional callback will be called when initialize is complete.
$('body').append PostCollectionView
PostCollectionView.initialize ->
console.log 'called when initialize is complete'
Specifies dependent views that will be loaded. Each dependent view add an initialize callback. The parent view will not finish initializing until the dependent views have finished initializing. Once finished the dependent view names will become available as properties of the parent view:
{ApplicationView} = View.create ApplicationView: [Builder,
views: ['PostCollectionView','SideBarView']
render: ->
@div(
@div class: 'main', @PostCollectionView
@div class: 'sidebar', @SideBarView
)
]
Specify a model for the view. Designed with Backbone.Model in mind, but can be any object that has an attributes property. The attributes property of the model will be passed as the context to a template if one is specified for the view. If using a callback with render the model will be accessed as @model.
post = new Backbone.Model title: 'Post Title'
{PostView} = View.create PostView:
render: 'post.eco'
model: post
# post.eco
# <h2><%= @title %></h2>
render() will be called for each model in the collection, with the model as the context to a template, or as the first argument to a callback. Asynchronous collection logic must be put into an initialize callback or a change or change:key event.
{PostCollectionView} = View.create PostCollectionView:
render: 'post_preview.eco'
collection: PostCollection
initialize: (next) ->
@collection.fetch success: next
collection is designed for use with a Backbone.Collection, but any class conforming to the following API will work:
- method: bind(event_name,listener)
- event: all()
- event: add(model)
- event: remove(model)
- event: refresh()
title = PostView.model.get 'title'
Set attributes on a view. By default this will trigger change and change:key events. Pass "{silent:true}" as the second argument to prevent those events from being triggered. Note that these attributes are not persisted to a view's model if present.
View PostView: ->
@on change: -> @render()
@set title: 'Title One' #render() called
@set {title: 'Title Two'}, {silent: true} #render() not called
The name of the view.
Set the DOM library to use (currently jQuery or Zepto are the only supported libraries). Must be set on the base View object. A DOM library is not required to use ViewJS.
View.extend
$: jQuery
When using a DOM library $ is available as a hybrid object and function. The function will act as the selector, and the object will contain all of the attributes of an Element wrapped by the DOM library (hide/show/addClass/etc). Note that the element object and $ method are always available, but the view will not contain any content until initialize has finished.
PostView.$.hide() #hide while loading
PostView.initialize ->
@$.show()
@$('li').addClass 'item'
Generate a DOM Element. Accepts a variable number of hashes of attributes, elements, other views, strings of content or a functions to call, or arrays (which can be nested) of any of the above, in any order. If Builder is passed as a mixin all valid tag names become available as methods.
{TableView} = View.create TableView: [Builder,
render: ->
@table cellpadding: 0, cellspacing: 0,
@tbody(
@tr(
@td 'Cell One'
@td 'Cell Two'
)
)
]
#without Builder
{ListView} = View.create ListView:
render: ->
@tag('ul',@tag('li','Item One'))
If a DOM library is present @tag and Builder methods will instead return a wrapped object containing the element (i.e. a jQuery or Zepto array). View will always look inside these objects for the actual elements so in practice you can use them as if they were the Element objects, with the added benefit of attaching event handlers inline, etc:
@ul @li @a('Link',href:'#').click ->
Specify a template to render:
View.create PostView:
model: post
render: 'post.eco'
or pass a callback. Callbacks work best with Builder, but . Asynchronous logic should be put inside of initialize or a change:key event handler.
View.create PostView: [Builder,
model: post
render: ->
@div class: 'post',
@a('My Link',href: '#').click -> false
@h2 @model.get 'title'
]
render() is automatically called when initialize() has completed. Subsequent calls to render() will replace the contents of the view's element.
View.create PostView:
render: 'post.eco'
on:
change:
id: (id) ->
Post.find id, (post) =>
@model = post
@render()
Adds helper methods to templates:
View.extend helpers:
url: (params) ->
@url params
The outer most DOM Element in the view which is available as soon as the view is created (before initialize or render are called). The Element is never removed even when the view is re-rendered.
$('body').append PostView
PostView.$.html() #empty
PostView.initialize ->
@$.html() #contents of post.eco
Override the default element which is a div with a className containing the name attribute of the view.
{PostCollectionView} = View.create
PostCollectionView: [Builder,
collection: PostCollection
element: ->
@ul()
render: (post) ->
@li post.get 'title'
]
View.create PostView:
delegate:
'click a.class': (event) ->
ViewServer will automatically compile the contents of the templates directory and set this property accordingly, but if using View standalone this property should contain filename: callback pairs. The callbacks must return HTML strings and not DOM Elements. The templates attribute must be set on the base View object.
View.extend
templates:
'post.eco': (attributes) ->
"html output"
The following events are triggered internally by View:
- initialize: -> Triggered when the view is initialized.
- change: -> Triggered when any attribute in the view has changed.
- change:key: (value) -> Triggered when a particular key changes.
- ready: -> Triggered the first time a view is rendered.
- render: -> Triggered when a view is rendered.
- activated: -> Triggered when a route activates the view.
- deactivated: -> Triggered when a view was active, and another view is activated by a route.
- error: (error) -> Triggered when an exception is thrown.
- warning: (warning) -> Triggered when a warning (such as a deprecation) occurs.
Bind a callback to an event. bind and on are aliases.
PostView.bind 'render', ->
console.log 'Post rendered'
An object containing event name, callback pairs can also be used:
PostView.on
render: ->
console.log 'Post rendered'
initialize: ->
console.log 'Post initialized'
The same behavior can be achieved using a mixin:
PostView.extend
on:
render: ->
console.log 'Post rendered'
PostView.unbind 'render', handler #unbinds a particular handler
PostView.unbind 'render' #unbinds all render handlers
PostView.unbind() #unbinds all
Unbinding all is not recommended as there are many internal events.
Trigger a given event with an arbitrary number of arguments.
PostView.bind 'custom', (arg1,arg2) ->
PostView.trigger 'custom', arg1, arg2
A simple implementation of AOP. A logger could be implemented as:
View.extend log: (method_name) ->
@before method_name, (next,args...) ->
response = next.apply @, args
console.log "#{@name}.#{method_name}", args, ' -> ', response
response
View.log 'set'
View.log 'get'
instance = View.create()
instance.set key: 'value'
Allows for the conditional execution of code depending on environment. The following environments are built in:
- server: Executed when the application is being processed by NodeJS
- browser: Executed by a remote client / web browser. Note that this will become "client" in a future release, but is not presently so due to internal usage. Usage of "browser" will continue to work but will issue a deprecation warning in the future.
View.env
server: -> console.log 'only run on the server'
browser: -> console.log 'only run on a browser'
New environment names can be set using the set key:
View.env set:
ie: -> !!window.attachEvent and not window.opera
The env directive can be used in a mixin.
View.extend
env:
ie: ->
@on render: -> console.log 'render called in IE'
The env directive in a mixin, can itself contain a mixin that will be passed to extend.
View.extend
env:
ie:
on:
render: -> console.log 'render called in IE'
This is automatically set by the ViewServer, but when using View standalone you must manually set this before you can use the Router mixin. Attributes matched in a route will be @set on the view when it is activated.
View.extend
routes:
'/about/': 'AboutPageView'
'/contact/': 'ContactPageView'
'/blog/:post_id': 'PostView'
Generates a URL for a given view and attributes. The view must have a corresponding route.
'/post/5' is View.url PostView: id: 5
'/post/5' is PostView.url id: 5
'/' is View.url IndexView: {}
'/' is IndexView.url()
The Router uses the excellent History.js to manage the history state in the browser. The recommended behavior is to serve browsers incapable of supporting the HTML5 history API a static HTML snapshot of the view. The ViewServer provides the legacy and html5 directives to @env to easily allow this.
When a view is activated by a route the deactivated event is triggered on the previous activated view, and the activated event is triggered on the newly active view. Client side the deactivated view is hidden and the activated view is shown. Server side, all views but the activated one will be removed from the DOM before it is serialized.
Each application that uses Routes must mixin the Router once. The Router mixin makes available @Router which will contain a div, which itself contains all of the elements of the routed views.
View.extend
routes:
'/about/': 'AboutPageView'
'/contact/': 'ContactPageView'
'/blog/:post_id': 'PostView'
View.create
ApplicationView: [Router,
render: ->
@div @Router
]
AboutPageView:
render: 'about.eco'
ContactPageView:
render: 'contact.eco'
PostView:
render: 'post.eco'
on:
change:
post_id: (post_id) ->
Post.find post_id, (post) =>
@model = post
@render()
The builder mixin adds all valid HTML5 tag names as methods to a view. Usage of Builder is covered in the tag method.
{TableView} = View.create TableView: [Builder,
render: ->
@table cellpadding: 0, cellspacing: 0,
@tbody(
@tr(
@td 'Cell One'
@td 'Cell Two'
)
)
]
Extends an Express server to serve the assets of a View based application, or execute the View application server side, serving the resulting DOM as static HTML.
Find a ViewServer by name:
BlogServer = View 'BlogServer'
Passing in name: callback pairs will call the callback with the ViewServer as it's context when the ViewServer is available:
ViewServer BlogServer: ->
@server.use express.logger()
The ViewServer's mixin system and create method works identically to View's.
ViewServer.create BlogServer: ->
port: 3001
public: __dirname + '/public'
The Express instance which handles requests for the ViewServer.
express = require 'express'
BlogServer.server.use express.logger()
BlogServer.server.get '/custom/', (request,response) ->
If no server attribute is supplied, an Express server will automatically be created. Or you can supply a custom one:
express = require 'express'
server = express.createServer()
{BlogServer} = ViewServer.create
server: server
The port number to listen on. When both port and public have been defined in the server @server.listen() will be called.
Full path to the public directory. When both port and public have been defined in the server @server.listen() will be called.
Full path to the templates directory. All eco and Jade templates in this directory will automatically be compiled to public/javascripts/templates.js when running the cake watch command.
JavaScript tags to send to the browser on each request. If a directory is specified it will be recursively searched for .js files.
BlogServer.extend javascripts: [
"public/javascripts/lib/view.js"
"public/javascripts/models/"
"public/javascripts/views/"
]
JavaScript to execute server side on each request. If a directory is specified it will be recursively searched for .js files.
BlogServer.extend execute: [
"public/javascripts/lib/view.js"
"public/javascripts/models/"
"public/javascripts/views/"
]
Add stylesheets to. If a directory is specified it will be recursively searched for .css files.
BlogServer.extend stylesheets: [
"public/stylesheets/a.css"
"public/stylesheets/b.css"
#now scan the directory for all remaining stylesheets
"public/stylesheets/"
]
Inject arbitrary meta information into the document head.
BlogServer.extend meta: [
'<link rel="alternate" type="application/rss+xml" title="Feed Name" href="url" />'
]
TODO
Execute code, or process mixin directives conditionally per request. The following environments are pre-defined:
- html5: If the user-agent supports the HTML5 history API.
- legacy: If the user-agent does not support the HTML5 history API.
View.env request,
html5: ->
console.log 'only run when an HTML5 capable user agent initiated the request'
legacy: (request) ->
console.log 'only run when a non HTML5 capable user agent initiated the request'
The following mixin directives can be used in an env directive:
- execute
- javascripts
- stylesheets
- meta
In the following example the Typekit JavaScript would be available in a script tag in the header for both legacy and HTML5 capable browsers, but the rest of the application would be run server side for legacy browsers, and client side for HTML5 browsers.
application_payload = [
"public/javascripts/lib/jquery.js"
"public/javascripts/lib/underscore.js"
"public/javascripts/lib/backbone.js"
"public/javascripts/lib/view.js"
"public/javascripts/templates.js"
"public/javascripts/models"
"public/javascripts/views"
]
{MyServer} = ViewServer.create MyServer:
public: __dirname + '/public'
javascripts: [
"http://use.typekit.com/xxx.js"
]
env:
legacy:
execute: application_payload
html5:
javascripts: application_payload
Define a new environment. Request may not be present, so always check for request? first.
ViewServer.env set:
ios: (request) ->
request? and request.headers['user-agent'].match /(iPhone|iPad)/
Not all environments depend on the request object. The following example sets development and production environments based on wether or not the code is run in the VMWare Cloud Foundry offering:
ViewServer.env set:
production: ->
process?.env?.VCAP_APPLICATION
development: ->
not process?.env?.VCAP_APPLICATION
The following events are triggered internally by ViewServer:
- error: (error) -> Triggered when an exception is thrown.
- warning: (warning) -> Triggered when a warning (such as a deprecation) occurs.
- request: (request,response) -> Triggered when an incoming request will be handled by a view.
Bind a callback to an event. bind and on are aliases.
BlogServer.bind 'request', (request,response) ->
console.log 'Incoming request'
An object containing event name, callback pairs can also be used:
BlogServer.on
request: (request,response) ->
console.log 'Incoming request'
The same behavior can be achieved using a mixin:
BlogServer.extend
on:
request: (request,response) ->
console.log 'Incoming request'
BlogServer.unbind 'error', handler #unbinds a particular handler
BlogServer.unbind 'error' #unbinds all error handlers
BlogServer.unbind() #unbinds all
Trigger a given event with an arbitrary number of arguments.
BlogServer.bind 'custom', (arg1,arg2) ->
BlogServer.trigger 'custom', arg1, arg2
Will register get handlers in express that will serve or execute JavaScript depending on the contents of the execute and javascripts directives, and will pass through route definitions to View.routes automatically.
BlogServer.extend
routes:
'/': 'IndexView'
'/post/:id': 'PostView'
You can optionally pass a callback for low level control just like in Express. Specifying a port number will proxy all requests below that path to the given port number, specifying a fully qualified URL will proxy all requests below that path to the given URL.
BlogServer.extend
routes:
'/local_api/': 3002
'/remote_api/': "http://api.service.com/"
'/page/:name': (request,response) ->
response.send 'Page Contents'
# reqests to:
# /local_api/posts/2.json would proxy the request to: localhost:3002/posts/2.json
# /remote_api/posts/2.json would proxy the request to: http://api.service.com/posts/2.json
TODO