Skip to content

Instantly share code, notes, and snippets.

@kristianmandrup
Last active December 27, 2015 11:09
Show Gist options
  • Save kristianmandrup/7316241 to your computer and use it in GitHub Desktop.
Save kristianmandrup/7316241 to your computer and use it in GitHub Desktop.
An attempt to refactor the racer-example by @Sebmaster to make for a more scalable example code that can be more easily be extended to include more models and should also make the code easier to understand. This example is partly inspired by the todos example that comes with racer. See https://github.com/codeparty/racer/tree/master/examples/todos
# refactoring of https://github.com/Sebmaster/racer-example/blob/master/app.js
var racer = require('racer');
var express = require('express');
var app = express();
var http = require('http');
var server = http.createServer(app);
var store = racer.createStore({
server: server,
db: require('livedb-mongo')(process.env.MONGOLAB_URI || process.env.MONGOHQ_URL, { safe: true }),
redis: require('redis-url').connect(process.env.REDISCLOUD_URL)
});
app.use(express.static(__dirname + '/public'));
app.use(require('racer-browserchannel')(store));
app.use(express.bodyParser());
app.get('/', function (req, res) {
res.sendfile(__dirname + '/public/index.htm');
});
// function to send back a bundle of the updated data
var racerDataBundler = function (err, entries) {
if (err) {
res.status(500);
res.send(err);
} else {
model.bundle(function (err, bundle) {
res.send(JSON.stringify(bundle));
});
}
}
var models = ['todos', 'users'];
var storedModel = store.createModel();
app.get('/model', function (req, res) {
// subscribe on updates to each model
for (model in models) {
storedModel.subscribe(model, racerDataBundler);
}
});
// not sure what this is for!?
// Sebmaster pls explain ;)
store.bundle(__dirname + '/client.js', function (err, js) {
app.get('/script.js', function (req, res) {
res.type('js');
res.send(js);
});
});
# https://github.com/Sebmaster/racer-example/blob/master/client.js
angular.module('racer.js', [], ['$provide', function ($provide) {
var copyUtils = require('./copyUtils');
var extendObject = copyUtils.extendObject;
var cloneObject = copyUtils.cloneObject;
// call function fn immediately using a zero timeout (for defered promise?)
var setImmediate = window && window.setImmediate ? window.setImmediate : function (fn) {
setTimeout(fn, 0);
};
var racer = require('racer');
$provide.factory('racer', ['$http', '$q', '$rootScope', function ($http, $q, $rootScope) {
// get the initial global model data from racer
$http.get('/model').success(function (data) {
racer.init(data);
});
// create a defered promise
var def = $q.defer();
// when racer is ready we wait for it to return the global model (one large JSON structure)
racer.ready(function (model) {
var paths = {};
var oldGet = model.get;
// here we override Model.prototype.get (see Racer API)
model.get = function (path) {
// has local value not yet been set?
if (!paths[path]) {
// local value has yet to be set
// get the object that was muatated in the global racer model
var oldObj = oldGet.call(model, path);
// set local value of the object model to the old value
paths[path] = oldObj
var racerPath = path ? path + '**' : '**';
// on any mutation event to global model emitted by racer
model.on('all', racerPath, function () {
// clone data (mutated object)
// since angular would set $ properties in the racer object otherwise
var newData = cloneObject(oldObj);
var localObj = paths[path];
// update the local model (paths), by extending with newData
// overwriting existing data with latest data from global racer model
paths[path] = extendObject(newData, localObj);
// immediately notify Angular rootScope of change, so it can react
setImmediate($rootScope.$apply.bind($rootScope));
});
}
// return updated model (localObj)
return paths[path];
};
// resolve promise on global racer model
def.resolve(model);
// apply changes to angular rootScope
$rootScope.$apply();
});
// return deferred promise
return def.promise;
}]);
}]);
// similar to jQuery extend (ie merging objects)
var extendObject = function(from, to) {
if (from === to) return to;
if (from.constructor === Array && to && to.constructor === Array) {
for (var i = 0; i < from.length; ++i) {
to[i] = extendObject(from[i], to[i]);
}
to.splice(from.length, to.length);
return to;
} else if (from.constructor === Object && to && to.constructor === Object) {
for (var key in to) {
if (typeof from[key] === 'undefined') {
delete to[key];
}
}
for (var key in from) {
to[key] = extendObject(from[key], to[key]);
}
return to;
} else if (to === undefined) {
return extendObject(from, new from.constructor());
} else {
return from;
}
}
// convenience method, for when we simply want to clone, not merge two objects
var cloneObject = function(obj) {
return extendObject(obj, undefined);
}
module.exports = {
extendObject: extendObject,
cloneObject: cloneObject
}
The nice thing about this code, is that client.js is a general purpose file for linking Angular with Racer. It can be used with any model.
In app.js you simply have to provide the models you want to subscribe to using:
var models = ['todos', 'users'];
So both of the files could be made into reusable module, app.js taking the list of models to subscribe to as constructor argument!
The key then, is really public/app.js, especially:
function TodoCtrl($scope, model) {
...
}
Which is the Angular Controller, linking the model to the Angular $scope. Here we simply define the getters and setters, using the racer Model.prototype interface (see Racer API), such as model.get(modelName), model.set(dataObj) and model.add(modelName, dataObj) :)
So the next step would be to modularize this sample app.
We should also avoid the app.get('/model') on the server and instead use sockets for this IMO,
unless we want to expose the whole model as a REST interface accessible by HTTP GET via this path?
$http.get('/model').success(function (data) {
racer.init(data);
});
We could use socket.io for this instead:
http://stackoverflow.com/questions/14389049/how-to-use-angularjs-with-socket-io or
http://net.tutsplus.com/tutorials/javascript-ajax/real-time-chat-with-nodejs-socket-io-and-expressjs/
// socket.io config (server)
var io = require('socket.io').listen(app.listen(port));
// function to send back a bundle of the updated data
var racerDataBundler = function (err) {
if (err) {
io.emit('error', err);
} else {
model.bundle(function (err, bundle) {
io.emit('data', JSON.stringify(bundle));
});
}
}
io.sockets.on('connection', function (socket) {
for (model in models) {
storedModel.subscribe(model, racerDataBundler);
}
});
app.factory('socket', function ($rootScope) {
var socket = io.connect();
return {
on: function (eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
},
emit: function (eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
})
}
};
on the client:
var $socket = io.connect('http://localhost:3700');
$socket.on('data').success(function (data) {
racer.init(data);
});
or perhaps using Angular socket factory
function MyCtrl($scope, socket) {
socket.on('message', function(data) {
...
}
"The subscribe is necessary actually (at least in the v0.5 rework). The subscription gets "transferred" to the client in the bundle() call.
Without the subscribe, no data will be available on the client. With a fetch, the data will not update.
It might be possible to subscribe to the model on the client instead of the server though."
This is worth investigating...
angular.module('MyApp', ['racer.js']).
config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) {
$locationProvider.html5Mode(true);
$routeProvider.
when('/', {
templateUrl: 'partials/todo.htm',
controller: TodoCtrl,
// using resolve to access racer model in controller (see below)
resolve: TodoCtrl.resolve
}).
otherwise({ redirectTo: '/' });
}]);
function TodoCtrl($scope, model) {
$scope.todos = model.get('todos');
$scope.users = model.get('users');
$scope.addTodo = function () {
model.add('todos', { text: $scope.newTodo, done: false });
};
$scope.addUser = function () {
model.add('users', { name: $scope.newUserName });
};
$scope.saveTodo = function (todo) {
model.set('todos.' + todo.id + '.done', todo.done);
return false;
};
$scope.saveUser = function (user) {
model.set('users.' + user.id + '.admin', user.admin);
return false;
};
}
// resolve is an angular method on a controller
// here setup to expose the global racer model
TodoCtrl.resolve = {
model: function (racer) {
return racer;
}
};
// global racer model
TodoCtrl.resolve.model.$inject = ['racer'];
TodoCtrl.$inject = ['$scope', 'model'];
<ul>
<li data-ng-repeat="todo in todos">
<input type="checkbox" data-ng-model="todo.done" data-ng-change="saveTodo(todo)"> {{todo.text}}
</li>
</ul>
Add new Todo:
<form data-ng-submit="addTodo()">
<dl>
<dt>To do:</dt>
<dd>
<input data-ng-model="newInput" type="text">
</dd>
<dd>
<input type="submit">
</dd>
</dl>
</form>
<ul>
<li data-ng-repeat="user in users">
<input type="checkbox" data-ng-model="user.admin" data-ng-change="saveUser(user)"> {{user.userName}}
</li>
</ul>
Add new User:
<form data-ng-submit="addUser()">
<dl>
<dt>User details:</dt>
<dd>
<input data-ng-model="newUserName" type="text">
</dd>
<dd>
<input type="submit">
</dd>
</dl>
</form>
@mgan59
Copy link

mgan59 commented Nov 19, 2013

@kristianmandrup thanks for this write/notes. I've been meaning to getting around to doing a deep dive into racerjs and this is a great starting point. I may try and go further and writeup some actual documentation on just racerjs for those that aren't interested in learning derby and would prefer to use angular/backbone or some other front-end framework. Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment