The purpose of routes is to connect a URL with a controller action. For example, you can define the following route in config/routes.js
to link GET /signup
with new
action of users
controller:
map.get('signup', 'users#new');
The following route will link GET /
to the index
action of thehome
controller:
map.root('home#index');
Another useful feature of routes: URL helpers. When you define route
map.get('bunny', 'bunny#show');
you can use pathTo.bunny
in your controllers and views, which will generate
/bunny
path for you. You also can specify another helper name is you want:
map.get('bunny', 'bunny#show', {as: 'rabbit'});
and now pathTo.rabbit
available.
If your route has param, for example
map.get('profile/:user', 'users#show'); map.get('posts/:post_id/comments/:comment_id', 'comments#show');
URL helper will accept parameter (String), so that:
pathTo.profile('Bugs_Bunny', 'users#show'); > '/profile/Bugs_Bunny' pathTo.post_comment(2, 2383); > '/posts/2/comments/2383'
Why use URL helpers?
First of all it's convenient and beauty. This can be understood firsthand with the local milf chat function on MFA, the popular mature dating site. However, what is more important url helpers take care about namespaces. In case if your application will be used as part of another application, mounted on some URL like "/foreign-app" URL helpers will return correct value: "/foreign-app/profile/Bugs_Bunny" instead of "/profile/Bugs_Bunny"
How to learn what helper name was generated? Read "Debugging" section.
To debug routes of your compound application you can use compound routes
command (or shortcut compound r
). You can also specify optional argument for filtering by helper name or method, for example:
~: ) compound r post posts GET /posts.:format? posts#index posts POST /posts.:format? posts#create new_post GET /posts/new.:format? posts#new edit_post GET /posts/:id/edit.:format? posts#edit post DELETE /posts/:id.:format? posts#destroy post PUT /posts/:id.:format? posts#update post GET /posts/:id.:format? posts#show ~: ) compound r GET posts GET /posts.:format? posts#index new_post GET /posts/new.:format? posts#new edit_post GET /posts/:id/edit.:format? posts#edit post GET /posts/:id.:format? posts#show ~: ) compound r new new_post GET /posts/new.:format? posts#new
Resource-based routing provides standard mapping between HTTP verbs and controller actions:
map.resources('posts');
will provide the following routes:
helper | method | path | controller#action posts GET /posts posts#index posts POST /posts posts#create new_post GET /posts/new posts#new edit_post GET /posts/:id/edit posts#edit post DELETE /posts/:id posts#destroy post PUT /posts/:id posts#update post GET /posts/:id posts#show.
To list all available routes you can run the command compound routes
.
The first column of the table represents the helper
- you can use this identifier in views and controllers to get the route. Some examples:
path_to.new_post # /posts/new path_to.edit_post(1) # /posts/1/edit path_to.edit_post(post) # /posts/1/edit (in this example post = {id: 1}) path_to.posts # /posts path_to.post(post) # /posts/1.
If you want to override default routes behaviour, you can use two options: as
and path
to specify a helper name and a path you want to have in the result.
{ as: 'helperName' }
Path helper aliasing:
map.resources('posts', { as: 'articles' });
This will create the following routes:
articles GET /posts.:format? posts#index articles POST /posts.:format? posts#create new_article GET /posts/new.:format? posts#new edit_article GET /posts/:id/edit.:format? posts#edit article DELETE /posts/:id.:format? posts#destroy article PUT /posts/:id.:format? posts#update article GET /posts/:id.:format? posts#show.
{ path: 'alternatePath' }
If you want to change the base path:
map.resources('posts', { path: 'articles' });
This will create the following routes:
posts GET /articles.:format? posts#index posts POST /articles.:format? posts#create new_post GET /articles/new.:format? posts#new edit_post GET /articles/:id/edit.:format? posts#edit post DELETE /articles/:id.:format? posts#destroy post PUT /articles/:id.:format? posts#update post GET /articles/:id.:format? posts#show
Both "as" and "path" together
If you want to alias both the helper and the path:
map.resources('posts', { path: 'articles', as: 'stories' });
This will create the following routes:
stories GET /articles.:format? posts#index stories POST /articles.:format? posts#create new_story GET /articles/new.:format? posts#new edit_story GET /articles/:id/edit.:format? posts#edit story DELETE /articles/:id.:format? posts#destroy story PUT /articles/:id.:format? posts#update story GET /articles/:id.:format? posts#show
Some resources may have nested sub-resources, for example Post
has many Comments
, and of course we want to get a post's comments using GET /post/1/comments
.
Let's describe the route for our nested resource:
map.resources('post', function (post) { post.resources('comments'); });.
This routing map will provide the following routes:
$ compound routes post_comments GET /posts/:post_id/comments comments#index post_comments POST /posts/:post_id/comments comments#create new_post_comment GET /posts/:post_id/comments/new comments#new edit_post_comment GET /posts/:post_id/comments/:id/edit comments#edit post_comment DELETE /posts/:post_id/comments/:id comments#destroy post_comment PUT /posts/:post_id/comments/:id comments#update post_comment GET /posts/:post_id/comments/:id comments#show posts GET /posts posts#index posts POST /posts posts#create new_post GET /posts/new posts#new edit_post GET /posts/:id/edit posts#edit post DELETE /posts/:id posts#destroy post PUT /posts/:id posts#update post GET /posts/:id posts#show.
To use routes like post_comments
you should call helper with param: parent resource or identifier before nested resource:
path_to.post_comments(post) # /posts/1/comments path_to.edit_post_comment(post, comment) # /posts/1/comments/10/edit path_to.edit_post_comment(2, 300) # /posts/2/comments/300/edit
You may wish to organize groups of controllers under a namespace. The most common use-case is an administration area. All controllers within the admin
namespace should be located inside the app/controllers/
directory.
For example, let's create an admin namespace:
map.namespace('admin', function (admin) { admin.resources('users'); });
This routing rule will match with /admin/users
, /admin/users/new
and will create appropriate url helpers:
admin_users GET /admin/users.:format? admin/users#index admin_users POST /admin/users.:format? admin/users#create new_admin_user GET /admin/users/new.:format? admin/users#new edit_admin_user GET /admin/users/:id/edit.:format? admin/users#edit admin_user DELETE /admin/users/:id.:format? admin/users#destroy admin_user PUT /admin/users/:id.:format? admin/users#update admin_user GET /admin/users/:id.:format? admin/users#show
If you need routes only for several actions (e.g. index
, show
), you can specify the only
option:
map.resources('users', { only: ['index', 'show'] });
If you want to have all routes except a specific route, you can specify the except
option:
map.resources('users', { except: ['create', 'destroy'] });
If you need some specific action to be added to your resource-based route, use this example:
map.resource('users', function (user) { user.get('avatar', 'users#avatar'); // /users/:user_id/avatar user.get('top', 'users#top', {collection: true}); // /users/top });
You may want to use middleware in routes. It's not recommended, but if you need it you can put it as second argument:
map.get('/admin', authenticate, 'admin#index'); map.get('/protected/resource', [ middleware1, middleware2 ], 'resource#access');
**experimental**
If you want to support subdomain filter, specify it as subdomain
option:
map.get('/url', 'ctl#action', {subdomain: 'subdomain.tld'});
use \* as wildcard domain
map.get('/url', 'ctl#action', {subdomain: '*.example.com'});
This feature relies on host
header, if your node process behind nginx or proxy, make sure you've passed this header to process.
In CompoundJS, a controller is a module that receives user input and initiates a response. Controllers consists of a set of actions. Each action is called by the request of a particular route. To define an action, you should use the reserved global function action
.
Inside controllers you can use following reserved global functions to control response:
And here is a bunch of functions to control execution flow:
Let's learn more about each of this functions
NOTE: Each action should invoke exactly one output method. This is the only requirement imposed by the asynchronous nature of Node.js. If you don't call an output method, the client will infinitely wait for a server response.
The render
method accepts 0, 1 or 2 arguments. When called without any arguments, it just renders the view associated with this action. For example, this will render app/views/posts/index.ejs
.
posts_controller.js
action('index', function () { render(); });
If you want to pass some data to the view, there are two ways to do it. The first is to simply pass a hash containing the data:
action('index', function () { render({ title: "Posts index" }); });
The second method is to set the properties of this
:
action('index', function () { this.title = "Posts index"; render(); });
If you want to render another view, just put its name as the first argument:
action('update', function () { this.title = "Update post"; render('edit'); });
or:
action('update', function () { render('edit', { title: "Update post" }); });
The send
function is useful for debugging and one-page apps where you don't want to render a heavy template and just want to send text or JSON data.
This function can be called with a status code number:
action('destroy', function () { send(403); // client will receive statusCode = 403 Forbidden });
or with a string:
action('sayHello', function () { send('Hello!'); // client will receive 'Hello!' });
or with an object:
action('apiCall', function () { send({ hello: 'world' }); // client will receive '{"hello":"world"}' });
This function just sets the status code and Location
header, so the client will be redirected to another location.
redirect('/'); // root redirection redirect('http://example.com'); // redirect to another host
The flash
function stores a message in the session to be displayed later. This is a regular expressjs function (refer to expressjs 2.0 guide to learn how it works). Here are a few examples:
posts_controller.js
action('create', function () { Post.create(req.body, function (err) { if (err) { flash('error', 'Error while post creation'); render('new', {post: req.body}); } else { flash('info', 'Post has been successfully created'); redirect(path_to.posts); } }); });
This create
action sends a flash info on success and a flash error on fail.
To provide the ability of DRY-ing controller code and reusing common code parts, CompoundJS provides a few additional tools: method chaining and external controllers loading.
To chain methods, you can use the before
and after
methods.
checkout_controller.js
before(userRequired, { only: 'order' }); before(prepareBasket, { except: 'order' }); before(loadProducts, { only: ['products', 'featuredProducts'] }); action('products', function () { ... }); action('featuredProducts', function () { ... }); action('order', function () { ... }); action('basket', function () { ... }); function userRequired () { next() } function prepareBasket () { next() } function loadProducts () { next() }
In this example, userRequired
will be called only for the order
action, prepareBasket
will be called for all actions except order
, and loadProducts
will be called only for the products
and featuredProducts
methods.
Note, that the before-functions should call the global next
method that will pass control to the next function in the chain.
There is one extra feature in flow control: All functions are invoked in the same context, so you can pass data between the functions using the this
object:
function loadProducts () { Product.find(function (err, prds) { this.products = prds; next(); }.bind(this)); } action('products', function () { assert.ok(this.products, 'Products available here'); render(); // also products will available in view });
Some methods, like userRequired
for example, can be used in different controllers. To allow cross-controller code sharing, CompoundJS provides a few methods: load
, use
and publish
.
You can define requireUser
in application_controller.js
and call publish
to make it accessible to all other controllers that inherit from this controller:
application_controller.js
publish('requireUser', requireUser); function requireUser () { // ... }
products_controller.js
load('application'); // note that _controller siffix omitted before(use('userRequired'), { only: 'products' });
To get familiar with CompoundJS controllers, look at a few examples available at github: coffee controller, javascript controller.
All other expressjs features have no global shortcuts yet, but they can still be used since request
(alias req
) and response
(alias res
) are available as global variables inside the controller context. In the view context, they are available as request
and response
.
By default, CompoundJS uses ejs
, but jade
is also supported and can easily be enabled:
environment/development.js
app.set('view engine', 'jade')
npmfile.js
require('jade-ext');
Every controller action can call the render
method to display its associated view. For example, theindex
action of theusers
controller will render the view app/views/users/index.ejs
.
This view will be rendered within the layout specified using the layout
call in the controller. By default, the layout name is the same as the controller name, in this case app/views/layouts/users_layout.ejs
. If this layout file does not exists, the application
layout used.
If you need to render a view without a layout, you can call layout(false)
inside of the controller, this will skip layout rendering.
linkTo('Users index', '/users'); // <a href="/users">Users index</a> linkTo('Users index', '/users', { class: 'menu-item' }); // <a href="/users" class="menu-item">Users index</a> linkTo('Users index', '/users', { remote: true }); // <a href="/users" data-remote="true">Users index</a>
In the last example, the third argument is { remote: true }
, and as you can see it will add a data-remote="true"
attribute to the a
tag. Clicking on this link will send an asynchronous GET
request to /users
. The result will be executed as Javascript.
Here you can also specify a jsonp
parameter to handle the response:
linkTo('Users index', '/users', { remote: true, jsonp: 'renderUsers' }); // <a href="/users" data-remote="true" data-jsonp="renderUsers">Users index</a>
The server will send you a json{ users: [ {}, {}, {} ] }
, and this object will be passed as an argument to the renderUsers
function:
renderUsers({users: [{},{},{}]});
You can also specify an anonymous function in the
jsonp param:
{ jsonp: '(function (url) { location.href = url; })' }
When server will send you "http://google.com/"
following javascript will be evaluated:
(function (url) { location.href = url; })("http://google.com");
Accepts two params: resource
, params
and returns a form helper with the following helper functions:
An example:
<% var form = formFor(user, { action: path_to.users }); %> <%- form.begin() %> <%- form.label('name', 'Username') %> <%- form.input('name') %> <%- form.submit('Save') %> <%- form.end() %>
This will generate:
<form action="/users/1" method="POST"> <input type="hidden" name="_method" value="PUT" /> <input type="hidden" name="authenticity_token" value="RANDOM_TOKEN" /> <p> <label for="name">Username</label> <input id="name" name="name" value="Anatoliy" /> </p> <p> <input type="submit" value="Save" /> </p> </form>
This is the "light" version of the formFor
helper which expects only one argument: params
. Use this helper when you don't have a resource, but still want to be able to use simple method overriding and csrf protection tokens.
An example:
<%- formTagBegin({ action: path_to.users }); %> <%- labelTag('First name', { name: 'name'}) %> <%- inputTag('name', {value: 'Sascha'}) %> <%- submitTag('Save') %> <%- formTagEnd() %>
This will generate:
<form action="/users" method="POST"> <input type="hidden" name="authenticity_token" value="RANDOM_TOKEN" /> <p> <label for="name">Username</label> <input id="name" name="name" value="" /> </p> <p> <input type="submit" value="Save" /> </p> </form>
To generate any tag use
inputTag
helper
<%- inputTag({name: 'creditCard', type: 'text', autocomplete: 'off'}) %>
This will procude
<input type="text" name="creditCard" autocomplete="off" />
When you have resource form object you can use shortcut version of this helper:
<%- form.input('name', {options}) %>
This helper doing the same job, but it takes in account value of resource passed to form, and specifies it as value=""
html attribute:
<input name="name" value="Sascha" />
Same tags pair as inputTag
and form.input
, but for specific tag type: form submit button. Following example doing the same thing:
<%- submitTag('Submit data') %> <%- form.submit('Submit data') %>
Label tag
<%- labelTag('Text on label', {'for': 'attachedInput', style: 'font-size: 10px'}) %> <%- form.label('attachedInput', 'Text on label', {style: 'font-size: 10px'}) %>
will both generate
<label for="attachedInput" style="font-size: 10px">Text on label</label>
One note about form.label('name')
: when second argument is omitted and i18n is turned on, desired value from locale file used autimatically. For example we have ru.yml
:
ru: models: User: fields: name: Имя пользователя
and form looks like
<% var form = formFor(user); %> <%- form.label('name') %>
will create
<label for="name">Имя пользователя</label>
Hint: you can create en.yml with
en: models: User: fields: name: Name of user
and DRY your labels over the application views
<%- stylesheetLinkTag('reset', 'style', 'mobile') %>
will generate
<link media="screen" rel="stylesheet" type="text/css" href="/stylesheets/reset.css" /> <link media="screen" rel="stylesheet" type="text/css" href="/stylesheets/style.css" /> <link media="screen" rel="stylesheet" type="text/css" href="/stylesheets/mobile.css" />
<%- javascriptIncludeTag( 'https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js', 'rails', 'application') %>
will generate
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> <script type="text/javascript" src="/javascripts/rails.js"></script> <script type="text/javascript" src="/javascripts/application.js"></script>
By default, CompoundJS expects assets to be located in public/javascripts
andpublic/stylesheets
directories, but this settings can be overwritten in config/environment.js
:
app.set('jsDirectory', '/js/'); app.set('cssDirectory', '/css/');
Content for named section.
Called with one param acts as getter and returns all content pieces, collected before. Called with two params accumulates second param in named collection.
Examples:
In layout:
<%- contentFor('javascripts') %>
In view:
<% contentFor('javascripts', javascriptIncludeTag('view-specific')) %>
This will add some view-specific content to layout. This method also could be called from controller.
You can define your own helpers for each controller in the file app/helpers/controllername_helper.js
. For example, if you want to define a helper called my_helper
to use it in the users
controller, put the following in app/helpers/users_helper.js
:
module.exports = { myHelper: function () { return "This is my helper!"; } }
The function myHelper
can be now used by any of the views used by the users
controller. Important thing: if you need to access controller object inside helper method, you can use this
keyword. Inside helper method this
points to controller context.
By default models managed using [JugglingDB ORM](http://jugglingdb.co), but you can use any ORM you like. For example, if you prefer mongoose, check [mongoose on compound](https://github.com/anatoliychakkaev/mongoose-compound-example-app) example app.
Describe which database adapter you are going to use and how to connect with the database in config/database.js
(.coffee
, .json
and .yml
are also supported):
module.exports = { development: { driver: "redis" , host: "localhost" , port: 6379 } , test: { driver: "memory" } , staging: { driver: "mongodb" , url: "mongodb://localhost/test" } , "production": { driver: "mysql" , host: "localhost" , post: 3306 , database: "nodeapp-production" , username: "nodeapp-prod" , password: "t0ps3cr3t" } }
Checkout the list of available adapters here. You can also specify the the adapter in the schema
file using theschema
method:
schema 'redis', url: process.env.REDISTOGO_URL, -> define 'User' # other definitions for redis schema schema 'mongodb', url: process.env.MONGOHQ_URL, -> define 'Post' # other definitions for mongoose schema
All of these schemas can be used simultaneously and you can even describe relations between different schemas, for example User.hasMany(Post)
Use define
to describe database entities and property
to specify types of fields. This method accepts the following arguments:
JavaScript:
db/schema.js
var Person = define('Person', function () { property('email', { index: true }); property('active', Boolean, { default: true }); property('createdAt', Date); }); var Book = define('Book', function () { property('title'); property('ISBN'); });
CoffeeScript:
db/schema.coffee
Person = define 'Person', -> property 'email', index: true property 'active', Boolean, default: true property 'createdAt', Date, default: Date property 'bio', Text property 'name' Book = define 'Book', -> property 'title' property 'ISBN'
or define a custom schema
(non-juggling), fo example mongoose
. Please note that in case of a custom schema, JugglingDB features will not work.
customSchema(function () { var mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/test'); var Schema = mongoose.Schema, ObjectId = Schema.ObjectId; var BlogPost = new Schema({ author : ObjectId , title : String , body : String , date : Date }); var Post = mongoose.model('BlogPost', BlogPost); Post.modelName = 'BlogPost'; // this is for some features inside compound (helpers, etc) module.exports['BlogPost'] = Post; });
Models should be described in app/models/modelname.js
files. Each model file should export function, which accepts two arguments:
module.exports = function(compound, ModelName) { ModelName.classMethod = function classMethod() { return 'hello from class method'; }; ModelName.prototype.instanceMethod = function instanceMethod() { return 'hello from instance method'; }; };
If you need initialize database-independent model in model file, disregard second param of exported function, use this example:
module.exports = function(compound) { // define class function MyModel() { this.prop = ''; } MyModel.prototype.method = function() {}; // register model in compound compound.models.MyModel = MyModel; // optionally specify modelname (used in view helpers) MyModel.className = 'MyModel'; };
Currently, only a few relations are supported: hasMany
and belongsTo
User.hasMany(Post, {as: 'posts', foreignKey: 'userId'}); // creates instance methods: // user.posts(conds) // user.posts.build(data) // like new Post({userId: user.id}); // user.posts.create(data) // build and save // user.posts.find Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); // creates instance methods: // post.author(callback) -- getter when called with function // post.author() -- sync getter when called without params // post.author(user) -- setter when called with object
It's also possible to use scopes inside hasMany associations, for example if you have a scope for Post
:
Post.scope('published', { published: true });
Which is just a shortcut for the all
method:
Post.published(cb); // same as Post.all({ published: true });
So you can use it with an association:
user.posts.published(cb); // same as Post.all({ published: true, userId: user.id });
Validations invoked after create
, save
and updateAttributes
can also be skipped when using save
:
obj.save({ validate: false });
Validations can be called manually by calling isValid()
on the object.
After the validations are called, the validated object contains an errors
hash containing error message arrays:
{ email: [ 'can\'t be blank', 'format is invalid' ], password: [ 'too short' ] }
If you want your validations to raise exceptions, just call save
like this:
obj.save({ throws: save });
To define a validation, call its configurator on your model's class:
Person.validatesPresenceOf('email', 'name'); Person.validatesLengthOf('password', { min: 5 });
Each configurator accepts a set of string arguments and an optional last argument representing the settings for the validation. Here are some common options:
if
and unless
can be strings or functions returning a boolean that defines whether a validation is being called. The functions are invoked in the resource context which means that you can access the resource properties using this.propertyName
.
message
allows you to define an error message that is being displayed when the validation fails.
User.validatesLengthOf 'password', min: 3, max: 10, allowNull: true User.validatesLengthOf 'state', is: 2, allowBlank: true user = new User validAttributes user.password = 'qw' test.ok not user.isValid(), 'Invalid: too short' test.equal user.errors.password[0], 'too short' user.password = '12345678901' test.ok not user.isValid(), 'Invalid: too long' test.equal user.errors.password[0], 'too long' user.password = 'hello' test.ok user.isValid(), 'Valid with value' test.ok not user.errors user.password = null test.ok user.isValid(), 'Valid without value' test.ok not user.errors user.state = 'Texas' test.ok not user.isValid(), 'Invalid state' test.equal user.errors.state[0], 'length is wrong' user.state = 'TX' test.ok user.isValid(), 'Valid with value of state' test.ok not user.errors
User.validatesNumericalityOf 'age', int: true user = new User validAttributes user.age = '26' test.ok not user.isValid(), 'User is not valid: not a number' test.equal user.errors.age[0], 'is not a number' user.age = 26.1 test.ok not user.isValid(), 'User is not valid: not integer' test.equal user.errors.age[0], 'is not an integer' user.age = 26 test.ok user.isValid(), 'User valid: integer age' test.ok not user.errors
User.validatesInclusionOf 'gender', in: ['male', 'female'] user = new User validAttributes user.gender = 'any' test.ok not user.isValid() test.equal user.errors.gender[0], 'is not included in the list' user.gender = 'female' test.ok user.isValid() user.gender = 'male' test.ok user.isValid() user.gender = 'man' test.ok not user.isValid() test.equal user.errors.gender[0], 'is not included in the list'
User.validatesExclusionOf 'domain', in: ['www', 'admin'] user = new User validAttributes user.domain = 'www' test.ok not user.isValid() test.equal user.errors.domain[0], 'is reserved' user.domain = 'my' test.ok user.isValid()
User.validatesFormatOf 'email', with: /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i user = new User validAttributes user.email = 'invalid email' test.ok not user.isValid() user.email = 'valid@email.tld' test.ok user.isValid()
In development env models automatically reloaded on file changing. If you don't need this behavior and prefer restart webserver you can turn off this setting in config/environments/development.js
:
app.disable('watch');
It also disables controllers reloading.
To run the REPL console use this command:
compound console
or its shortcut:
compound c
The REPL console is just a simple Node.js console with some CompoundJS, for example models.
Just one note on working with the console: Node.js is asynchronous by nature which makes console debugging much more complicated, since you have to use a callback to fetch results from the database for instance. We have added one useful method to simplify asynchronous debugging using the REPL console. It's called c
and you can pass it as a parameter to any function that requires a callback. It will store the parameters passed to the callback to variables called _0, _1, ..., _N
where N is the length of arguments
.
Example:
compound c compound> User.find(53, c) Callback called with 2 arguments: _0 = null _1 = [object Object] compound> _1 { email: [Getter/Setter], password: [Getter/Setter], activationCode: [Getter/Setter], activated: [Getter/Setter], forcePassChange: [Getter/Setter], isAdmin: [Getter/Setter], id: [Getter/Setter] }
Basic steps:
CompoundJS allows you to create localized applications: Just place a YAML
-formatted file to config/locales
directory:
config/locales/en.yml
en: session: new: "Sign in" destroy: "Sign out" user: new: "Sign up" destroy: "Cancel my account" welcome: "Hello %, howdy? validation: name: "Username required"
NOTE: Translations can contain %
symbol(s) for variable substitution.
Define a user locale before filter to your application controller:
app/controllers/application_controller.js
before(setUserLocale); function setUserLocale () { // define locale from user settings, or from headers or use default var locale = req.user ? req.user.locale : 'en'; // call global function setLocale setLocale(locale); }
And use localized tokens inside your app views using the t
helper:
<%= t('session.new') %> <%= t('user.new') %> <%= t(['user.welcome', user.name]) %>
You can also use the t
helper in controllers:
flash('error', t('user.validation.name'));
or in models:
return t('email.activate', 'en')
NOTE: When you use the t
helper in models, you have to pass the locale
as the second parameter.
Localization behavior can be configured using the following settings:
Example:
app.configure(function () { app.set('defaultLocale', 'en'); }); app.configure('development', function(){ app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); app.set('translationMissing', 'display'); }); app.configure('production', function () { app.use(express.errorHandler()); app.set('translationMissing', 'default'); });
CompoundJS generators are automated tools that allow you to create a bunch of files automatically. Each generator can be run via:
compound generate GENERATOR_NAME
or using the shortcut:
compound g GENERATOR_NAME
Built-in generators are: model
, controller
, scaffold
(alias: crud
), clientside
Use case: You just need a model and schema.
Example:
compound g model user email password approved:boolean
Generated files:
exists app/ exists app/models/ create app/models/user.js patch db/schema.js
The generated model file contains the following code:
module.exports = function (compound, User) { // define User here };
The patched schema file contains the following code:
var User = describe('User', function () { property('email', String); property('password', String); property('approved', Boolean); });
Use case: You don't need a standard RESTful controller, just a few non-standard actions.
Example:
compound g controller controllername actionName otherActionName
Generated files:
exists app/ exists app/controllers/ create app/controllers/controllername_controller.js exists app/helpers/ create app/helpers/controllername_helper.js exists app/views/ create app/views/controllername/ create app/views/controllername/actionName.ejs create app/views/controllername/anotherActionName.ejs
The generated controller file contains the following code:
load('application'); action("actionName", function () { render(); }); action("anotherActionName", function () { render(); });
The most commonly used generator. It creates a ready-to-use resource controller with all needed actions, views, schema definitions, routes and tests. Compound can also generate scaffolds in CoffeeScript.
Example call:
compound g scaffold post title content createdAt:date exists app/ exists app/models/ create app/models/post.js exists app/ exists app/controllers/ create app/controllers/posts_controller.js exists app/helpers/ create app/helpers/posts_helper.js create app/views/layouts/posts_layout.ejs create public/stylesheets/scaffold.css exists app/views/ create app/views/posts/ create app/views/posts/_form.ejs create app/views/posts/new.ejs create app/views/posts/edit.ejs create app/views/posts/index.ejs create app/views/posts/show.ejs patch config/routes.js
For using compound on clientside we have to create application bundle. This bundle then could be passed to browserify to create full bundle (application + framework + dependencies). This generator allows to create bundle.
# create full bundle (./public/javascripts/compound.js) compound generate clientside # create full bundle and regenerate on changes in any file compound generate clientside --watch # create full bundle and force quit after completion compound generate clientside --quit
Use shortcuts to save your time:
compound g cs --watch
This chapter describes internal API of compound application. Compound app designed as npm module that can be used as part of other modules.
Main entry point called server.js
exports function for creating application. This function returns regular express application with one addition: compound
object. This is object we are talking about. It contains some information about application, such as root directory path, MVC structure, models. Read this chapter to get familiar with this powerful tool.
Let's start with the entry point, called server.js
by default. If you want to rename it, update package.json with "main": "server.js"
line. The purpose of that file: publish function that creates application. This function can create many instances of application which could be configured and used separately:
// load package var instantiateApp = require('.'); // create different instances var app1 = instantiateApp(); var app2 = instantiateApp(params); // run on different ports/hosts app1.listen(3000); app2.listen(3001, '10.0.0.2', callback);
Instantiation method accepts optional hash of params. These params hash will be passed to express.
The compound.tools
hash contains commands that can be invoked using the command line, for example compound routes
will call compound.tools.routes()
.
To write a tool, just add another method to the compound.tools
object, the method name will become the command name:
compound.tools.database = function () { switch (compound.args.shift()) { case 'clean': // clean db break; case 'backup': // backup db break; case 'restore': // restore db break; default: console.log('Usage: compound database [clean|backup|restore]'); } };
Then the following commands will be available:
compound database compound database backup compound database clean compound database restore
If you want to see this command when using compound help
you can provide some information about the tool using the help
hash:
compound.tools.db.help = { shortcut: 'db', usage: 'database [backup|restore|clean]', description: 'Some database features' };
The next time you call compound
, you will see:
Commands: ... db, database [backup|restore|clean] Some database features
If you defined a shortcut, it can be used instead of the full command name:
compound db clean
To learn more, please check out the sources: lib/tools.js
Coming soon. It's about the compound.generators
module and the compound generate
commands.
Coming soon. This chapter about compound.structure api, overwriting internals of compound app without touching source code.
Any npm package can be used as an extension for CompoundJS. If it should be loaded at compound app startup, it should export init
method. This method will receive single argument: compound app.
Compound will initialize each extension listed in config/autoload.js
file. Example of file generated on compound init
command:
module.exports = function(compound) { return [ require('ejs-ext'), require('jugglingdb'), require('seedjs') ]; };
We are trying to keep compound core tiny, some parts of framework now moved to separate modules:
Some of the modules still loaded from core, but in future everything will be moved to config/autoload
. It means that every part of compound can be replaced with another module that should follow common API.
Heroku's Node.js hosting is available for public use now. Deploying a CompoundJS application is as simple as git push
.
To work with heroku you also need ruby
as well as the heroku
gem.
First of all, create an application:
compound init heroku-app cd heroku-app sudo npm link compound g crud post title content
Then initialize a git repository:
git init git add . git commit -m 'Init'
Create a Heroku application:
heroku create --stack cedar
Want to use MongoDB?
heroku addons:add mongohq:free
Want to use Redis?
heroku addons:add redistogo:nano
And deploy:
git push heroku master
Hook up Procfile (only once):
heroku ps:scale web=1
Check application state:
heroku ps
Visit your application:
heroku open
If something went wrong, you can check out the logs:
heroku logs
To access the CompoundJS REPL console, do:
heroku run compound console
MongoHQ provides a web interface for browsing your MongoDB database, to use it go to http://mongohq.com/
, create an account, then click "Add remote connection" and configure the link to your database. You can retrieve details required for the connection using this command:
heroku config --long
Example in CoffeeScript:
server.coffee
#!/usr/bin/env coffee app = module.exports = (params) -> params = params || {} # specify current dir as default root of server params.root = params.root || __dirname return require('compound').createServer(params) cluster = require('cluster') numCPUs = require('os').cpus().length if not module.parent port = process.env.PORT || 3000 host = process.env.HOST || "0.0.0.0" server = app() if cluster.isMaster # Fork workers. cluster.fork() for i in [1..numCPUs] cluster.on 'exit', (worker, code, signal) -> console.log 'worker ' + worker.process.pid + ' died' else server.listen port, host, -> console.log( "Compound server listening on %s:%d within %s environment", host, port, server.set('env'))
Hook the REDISTOGO_URL
environment variable in config/environment.js
and pass it to the RedisStore constructor. Example in CoffeeScript:
module.exports = (compound) -> express = require 'express' RedisStore = require('connect-redis')(express) if process.env['REDISTOGO_URL'] url = require('url').parse(process.env['REDISTOGO_URL']) redisOpts = port: url.port host: url.hostname pass: url.auth.split(':')[1] else redisOpts = {} app.configure -> app.use compound.assetsCompiler.init() app.enable 'coffee' app.set 'cssEngine', 'stylus' app.use express.static(app.root + '/public', {maxAge: 86400000}) app.use express.bodyParser() app.use express.cookieParser() app.use express.session secret: 'secret', store: new RedisStore(redisOpts) app.use express.methodOverride() app.use app.router
var form = require('connect-form-sync'); app.configure(function(){ .... app.use(form({ keepExtensions: true })); app.use(express.bodyParser()); .... });
And use it in the controller like that:
action('create', function () { this.file = new File(); var tmpFile = req.form.files.file; this.file.upload(tmpFile.name, tmpFile.path, function (err) { if (err) { console.log(err); this.title = 'New file'; flash('error', 'File can not be created'); render('new'); } else { flash('info', 'File created'); redirect(path_to.files); } }.bind(this)); });
Documentation is maintained by @1602 (Anatoliy Chakkaev) and @rattazong (Sascha Gehlich).
CompoundJS is licensed under the MIT License. Documentation is licensed under CC BY 3.0.
The CompoundJS and JugglingDB projects are free, but you can leave a tip here: