Skip to content
TheHydroImpulse edited this page Sep 30, 2012 · 7 revisions

Tower.js Controller Docs.

Controller request Object (on the Server)

On the server, the request object is straight from Express. Here is the api:

# get raw header value
request.get('content-type')
# set raw header value
request.set('content-type', 'application/json; charset=utf-8')
# get content types accepted by client
request.accepted
# get character encodings accepted by client
request.acceptedCharsets
# get languages accepted by client
request.acceptedLanguages
# check if a specific content type is accepted by the requesting client
request.accepts('html')
# check if charset is accepted by client
request.acceptsCharset('utf-8')
# check if connected client accepts a specific language
request.acceptsLanguage()
# get param value (or default if missing)
request.param('createdAt', _(2).days().ago())
# check if request is of a specific content-type
# note, technically the `content-type` header is only valid
# for PUT and POST, not GET (and not sure about DELETE).
request.is('html')
# get protocol ("http" or "https")
request.protocol
# check if request is "https"
request.secure #=> true|false
# get remote ip address
request.ip
# get auth credentials from the "Authorization" header
# "http://tobi:[email protected]"
request.auth #=> { username: 'tobi', password: 'hello' }
# get array of subdomains
request.subdomains
# get host from "Host" header
request.host
# Check if the request is fresh, aka
# - Last-Modified and/or the ETag
# - still match.
request.fresh
request.stale #=> !request.fresh
# Check if it's an Ajax request via the 'X-Requested-With' header
request.xhr
# csrf
request.session._csrf
# In the middleware:
app.use express.csrf(value: (request) -> request.body._csrf || request.query._csrf)
# https://developers.google.com/blogger/docs/2.0/json/performance
app.use express.compress filter: (request, response) ->
  /json|text|javascript/.test(response.get('content-type'))
# Limit request bodies to the given size in `bytes`.
app.use express.limit('5.5mb')

If the requesting client does not accept one of your encodings or languages, then respond with:

406 "Not Acceptable"

Rate Limiting

Controller response Object (on the Server)

response.status(200)
# Set Link header field with the given `links`.
response.links
  next: 'http://api.example.com/users?page=2'
  last: 'http://api.example.com/users?page=5'
# Write to the response body,
# which is taken care of in the `render` method already.
response.send(new Buffer('wahoo'))
response.send(some: 'json')
response.send('<p>some html</p>')
response.send(404, 'Sorry, cant find that')
response.send(404)
# JSON
response.json(some: 'json')
# JSONP
response.jsonp(some: 'json')
# Send file for download, options are:
# - `maxAge` defaulting to 0
# - `root`   root directory for relative filenames
response.sendfile(path, options)
# Send file as attachment (wraps `sendFile`, sets "Content-Disposition" header)
response.download(path, options)
# Set "Content-Type" header through mime-lookup
response.type('html') # or `response.contentType = 'html'`
# Set response header
response.set('Content-Type', 'application/json; charset=utf-8')
# Get response header
response.get('Content-Type')
# Some attachment thing?
response.attachment(filename)
# Remove cookie
response.clearCookie(name)
# Save cookie
# "Remember Me" for 15 minutes
response.cookie('rememberme', '1', expires: new Date(Date.now() + 900000), httpOnly: true)
# save as above
response.cookie('rememberme', '1', maxAge: 900000, httpOnly: true)
# redirect
response.redirect(url)
# render template
response.render(fn)

Controller Actions

By convention there are 7 RESTful actions for a controller:

  1. index: Show all resources (GET)
  2. new: Build a new resource and render a form (GET)
  3. create: Create a new resource (POST)
  4. show: Find and show a resource by id (GET)
  5. edit: Find a resource by id and render it in a form to edit (GET)
  6. update: Update an existing resource (PUT)
  7. destroy: Destroy an existing resource (DELETE)

If you wanted to manually write out simple controller actions, this is how you might do it:

class App.PostsController extends Tower.Controller
  index: ->
    App.Post.all (error, posts) =>
      @render "index", locals: {posts}
    
  new: ->
    post = new App.Post
    @render "new", locals: {post}
    
  create: ->
    App.Post.create @params.post, (error, post) =>
      @redirectTo post
    
  show: ->
    App.Post.find @params.id, (error, post) =>
      @render "show", locals: {post}
    
  edit: ->
    App.Post.find @params.id, (error, post) =>
      @render "edit", locals: {post}
    
  update: ->
    App.Post.find @params.id, (error, post) =>
      post.updateAttributes @params.post, (error) =>
        if error
          @render "edit", locals: {post}
        else
          @redirectTo post
    
  destroy: ->
    App.Post.find @params.id, (error, post) =>
      post.destroy (error) =>
        @redirectTo "index"

Note: The above actions won't respond to different content types differently (i.e. for JSON, give me a JSON string, for HTML give me some rendered HTML). But for some cases that's all you need.

In order for those actions to be accessible, routes must be defined. Routes to all 7 RESTful actions are generated with the following declaration in config/routes.coffee:

Tower.Route.draw ->
  @resources "posts"

More on routes in the routes section.

Custom Actions

Sometimes RESTful actions aren't enough. To add custom actions, just add a route mapping to the method name for the controller.

Tower.Route.draw ->
  @resources "posts", ->
    @get "dashboard"

Note: If you start thinking you need to add a custom action to your controller, I personally recommend asking the community (stack overflow, twitter, github, etc.) if there's a way to fit it into the paradigm. Several times I thought "this is the one time I need a custom action", but 99% of the time I found a way to fit it in. This always simplified the system.

# on the client it has to deserialize `page: 10` back into a hash from a string.
Tower.goTo(Tower.urlFor(App.Post, page: 10))
# keeps the params as a hash, and appends them to the hash, a more optimized way to do it.
Tower.goTo(Tower.urlFor(App.Post), page: 10)

On the client there is no middleware (at least yet). It just complicates things. Should probably get rid of the idea of Tower.HTTP.Request and Tower.HTTP.Response.

Single Page App Flow

The Server

When a user first comes to your site they can come from any url, such as:

/
/posts
/posts/10

The server tells us the request is coming from a specific user agent and that the desired format is HTML.

But, since your app works through primarily a JSON API, it should return the same base HTML layout no matter what the request is. And ideally, it should return a base set of data in addition to the data for the specific url they're accessing.

Say you're imagining if they go to the root / path, it will bootstrap the current user and the recent posts into the HTML response. That means youre root route, which by default points to application#welcome should bootstrap that data.

Now the thing is, conceptually, you're probably going to build out your single page client app assuming you'll always have that bootstrapped data available. That means we need to always render that bootstrap data when a user first enters your site, no matter what the path.

To do this, all you need is to have your bootstrap data function be run before any action.

This introduces another complexity though. What if you want to render a path, such as /faq, and want it to just to render static HTML like you would in Rails? All you need to do is say @skipBeforeFilter 'boostrap', only: 'faq'. This is assuming you use the default controller action implementation, too. If you write your own controller action, you just respond to html only.

The Client

Once you get to the client, you have your bootstrap data that's the same for any path, and the data that's specific to the path.

What happens now is the App.stateManager traverses it's states based on the routes, culminating in a call to a controller action on the client. The controller action will, by default, render the Ember view for that action.

Controller Callbacks

class App.PostsController extends App.ApplicationController
  @beforeAction 'check'

Client-side Controllers

[todo, just notes so far / partial implementation in development branch]

Overview

Client-side controllers will, like server-side controllers, have a default implementation that just works for the 7 common actions.

Differences between client and server controllers

  • server-side controllers have to respond to different formats (json, html, xml, etc.), client-controllers don't.
  • client-side controllers can handle DOM events, keyboard events, socket messages, and urls (using history pushstate), server-controllers only handle urls and socket messages.

The index action.

On the server, the index usually fetches a collection of models matching a set of criteria from the database, and renders them into JSON or HTML. Pretend our server-side index action looks like this:

# server-side controller
class App.UsersController extends App.ApplicationController
  index: ->
    App.User.where(createdAt: '>=': _(2).days().ago()).all (error, users) =>
      @render "users/index", locals: {users: users}

On the client, when using Ember.js, we want to somehow cache that collection that was returned from App.User.where....

You could do that by refactoring it to a separate method:

# client-side controller
class App.UsersController extends App.ApplicationController
  users: ->
    @_users ||= App.User.where(createdAt: '>=': _(2).days().ago()).all
    
  index: ->
    @render "users/index", locals: {users: @users}

Assuming the above example was on the client, and assuming App.User...all returned a users array, then our index action on the client would always render the initial set of users in our client-side store. If we added users to the client store, we wouldn't see those updates. (Granted, you could use the first example server-side controller on the client, and you would get the updated users collection, because you're making the query every time, but that's not what we're focusing on).

This is where you start using Ember.js. One example is to use computed properties, which allows you to define basically a getter/setter:

# client-side controller
class App.UsersController extends App.ApplicationController
  users: Ember.computed(->
    App.User.where(createdAt: '>=': _(2).days().ago()).all
  ).cacheable()
    
  index: ->
    @render "users/index", locals: {users: @get('users')}

This really doesn't change anything (because Tower's chainable scopes are already Ember.ArrayProxy instances, so they're bindable by default). Also, this client-side controller is starting to look a lot different than the server-side controller:

# server-side controller
class App.UsersController extends App.ApplicationController
  index: ->
    App.User.where(createdAt: '>=': _(2).days().ago()).all (error, users) =>
      @render "users/index", locals: {users: users}

What if you could make them the same? You can, like this:

# client-side controller
class App.UsersController extends App.ApplicationController
  # don't even need to define it, just showing you
  users: null
  
  index: ->
    @getUsers (error, users) =>
      @render "users/index"
      
  getUsers: (callback) ->
    users = @get('users')
    return callback.call(@, null, users) if users
    App.User.where(createdAt: '>=': _(2).days().ago()).all (error, users) =>
      @set('users', users) # THIS IS KEY
      callback.call(@, error, users)

You could use that on both the client and the server.

That simple call to @set('users', users) will tell ember to begin watching that collection. And we only initialize it once - on the server, since every controller is a new instance, this will always be null, but on the client, it will only be null once.

Then, @render 'users/index' will, on the client, create a new Ember.View if one hasn't been instantiated yet, otherwise it will use a new one. And on the server, it will just render the HTML string.

Look at this a little closer, you can define scopes to use in your Ember/Handlebars templates!

# client-side controller
class App.UsersController extends App.ApplicationController
  # don't even need to define it, just showing you
  users:        null
  recentUsers:  null
  
  index: ->
    if @params.recent == true
      @getRecentUsers (error, users) =>
        @render "users/index"
    else
      @getUsers (error, users) =>
        @render "users/index"
      
  getRecentUsers: (callback) ->
    users = @get('recentUsers')
    return callback.call(@, null, users) if users
    App.User.where(createdAt: '>=': _(2).days().ago()).all (error, users) =>
      @set('recentUsers', users) # THIS IS KEY
      callback.call(@, error, users)
      
  getUsers: (callback) ->
    users = @get('users')
    return callback.call(@, null, users) if users
    App.User.all (error, users) =>
      @set('users', users) # THIS IS KEY
      callback.call(@, error, users)
{{#each App.usersController.recentUsers}}
...
{{/each}}

But because you have the @cursor() object in the controllers, which is passed to the scope, you can just customize that to scope any list of users.

Just so you know, this is all implemented internally, in a very dynamic way, making it so you don't have to write any code in your controllers and you get this by default.

Now the question pops up, what if you want more for your client-side controllers? No problem, you are totally free to create a controller specific to the client and one specific to the server, or add stuff specific to the client to the main controller, and just never use that code on the server, it's up to you. So you could do this:

# client-side controller
class App.UsersController extends App.ApplicationController
  # don't even need to define it, just showing you
  users:        App.User.all
  recentUsers:  App.User.where(createdAt: '>=': _(2).days().ago()).all
{{#each App.usersController.recentUsers}}
...
{{/each}}

Boom, you set them by default, and they never need to be set again on the client. Just some ideas.

Query Parameter API

You can define parameters which should be parsed into a Tower.Model.Criteria. This allows you to build a very robust query api with minimal effort.

Tower uses a set of pseudo-ruby conventions for defining ranges and sets of values to query by. They are defined with the @param class method on a controller. Tower looks for the attribute definition on the model class associated with the controller, unless you have specified the type option.

class App.EventsController extends Tower.Controller
  @param "title"
  @param "createdAt"
  @param "memberCount"
  @param "tags", type: "Array"
  @param "coordinates"
  @param "admin", source: "admin.firstName"
  
  index: ->
    App.Event.where(@criteria()).all (error, events) =>
      @render json: events
  • String
  • Date
  • Array
  • Number
  • Order
  • Coordinates

String

Query

title=Hello+World
title=Hello+-World
title='Hello+World'

Criteria

{ "title" : { "$match" : ["Hello", "World"] } }
{ "title" : { "$match" : ["Hello"], "$notMatch" : ["World"] } }
{ "title" : { "$match" : ["Hello World"] } }

Date

Query

createdAt=12-31-2011
createdAt=12-31-2011..t
createdAt=t..12-31-2011
createdAt=12-21-2011..12-31-2011
createdAt=12-21-2011..12-31-2011,01-04-2012

Criteria

{ "createdAt" : Date(12-31-2011) }
{ "createdAt" : { "$gte": Date(12-31-2011) } }
{ "createdAt" : { "$lte": Date(12-31-2011) } }
{ "createdAt" : { "$gte": Date(12-21-2011), "$lte": Date(12-31-2011) } }
{ "$or": [ { "createdAt" : { "$gte": Date(12-21-2011), "$lte": Date(12-31-2011) } }, { "createdAt" : Date(01-04-2012) } ] }

Number

Query

likeCount=1
likeCount=1..n
likeCount=n..100
likeCount=1..100
likeCount=1..100,1000
likeCount=-10
likeCount=^10
likeCount=^10,1000
likeCount=^10..1000
likeCount=^-1000

Criteria

{ "likeCount" : 1 }
{ "likeCount" : { "$gte": 1 } }
{ "likeCount" : { "$lte": 1 } }
{ "likeCount" : { "$gte": 1, "$lte": 100 } }
{ "$or": [{ "likeCount" : { "$gte": 1, "$lte": 100 } }, { "likeCount" : 1000 }] }
{ "likeCount": -10 }
{ "likeCount": { "$neq": -10 } }
{ "likeCount": { "$neq": 10, "$in": [1000] } }
{ "nor": [ { "likeCount" : { "$gte": 10, "$lte": 1000 } } ] }
{ "likeCount": { "$neq": -1000 } }

Array

Query

tags=javascript
tags=^java
tags=ruby,^java
tags=[ruby,javascript]

Criteria

{ "tags" : { "$in": ["javascript"] } }
{ "tags" : { "$nin": ["java"] } }
{ "tags" : { "$in": ["ruby"], "$nin": ["java"] } }
{ "tags" : { "$all": ["ruby", "javascript"] } }

Coordinates

Query

coordinates=51.509981,-0.074704
coordinates=51.509981,-0.074704,10

Criteria

{ "coordinates" : { "$near": [51.509981, -0.074704] } }
{ "coordinates" : { "$near": [51.509981, -0.074704] , "$maxDistance" : 10 } }

Order

Query

sort=title
sort=title+
sort=title-
sort=createdAt-,title
sort=createdAt+,title+

Criteria

{ "sort" : [["title", "asc"]] }
{ "sort" : [["title", "asc"]] }
{ "sort" : [["title", "desc"]] }
{ "sort" : [["createdAt", "desc"], ["title", "asc"]] }
{ "sort" : [["createdAt", "asc"], ["title", "asc"]] }

Complete URL Query Examples

http://events.towerjs.org/?createdAt=12-25-2011
http://events.towerjs.org/?createdAt=12-25-2011..12-31-2011
http://events.towerjs.org/?tags=javascript,ruby
http://events.towerjs.org/?memberCount=10..n
http://events.towerjs.org/?sort=createdAt-,title+
http://events.towerjs.org/?createdAt=12-25-2011..12-31-2011&tags=javascript,ruby
http://events.towerjs.org/?createdAt=12-25-2011..12-31-2011&tags=javascript,ruby&sort=createdAt-,title+
http://events.towerjs.org/?createdAt=12-25-2011..12-31-2011&tags=javascript,ruby&memberCount=10..n&sort=createdAt-,title+
http://events.towerjs.org/?createdAt=12-25-2011..12-31-2011&tags=javascript,ruby&memberCount=10..n&coordinates=51.509981,-0.074704,10&sort=createdAt-,title+

The last url above would generate the criteria:

{ 
  "coordinates" : { "$near": [51.509981, -0.074704] , "$maxDistance" : 10 },
  "createdAt" : { "$gte": Date(12-21-2011), "$lte": Date(12-31-2011) },
  "memberCount" : { "$gte": 10 },
  "sort" : [["createdAt", "desc"], ["title", "asc"]], 
  "tags" : { "$in": ["ruby", "javascript"] }
}

(todo) OR Queries Over Several Attributes

You can do OR searches over several attributes, i.e. "find all posts in the past 2 days OR those tagged with 'javascript'". Just prepend each OR block with an array index:

# find all posts between christmas and new years eve, or those tagged with "javascript" and "ruby", then sort by date and title
[0]createdAt=12-25-2011..12-31-2011&[1]tags=javascript,ruby&sort=createdAt-,title+
createdAt[0]=12-25-2011..12-31-2011&tags[1]=javascript,ruby&sort=createdAt-,title+

Flash Messages

The flash is a way to pass messages to your user that will appear on screen only once, then disappear when they change or refresh the page -- unless they do something to cause the message to be added back to the flash, like repeating the same error. This feature is modeled on a similar feature that appears in both Rails and Express. The code was modeled closely on Express's, but they function more closely to how they function in Rails --the developer can just use them, without needing to add dynamic helpers or create additional templates.

Usage

Using the flash is simple, just call @flash from your controller, and pass it one of three types "error", "success", or "info" followed by the message you want it to flash to your user. For instance:

class App.UsersController extends App.ApplicationController
  index: ->
    App.User.all (error, @users) =>
      @flash 'info', 'Hello!'
      @flash 'error', 'Danger Will Robinson!'
      @flash 'success', 'You Did It!!!!!'
      @render "index"

will produce the following:

Three Flash Messages

(Note the X's in the corner of each flash message. These come from twitter-bootstrap and allow the user to easily dismiss the messages if they don't want to continue looking at them.)

Dynamic Usage

The flash message can also be generated dynamically, by allowing your program to determine when to call the @flash method based on conditional statements.

Simple Case

For instance, in the simple case, let's assume a posts model with a simple validation for presence on the title field:

class App.Post extends Tower.Model
  @field "title", type: "String"
  @field "body", type: "String"
  @field "userId", type: "Integer"

  @belongsTo "user"

  @validates 'title', presence: true

  @timestamps()

Currently, if the user forgets to fill in the title field, the app will simply fail silently. This could be quite frustrating to a user, who doesn't understand why his post refuses to save. We can solve that with a simple addition to the controller. In the "create" action of the posts controller, do this:

class App.PostsController extends App.ApplicationController
  create: ->
    App.Post.create @params.post, (error, @post) =>
      if (k for own k, message of @post.errors).length isnt 0
        @flash 'error', message
        @render "new"
      else
        @render "show"

Now the user gets returned to the form, with what they have filled in so far left intact, and an easy to spot error message letting them know what they need to do to correct it.

Flash Messages: Single Error

Note that we can't just do a simple check on @posts.errors, because of a quirk in Javascript in which

{} === {} // false

returns false. This would cause the form to need to be submitted twice each time, regardless of whether there was an error or not.

More Complex Message Generation

What if there is more than one validation on the model. Let's assume a user model like this:

class App.User extends Tower.Model
  @field "email", type: "String"
  @field "firstName", type: "String"
  @field "lastName", type: "String"

  @hasMany "posts"

  @validates 'lastName', 'email', presence: true

  @timestamps()

Still pretty simple, but this time, the validations check that two fields are completed. In our controller we need to set up something like this:

class App.UsersController extends App.ApplicationController
  create: ->
    App.User.create @params.user, (error, @user) =>
      if (k for own k of @user.errors).length isnt 0
        console.log @user.errors
        errString = ""
        num = 0
        for key, value of @user.errors
          num++
          errString = "#{errString} #{num}. #{value}"
        @flash 'error', errString
        @render "new"
      else
        @render 'show'

Now, if the user were to leave both the email and the last name fields blank, they would see something like:

Flash Messages: Multiple Errors

While you can add one message to each of the flash types "error", "success", and "info", as well as create as many additional types as you desire (simply add them to the template in app/views/shared/_flash.coffee), any given type can only hold one message at a time, given the current instantiation. So, we need to build up the message we want to pass to it with some simple string concatenation. This allows all of the error messages to be passed to the flash and shown in the browser when the user is returned to his form.

A note about the above, we could have saved a few lines, and had slightly cleaner looking code, by using the line:

errString = "#{errString} <br /> #{value}"

Which would have placed a new line between each message, instead of numbering them on the same line. (Unfortunately, "\n" won't work here). However, I felt like for the wiki it would be best to use the safest example possible.

About the Messages

The flash messages are styled with Twitter-Bootstrap, in keeping with the styling of the rest of Towerjs apps. The template for the messages is stored in app/views/shared/_flash.coffee which is rendered from app/views/layouts/application.coffee. Developers are, of course, free to alter the styling or templates, or remove them completely, as suits the needs of their particular projects.

Pagination via the Controller

[todo] Each cursor has the properties currentPage, pageCount, totalCount, hasNextPage, hasPreviousPage, hasNext, hasPrevious. This is then bound to your current model cursor:

class App.PostsController extends Tower.Controller
  all: App.Post.all()
  
  next: ->
    @get('all').nextPage().find()
    
  prev: ->
    @get('all').previousPage().find()

A cursor.find()

Rendering

  • Templates
  • JSON
  • The Rendering Process

Rendering Templates

class App.PostsController extends Tower.Controller
  index: ->
    @render "index"

Rendering JSON

class App.PostsController extends Tower.Controller
  show: ->
    App.Post.find @params.id, (error, post) =>
      @render json: post

The Rendering Process

respondWith

show: ->
  App.Post.find @params.id, (error, post) =>
    @respondWith post, (format) =>
      format.html => @render "show"

This will perform content negotiation, i.e. it will figure out what the mime type the browser prefers and run the corresponding responder method (for html, json, csv, etc.). Those methods then call the render method.

render

show: ->
  App.Post.find @params.id, (error, post) =>
    @render "show"

Calling the render method directly forces a specific content type to be rendered. Here is the method signature:

render json: {hello: "world"}
render "show"                 # render action: "show"
render "posts/show"           # render file: "posts/show"
render -> h1 "Hello World"
render text: "success", status: 200

_normalizeRender

This converts the render arguments into a normalized options hash.

_renderToBody

_renderOption

_renderTemplate

Requests

On the server Tower uses superagent.

  • Location
  • Cookies
  • User Agent

URL

User Agent

Subdomains

Cookies

Session

Tower.Controller.Resources

Tower goes by the convention that every controller represents one resource, one model.

A controller doesn't need to follow these conventions, for example with a DashboardController or SearchController. In those cases, overriding the methods starts you with a clean slate. However, you'll quickly see how powerful this is.

Redirecting

The Resource

You can customize the variable names and resource type:

class App.PostsController extends Tower.Controller
  @resource type: "Article", collection: "articles", resource: "article", key: "data", id: "dataId"

Internals

The default implementation for a Tower.Controller looks like this:

class App.PostsController extends Tower.Controller
  index: ->
  
  new: ->
  
  create: ->
  
  show: ->
  
  edit: ->
  
  update: ->
  
  destroy: ->

Pub/Sub

This section describes how to setup pub/sub in your application.

Tower.Controller.subscriptions

class App.PostsController extends App.ApplicationController
  @subscriptions 'all', 'recent', 'archived'

You can also use the publish method, which is just an alias to subscriptions:

class App.PostsController extends App.ApplicationController
  @publish 'all', 'recent', 'archived'

The keys passed to subscriptions point to methods in the controller that return a Tower.Model.Cursor. The cursor is an Ember.ArrayProxy that acts as a collection of models matching specific criteria.

class App.PostsController extends App.ApplicationController
  @subscriptions 'all', 'recent', 'archived'
  
  all: App.Post.all()
  
  recent: App.Post.recent()
  
  archived: ->
    App.Post.where(authorId: @currentUser.get('id')).archived()

You can set the subscription to 1 of 3 things:

  1. A Tower.Model.Cursor object, which you'll get from .all() (see Tower.Model.Scope for details).
  2. A Tower.Model.Scope object, which is everything before you call .all(). This will be converted to a criteria when first called, just makes it so you have to write less code.
  3. A Function. This allows you to integrate things like the current user id for authentication for example. However, there will be better ways to grab currentUser.id without using a Function soon.

Error Handling in the Controller

Authentication in the Controller

Authorization in the Controller

Caching in the Controller

HTTP Caching

1. Expiration Headers

  • Back-end sets Cache-Control: public, max-age: 60
  • Gets cached in gateway cache an browser cache.
  • Public says it is good for many clients.
  • Cached for 60s.
show: ->
  @expiresIn 60.seconds, public: true
  # stuff
  @render

2. Validation (Conditional Get)

  • Back-end adds ETag or Last-modified, e.g. ETag: abcdef012345
  • Last-modified is redundant, basically there for HTTP 1.0 clients.
  • On 2nd request, gateway cache realizes it has this page in cache, then sends a GET /foo, Host: foo.com, If-None-Match: abcdef012345 to the back-end.
  • If back-end returns a 304 Not Modified, gateway cache returns cached version.
show: ->
  @foo = Foo.find(@params.id)
  @freshWhen etag: @foo, lastModfied: @foo.get("updatedAt")
show: ->
  @foo = Foo.find(@params.id)
  if @isStale(etag: @foo, lastModfied: @foo.get("updatedAt"))
    @respondTo ...

Combine Expiration and Validation

  • Back-end sets Cache-control: public, max=age=60 and ETag: abcdef012345
  • In < 60 seconds, cache-control takes precedence
  • After 60 seconds, it queries back-end using ETag
  • Back end can then send back a 304 not modified with a new Cache-control: public, max-age: 60

Handling Events

All controller actions are just events. This means then that controllers handle events:

  • DOM events
  • Keyboard events
  • Gesture events
  • socket messages
  • url requests

Instead of having to create a controller for each type of message, why not just establish some conventions:

Note: As of the current master branch manual socket manipulation (@on, @emit) are broken / not fully implemented. Work in progress.

class App.PostsController extends Tower.Controller
  # socket.io handler
  @on "create", "syncCreate" # created by default... knows because it's named after an action
  @on "notification", "flashMessage" # knows it's socket because 'notification' isn't an action or dom event keyword
  @on "mousemove", "updateHeatMap", type: 'socket' # if you name a socket event after a keyword then pass the `type: 'socket'` option.
  
  # dom event handler
  @on "click", "click"
  @on "click .item a", "clickItem"
  # or as an object
  @on "click .itemA a": "clickItemA",
    "click .itemB a": "clickItemB",
    "click .itemC a": "clickItemC"
  
  @on "change #user-first-name-input", "enable", dependent: "#user-last-name-input"
  @on "change #user-first-name-input", "enable #user-last-name-input" # enable: (selector)
  @on "change #user-first-name-input", "validate"
  @on "change #user-first-name-input", bind: "firstName"
  @bind "change #user-first-name-input", "firstName"
  @on "click #add-user-address", "addAddress"
  @on "click #add-user-address", "add", object: "address"
  @on "click #remove-user-address", "removeAddress"
  # $(window).on('click', '#user-details', "toggleDetails");
  @on "click #user-details", "toggleDetails"

  # show or hide
  toggleShowHide: ->

  show: ->

  hide: ->

  toggleSelectDeselect: ->

  select: ->

  deselect: ->

  toggleAddRemove: ->

  add: ->

  remove: ->

  toggleEnableDisable: ->  
    if _.blank(value)
      @disable()
    else
      @enable()

  # enable or disable
  enable: ->
    $(options.dependent).attr("disabled", false)

  disable: ->
    $(options.dependent).attr("disabled", true)

  validate: (element) ->
    element

  invalidate: ->

  bind: ->
  
  next: ->
    
  prev: ->

Manual Sockets / Pub/Sub Sockets.

Note: This feature is in-progress as is not currently shipped in Tower. Nonetheless this feature should be ready soon.

For typical models, Tower employs a data-synchronization feature that handles real-time change. This is all great and fancy but what if you need more control in your hands?

Pub/Sub sockets are the answer. Both the client and server controllers will have this functionality and the api is indifferent. On either the client or the server you would first subscribe (@on) or listen on a particular message.

Listen

@on 'notification'

The first parameter holds the type of message your listening on. This can be anything you want. The second parameter contains either a string with a method name or an anonymous function.

class App.SomeController extends App.ApplicationController

  @on 'notification', 'someMethodHere'
  
  someMethodHere: (event) ->
    # Some logic here.
 
  @on 'notification', (event) ->
    # A shorter syntax.

Both ways will work the later is just shorter.

Send

Sending a socket message is no different and just as simple.

class App.SomeController extends App.ApplicationController
  
  index: ->
    @emit 'notification', message: 'Hello, World'

The first parameter is identical to @on, the difference is the next parameters which are simple json objects.

Observing and Binding to Controllers

Tower.Controller extends the Ember.Object, which is observable.

Resources

Clone this wiki locally