Reputation: 15217
I have several simple REST resources (People, Houses, etc.), typically implemented as:
angular
.module('People')
.factory('Person', ['$resource', function($resource) {
return $resource('/api/people/:id', {id: '@id'}
);
}]);
Each type of resource has a View page and an Edit page. But the functionality in the controllers for each type of resource is largely the same. For instance, the View controller is always implemented as follows:
angular
.module('People')
.controller('PersonCtrl', ['$scope', '$routeParams', '$location', 'Person', function ($scope, $routeParams, $location, Person) {
// load the person
$scope.person = Person.get({id: $routeParams.id});
// edit this person
$scope.edit = function() {
$location.path($location.path() + '/edit');
};
}]);
The Edit controller is always something like this:
angular
.module('People')
.controller('PersonEditCtrl', ['$scope', '$routeParams', '$location', 'Person',
function($scope, $routeParams, $location, Person) {
// edit mode!
$scope.editMode = true;
// get the person
$scope.person = Person.get({id: $routeParams.id});
// delete this person
$scope.delete = function() {
if (window.confirm('Are you sure you wish to delete ' + $scope.person.name + '?')) {
Person.delete({id: $routeParams.id}, function() {
$location.path('/people');
});
}
};
$scope.save = function() {
Person.update($scope.person);
$location.path('/people/' + $routeParams.id);
};
}
]);
What is the best way to DRY up the controllers, so that I can reuse the same code for an arbitrary resource?
Upvotes: 0
Views: 535
Reputation: 3437
I recommend creating a module for the controller and anything other resources that the controller requires. Then just inject that module into your top level module.
So:
angular.module('Edit').
controller('EditController', ['$scope', function($scope) {
...
}]);
angular.module('People', ['Edit']);
angular.module('Houses', ['Edit']);
Edit: Turns out it's not quite that simple, right? As you pointed out in your comment, you also need to include some representation of the data that's being edited. This can get tricky, since you are trying to have a unified controller that can be used for editing different types of data. They must have a unified interface in order for this to work.
Disclaimer: In no way can I claim that this is Angular Best Practice, but this is something that I have implemented. In practice, there were some dirty hacks required, but I think more careful design of the data structures can alleviate this. Below is a simplified example of my implementation, with comments. The purpose of this controller was to display different types of data in lists.
var listModule = angular.module('list', [/*dependencies*/]).
// data in my case is the result of the route resolve.
// if you don't use routing, then you can probably just store the data in the service
controller('ListController', ['$scope', 'service', 'data',
function($scope, service, data) {
// whatever your controller needs to do - such as:
$scope.save = function() {
service.save($scope.someData).
then(function(r) {
// update the UI
});
};
}
]).
// this is the "interface" that your services will implement
// definitely not something you will find in the angular docs, but basic OOP, right?
// since it's a provider you can make it even more reusable with a configuration api
provider('Listable', [
function() {
return {
// optional configuration methods
setBaseUrl: function(url) {
this.baseUrl = url;
},
// if you aren't familiar with providers, this returns the service
$get: ['$http',
function($http) {
// access configs
var provider = this;
// This is the "class" that your services will "extend"
// whatever parameters you need
// probably best to use some conventions in your server api, to make this simpler
function Listable(name, fields) {
this.name = name;
this.fields = fields;
this.baseUrl = provider.baseUrl; // and other configs
}
// this is the API
Listable.prototype.index = function() {
return $http({
method: 'GET',
url: this.baseUrl + this.name // or something
});
};
Listable.prototype.save = function(data) {
return $http({
method: 'POST',
url: this.baseUrl + this.name,
data: data
});
};
return Listable;
}
]
};
}
]);
This would also be a handly place to keep related UI components like directives and filters that would be used by this controller.
Once that is done, you can use it like this:
angular.module('MyApp', ['list']).
factory('People', ['Listable',
function(L) {
var People = new L('people', ['first_name', 'last_name', 'dob']);
// you can also override methods of listable, or implement things that may be specific to each data type:
People.create = function() {
return {
// some initial Person object
}
};
return People; // People is a service
}
]);
Especially if you aren't using routing, I think it will be important to implement a load
method in the service, to get the data into the controller. You don't want to try and bind UI elements to data in the service, so you should load that into the model any time that it is used (in my opinion).
Again, I do not know if this implementation would pass the Angular Best Practice exam, but if you are nuts about being DRY (like me!), then it is a possibility.
Upvotes: 1