ES6 model layer for angular.js
Many of the so called MV* frameworks have some explicit support for a model layer. When working in backbone.js, the model layer is very clearly defined in Backbone.Model and Backbone.Collection.
Instances of Backbone.Model and Backbone.Collection interact with a rest api using $.ajax. In the world of backbone there is a lot of buy in to the evented architecture. When a model or collection finishes fetching, the instance publishes an event which listeners can then act on. They have clear #save, #create, #fetch methods that hit the server and update the model accordingly.
Since working with angular I’ve explored a few solutions (Restangular and $resource) for managing the model layer, but was left wanting. I feel like the designers of angular wanted to be a bit more flexible at the model layer and as a result didn’t build in much on top of plain-old-javascript-objects (POJO).
As a result Angular has a component called Service which when injected into a function new’s up an instance of the Service object which might contain your custom logic for interacting with a backend API or with local storage.
I found myself repeating a lot of logic for each service and essentially rewriting much of the Backbone.Model class in my angular services. For example:
myApp.factory('Workouts', function ($http, $q, loc) {
function url(id) {
if (id) {
return loc.apiBase + '/workouts/' + id;
}
return loc.apiBase + '/workouts';
}
function Workout(attrs) {
this.id = attrs.id;
this.completed_date = attrs.completed_date;
this.workout_sets = attrs.workout_sets;
}
return {
all: function () {
var dfd = $q.defer();
$http.get(url()).then(function (resp) {
var workouts = _.map(resp.data, function() {
return new Workout(w);
});
dfd.resolve(workouts);
}, function (resp, status) {
$rootScope.$broadcast('event:auth-loginRequired', status);
dfd.reject(resp.data);
});
return dfd.promise;
},
get: function (id) {
var dfd = $q.defer();
$http.get(url(id)).then(function (resp) {
dfd.resolve(resp.data);
}, function (resp) {
dfd.reject(resp.data);
});
return dfd.promise;
}
};
});
The all method here was essentially the same across all services in the system and only differed slightly in cases where I was massaging the incoming data.
@shawndromat pointed me towards a pattern where using a Model factory might help by returning a constructor function that I could use for each of my Model layer classes.
Here’s a simplified (without caching) version of my Model factory:
myApp.factory('Model', function ($http, $q, $window, loc) {
return function (options) {
var path = options.path;
var url = loc.apiBase + path;
class Model {
constructor(attrs) {
this.setup.apply(this, arguments);
attrs = attrs || {};
this.attributes = {};
this.set(this.parse(attrs));
this.initialize.apply(this, arguments);
}
get id() {
return this.attributes.id;
}
initialize() {
}
setup() {
}
set(attrs) {
for(var attr in attrs) {
this.attributes[attr] = attrs[attr];
}
return this;
}
get(key) {
return this.attributes[key];
}
parse(response) {
return response;
}
save() {
if(this.id) {
return this.update();
} else {
return this.create();
}
}
update() {
var dfd = $q.defer();
$http
.put(this.url(), this.attributes)
.then((response) => {
this.set(this.parse(response.data));
dfd.resolve(this);
}, (response) => {
dfd.reject(this);
});
return dfd.promise;
}
create() {
var dfd = $q.defer();
$http
.post(this.url(), this.attributes)
.then((response) => {
this.set(this.parse(response.data));
dfd.resolve(this);
}, (response) => {
dfd.reject(this);
});
return dfd.promise;
}
url() {
if(this.id) {
return url + '/' + this.id;
} else {
return url;
}
}
}
Model.url = () => { return url; };
var _all = [];
Model.all = function () {
$http.get(url, { cache: true }).then((response) => {
_.each(response.data, (data) => {
if(!_.any(_all, (m) => {
return m.id === data.id;
})) {
_all.push(new Model(data));
}
});
});
return _all;
};
Model.create = function (attributes) {
var model = new Model(attributes);
_all.push(model);
return model.save();
};
return Model;
};
});
By injecting the Model class into other factories I can create constructor functions that override extend business logic, as well as extend interactions with the backend.
myApp.factory('Workout', function (Model, WorkoutSet) {
var Workout = Model({ path: '/workouts' });
Workout.prototype.setup = function () {
this.workout_sets = [];
};
Workout.prototype.parse = function (response) {
if(response.workout_sets) {
console.log('response has workout_sets');
this.workout_sets = _.map(response.workout_sets, (s) => {
return new WorkoutSet(s);
});
delete response.workout_sets;
}
return response;
};
return Workout;
});
I would love to hear your comments and suggestions regarding this implementation.