I'm developing a small single page app using AngularJS and a REST backend powered by Django Rest Framework.
It's rather simple so far and everything is working, but before I go any further, some points don't feel "right"...
The application so far manages a list of projects which are associated to a city.
So I implemented 2 factories using $resource : Project and City.
- GET /projects returns all the projects in json
- GET /cities returns all the cities in json
Now I navigate in the app using ui-router and the following states :
- projects : display the list of projects
- project/{id} : display one project
- project/edit/{id} : creates a new project (if id is null) or updates the project
idem for cities.
Now I did associate a controller for each state, and basically I was querying the backend everytime the state changed to get the list of projects or the single project.
1) I thought it would make sense to maintain the list of project one and for all, by calling the Project.query() and City.query() in my app.run() and saving these in $rootScope.
Now evertime I update or delete an object, I need to iterate (forEach() ) over the whole $rootScope.projects looking for a matching id and update/remove accordingly.
Same goes when opening a single project (/project/{id}), I need to search into the $rootScope.projects to find the project.
Is that ok, or is it best practice to always synchronize with the server upon such operation ? For now there's only one user editing projects, but this may change in the future.
2) As it's anyway required to have the city name (and not only the id) in my project, when i GET /projects, I receive a nested object for city e.g. {id: 1, name: 'New York'}. When I update/create I send a flat representation of my project with just the id (e.g. city: 1). If everything goes well, the server replies with 201 OK and the project attached. The problem is that the project attached is also flat, so when I update the $rootScope.projects, I need to :
- first find the id of the project to update
- loop through the cities to find which city is associated with this project
- replace project.city with the city object found above.
This is ok... but maybe not optimal. Is there a better way to do it ? I can have a flat GET and use the $rootScope.cities to fill the template accordingly.
3) If I open directly the app on a state with one project only, like /#/project/1, the controller tries to find the project id 1 in $rootScope.projects before the GET is completed. I've added :
if(!$rootScope.$$phase) { //this is used to prevent an overlap of scope digestion
$rootScope.$apply(); //this will kickstart angular to recognize the change
}
just after both query() calls, and it works. Just again wondering if it's good practice...
Well, I'd be interested in hearing from some of you who already exprimented with this. I can provide more code, but as I said it's working, so I'm more looking for a feedback on the actual design of the angular app.
Thanks !
EDIT 1
Thanks for your reply, I've added a service, I don't even have to $watch, seems to work perfectly. I'd like to share some code, just to point out some bad practice or stuff that can be improved. I found it difficult to find complete examples...
So here you go, it's pretty straight forward I hope the comments make it clear enough. Basically that's my projects.js script, it defines the Project resource used to communicate via REST with the server, the ProjectService which handles the communication between the controllers and the Porject resource, and the different controllers used on the differents states of the application (list projects, view project and edit project).
/**
* Controllers and Resource manager for projects.
*/
/**
* This is the resource link to the REST framework.
* Adapted the "update" method (available view $update) to
* use the PUT method as per Django Rest requirements
*/
angularApp.factory('Project', ['$resource', function($resource){
return $resource('/api/projects/:id', {id: '@id'}, {
update: {method:'PUT', params: {id: '@id'}},
});
}]);
/**
* This Service handles the project management
*/
angularApp.factory('ProjectService', ['Project', function(Project) {
var projectsLoaded = false,
projects = [];
return {
/**
* Returns the complete list of the projects
* from the server.
* If the projects have already been loaded, then
* use the cache instead.
*/
getProjects: function() {
if (projectsLoaded) {
return projects;
} else {
projects = Project.query(function(){
projectsLoaded= true;
});
return projects;
}
},
/**
* Load a single project from the server.
* If the full list has already been loaded, then
* find it in the list instead
*
* @param Integer projectId
*/
getSingleProject: function(projectId) {
var toReturn = false;
if(!projectsLoaded) {
toReturn = Project.get({id: projectId});
} else {
projects.forEach(function(project, index) {
if(project.id == projectId) {
toReturn = project;
}
});
}
return toReturn;
},
getNewProject: function() {
return new Project();
},
/**
* Deletes a project.
* If the project list is already loaded, then update the list
* accordingly
*
* @param Project project : project to delete
* @param callbackSuccess function(result)
* @param callbackRejection function(rejection)
*/
delete: function(project, callbackSuccess, callbackRejection) {
project.$delete().then(function(result){
if(projectsLoaded) {
projects.forEach(function(project, index) {
if(project.id == result.id) {
projects.splice(index, 1);
}
})
};
callbackSuccess(result);
}, function(rejection) {
callbackRejection(rejection);
});
},
/**
* Creates a new project.
* If the project list is loaded, then add the newly created
* project to the list.
*
* @param Project projectToSave : the project to save in the database
* @param callbackSuccess function(result) : result is the value returned by the server
* @param callbackRejection function(rejection)
*/
save: function(projectToSave, callbackSuccess, callbackRejection) {
projectToSave.$save().then(function(result) {
if(projectsLoaded) {
projects.unshift(result);
}
callbackSuccess(result);
}, function(rejection) {
callbackRejection(rejection);
});
},
/**
* Updates a project, also updates the list if needed
*
* @param Project projectToUpdate to update
* @param callbackSuccess function(result)
* @param callbackRejection function(rejection)
*/
update: function(projectToUpdate, callbackSuccess, callbackRejection) {
projectToUpdate.$update().then(function(result){
if(projectsLoaded) {
projects.forEach(function(project, index) {
if(result.id == project.id) {
project = result;
}
})
}
callbackSuccess(result);
}, function(rejection) {
callbackRejection(rejection);
});
},
};
}]);
/**
* Controller to display the list of projects
*/
angularApp.controller('ProjectListCtrl', ['$scope', 'ProjectService', function ($scope, ProjectService) {
$scope.projects = ProjectService.getProjects();
}]);
/**
* Controller to edit/create a project
*/
angularApp.controller('ProjectEditCtrl', ['$scope', '$stateParams', 'ProjectService', '$state', function ($scope, $stateParams, ProjectService, $state) {
$scope.errors = null;
if($stateParams.id) {
$scope.project = ProjectService.getSingleProject($stateParams.id)
} else {
$scope.project = ProjectService.getNewProject();
}
$scope.save = function() {
ProjectService.save($scope.project,
function(result) {
$state.go('project_view', {id:result.id});
}, function(rejection) {
$scope.errors = rejection.data;
}
);
};
$scope.update = function() {
ProjectService.update($scope.project,
function(result) {
$state.go('project_view', {id: result.id});
}, function(rejection) {
$scope.errors = rejection.data;
}
);
};
}]);
/**
* Controller to show one project and delete it
*/
angularApp.controller('ProjectCtrl', ['$scope', '$stateParams', 'ProjectService', '$state', function($scope, $stateParams, ProjectService, $state) {
$scope.project = ProjectService.getSingleProject($stateParams.id)
$scope.delete = function() {
ProjectService.delete($scope.project,
function(result){
$state.go('projects')},
function(rejection){
console.log(rejection)
}
);
}
}]);