Вопрос

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)
      }
    );
  }
}]);

Нет правильного решения

Другие советы

You've got a good start, you just need to dig a little deeper into Angular and everything will fall into place.

To begin with, use Services/Factories/Providers. They function a lot like resources and can be injected, and they work as singletons. You should ALWAYS use services rather than $rootScope as a best practice, even though they work similarly, because you can't make as many silly mistakes with services and your code will be cleaner.

For question 1, for example, you might make a service for your Projects and Cities that uses your Project and City resources behind the scenes; this service would function as your data storage singleton instead of $rootScope, and it could provide convenience methods so the consumer doesn't have to manually do query() calls.

For question 2, it would be up to you whether to return only the changed project or all projects on the server. I would recommend returning all projects to avoid the problem you've found, or maybe having the server accept a parameter to let the consumer choose what data they want returned.

For question 3, as a rule of thumb, if you have to manually call $apply(), you might be doing something wrong. You should only be calling $apply() if you are performing code outside of the Angular framework (like if you're using a jQuery method, or handling a custom event) and need your model to update in response to what you did. In your case, since you're using Angular's resource, you shouldn't need to call $apply().

I think what you really want to do is $watch() your data services for changes. This is, basically, like calling $apply(), but the difference is that you're letting Angular determine when updates are needed, which can be more efficient, cleaner, and safer. Here's a hypothetical example of how you could set this up:

function MySuperController($scope, DataService){
    //get data from the service singleton
    $scope.projects = DataService.getCoolProjects();

    //watch for changes in that data
    $scope.$watch(
        //thing to watch
        //Will get called a LOT, so make sure it's not time-intensive!
        function(){
            return DataService.getCoolProjects();
        },
        //what to do on change
        function(changedData){
            $scope.projects = changedData;
            //no $apply() needed; angular will do automatically!
        },
        //do a 'deep' watch that checks the value of each project
        true
    );
}

Now, here's what will happen, in a nutshell:

  • You'll make your GET request, which will retrieve data and store it in DataService.
  • Angular will check its $watches
  • Your $watch will notice that DataService.getCoolProjects() is different than it used to be
  • Your code will change a property of your $scope
  • Angular will auto-apply your change, as usual

Give those a shot and see how they work. Once you get the hang of Services and $watch, you'll find that it's much easier to write good Angular code that works all the time, and not just "on accident".

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top