-
Notifications
You must be signed in to change notification settings - Fork 120
Controllers
Tower.js Controller Docs.
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"
- https://github.com/jhurliman/node-rate-limiter
- https://github.com/cloudkick/rate-limiter
- http://chris6f.com/rate-limiting-with-redis
- http://www.joshdevins.net/2010/11/22/rate-limiting-http-proxy-in-node-js-and-redis/
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)
By convention there are 7 RESTful actions for a controller:
-
index
: Show all resources (GET
) -
new
: Build a new resource and render a form (GET
) -
create
: Create a new resource (POST
) -
show
: Find and show a resource by id (GET
) -
edit
: Find a resource by id and render it in a form to edit (GET
) -
update
: Update an existing resource (PUT
) -
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.
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
.
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.
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.
class App.PostsController extends App.ApplicationController
@beforeAction 'check'
[todo, just notes so far / partial implementation in development branch]
Client-side controllers will, like server-side controllers, have a default implementation that just works for the 7 common actions.
- 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.
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.
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
title=Hello+World
title=Hello+-World
title='Hello+World'
{ "title" : { "$match" : ["Hello", "World"] } }
{ "title" : { "$match" : ["Hello"], "$notMatch" : ["World"] } }
{ "title" : { "$match" : ["Hello World"] } }
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
{ "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) } ] }
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
{ "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 } }
tags=javascript
tags=^java
tags=ruby,^java
tags=[ruby,javascript]
{ "tags" : { "$in": ["javascript"] } }
{ "tags" : { "$nin": ["java"] } }
{ "tags" : { "$in": ["ruby"], "$nin": ["java"] } }
{ "tags" : { "$all": ["ruby", "javascript"] } }
coordinates=51.509981,-0.074704
coordinates=51.509981,-0.074704,10
{ "coordinates" : { "$near": [51.509981, -0.074704] } }
{ "coordinates" : { "$near": [51.509981, -0.074704] , "$maxDistance" : 10 } }
sort=title
sort=title+
sort=title-
sort=createdAt-,title
sort=createdAt+,title+
{ "sort" : [["title", "asc"]] }
{ "sort" : [["title", "asc"]] }
{ "sort" : [["title", "desc"]] }
{ "sort" : [["createdAt", "desc"], ["title", "asc"]] }
{ "sort" : [["createdAt", "asc"], ["title", "asc"]] }
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"] }
}
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+
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.
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:
(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.)
The flash message can also be generated dynamically, by allowing your program to determine when to call the @flash method based on conditional statements.
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.
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.
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:
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.
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.
[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()
- Templates
- JSON
- The Rendering Process
class App.PostsController extends Tower.Controller
index: ->
@render "index"
class App.PostsController extends Tower.Controller
show: ->
App.Post.find @params.id, (error, post) =>
@render json: post
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.
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
This converts the render arguments into a normalized options hash.
On the server Tower uses superagent
.
- Location
- Cookies
- User Agent
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.
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"
The default implementation for a Tower.Controller
looks like this:
class App.PostsController extends Tower.Controller
index: ->
new: ->
create: ->
show: ->
edit: ->
update: ->
destroy: ->
This section describes how to setup pub/sub in your application.
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:
- A
Tower.Model.Cursor
object, which you'll get from.all()
(seeTower.Model.Scope
for details). - 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. - A
Function
. This allows you to integrate things like the current userid
for authentication for example. However, there will be better ways to grabcurrentUser.id
without using aFunction
soon.
- 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
- 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 ...
- 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
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: ->
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.
@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.
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.
Tower.Controller
extends the Ember.Object
, which is observable.