Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using same controller for all CRUD operations (Rails-alike)

I have an angular controller that fetches a resource on creation:

angular.module('adminApp')
  .controller('PropertiesCtrl', function ($log, $scope, Property, $location) {
    $scope.properties = Property.query()  
  });

Now I want to add logic to the controller to be able to create a "Property" resource:

angular.module('adminApp')
  .controller('PropertiesCtrl', function ($log, $scope, Property, $location) {
    $scope.properties = Property.query()  
    $scope.create = function(){
      //logic to create
    };
  });

However, when I am on the form to create a "Property", an unnecessary call is made to fetch all the properties first. How do I avoid this?


Potential solutions?

  1. I could create a separate controller specifically for the creation of the Property that would not fetch the properties. However, it would make it simpler to encapsulate all CRUD operations for a single resource under a single controller.
  2. I could create a function to fetch all properties. However, my index page uses "properties" directly. I would first need to fetch the data calling some method and then using the data (somehow?)
like image 506
Karan Avatar asked Aug 25 '13 16:08

Karan


2 Answers

My reaction is that it sounds like you are trying to use a controller as a service, and like you are trying to put to many features into one controller.

So there are two main things you should think about. First of all it is fairly important create controller that only have one specific purpose each. Note that this is not the same thing as using the controller only once, you are encuraged to use the same controller in several different places if you have a feature that should appear in more than one place. It just means that the controller shouldn't be doing several things at once.

Let's take a photo gallery as an example. While you could create one controller that gets all the photos, lets you add new photos, and lets you edit and delete existing photos all in one, this would be a bad idea. What if you decide that adding a photo can also be done from another page, "Page X"? If you where to reuse the same controller then you would also be requesting the gallery from the server and setting up controls for things you don't intend to be on that page.

If you instead made one controller that is only responsible for getting the content, a separate controller for adding new photos, another one for editing, etc, then it would be easy. You would just implement the create-controller on "Page X" and you don't have to worry about accidentaly triggering more than you wanted. You can choose to implement exactly the feature you want on a page and only that feature. This also keeps your controllers small, easy to read and quick to edit/bugfix, so it's a win/win!

Secondly, if you want to gather all your CRUD resources into one object (which I would also want to do), they should not be in a controller, they should be in a service. So you have one PhotoAPI which exposes CREATE, READ, UPDATE, and DELETE functions. Then your index controller just calls the READ function, your create controller the CREATE function etc. The controllers define what functions and data are available where, but the logic is in the combined service. That way you can clump your resources to make them easy to find, without creating the problems with having a combined controller.

So something like:

app.service('PhotoAPIService', [
function() {
   this.READ = function() {
     // Read logic
   }

  this.CREATE = function() {
     // Create logic
   }
}]);

app.controller('PhotoIndexController', [
'$scope',
'PhotoAPIService',
function($scope, PhotoAPIService) {
   $scope.photos = PhotoAPIService.READ(<data>);
}]);


app.controller('PhotoCreateController', [
'$scope',
'PhotoAPIService',
function($scope, PhotoAPIService) {
   $scope.createPhoto = PhotoAPIService.CREATE;
}]);
like image 76
Erik Honn Avatar answered Nov 06 '22 08:11

Erik Honn


I see from your question (and from your SO tags) that you want to create a Rails-like controllers in AngularJS. Since both frameworks (Rails and AngularJS) share a similar MVC principle this is actually quite easy to accomplish.

Both frameworks allow you to instruct different routes to use the same controller.

In Rails, your usual index/show/new/edit/destroy methods (actions) are provided out of the box (with scaffolding). These default actions are mapped to different, well established routes and HTTP methods.

CRUD/List routes in Rails enter image description here

Now, in AngularJS applications (or all SPAs for that matter) you need only a subset of these routes, because client-side routing understands only GET requests:

CRU/List routes in AngularJS enter image description here

AngularJS natively does not provide a scaffolding mechanism that would generate all your CRUD routes for you. But nevertheless it provides you with at least two different ways of wiring up your CRUD/List routes with a single controller.

Option 1 (Using $location.path())

Using location.path() method you can structure your PhotosCtrl to do different things depending on, well, location path.

Routes:

app.config(
  [
    '$routeProvider',
    function ($routeProvider) {

      $routeProvider
        .when('/photos', {
          templateUrl: 'photos/index.html',
          controller: 'PhotosCtrl'
        })
        .when('/photos/new', {
          templateUrl: 'photos/new.html',
          controller: 'PhotosCtrl'
        })
        .when('/photos/:id', {
          templateUrl: 'photos/show.html',
          controller: 'PhotosCtrl'
        })
        .when('/photos/:id/edit', {
          templateUrl: 'photos/edit.html',
          controller: 'PhotosCtrl'
        });

    }
  ]
);

Controller:

app.controller('PhotosCtrl', [
  '$scope',
  'Photos', // --> Photos $resource with custom '$remove' instance method
  '$location',
  '$routeParams',
  function($scope, Photos, $location, $routeParams){
    if($location.path() === '/photos'){
      // logic for listing photos
      $scope.photos = Photos.query();
    }

    if($location.path() === '/photos/new'){
      // logic for creating a new photo
      $scope.photo = new Photos();
    }

    if(/\/photos\/\d*/.test($location.path())){ // e.g. /photos/44
      // logic for displaying a specific photo
      $scope.photo = Photos.get({id: $routeParams.id});
    }

    if(/\/photos\/\d*\/edit/.test($location.path())){ // e.g. /photos/44/edit
      // logic for editing a specific photo
      $scope.photo = Photos.get({id: $routeParams.id});
    }

    // Method shared between 'show' and 'edit' actions
    $scope.remove = function(){
      $scope.photo.$remove();
    }

    // Method shared between 'new' and 'edit' actions
    $scope.save = function(){
      $scope.photo.$save();
    }

  }
]);

These four ifs makes the controller look a bit messy, but when replacing 4 different controllers with one, few conditionals are inevitable.

Option 2 (Using resolve property)

This option employes the resolveproperty of the route configuration object to produce different 'action identifier' for different routes.

Routes:

app.config(
  [
    '$routeProvider',
    function ($routeProvider) {

      $routeProvider
        .when('/photos', {
          templateUrl: 'photos/index.html',
          controller: 'PhotosCtrl',
          resolve: {
            action: function(){return 'list';}
          }
        })
        .when('/photos/new', {
          templateUrl: 'photos/new.html',
          controller: 'PhotosCtrl',
          resolve: {
            action: function(){return 'new';}
          }
        })
        .when('/photos/:id', {
          templateUrl: 'photos/show.html',
          controller: 'PhotosCtrl',
          resolve: {
            action: function(){return 'show';}
          }
        })
        .when('/photos/:id/edit', {
          templateUrl: 'photos/edit.html',
          controller: 'PhotosCtrl',
          resolve: {
            action: function(){return 'edit';}
          }
        });

    }
  ]
);

Controller:

app.controller('PhotosCtrl', [
  '$scope',
  'Photos',
  '$routeParams',
  'action'
  function($scope, Photos, $routeParams, action){
    if(action === 'list'){
      // logic for listing photos
      $scope.photos = Photos.query();
    }

    if(action === 'new'){
      // logic for creating a new photo
      $scope.photo = new Photos();
    }

    if(action === 'show')
      // logic fordisplaying a specfiic photo
      $scope.photo = Photos.get({id: $routeParams.id});
    }

    if(action === 'edit')
      // logic for editing a specfic photo
      $scope.photo = Photos.get({id: $routeParams.id});
    }

    // Method shared between 'show' and 'edit' actions
    $scope.remove = function(){
      $scope.photo.$remove();
    }

    // Method shared between 'new' and 'edit' actions
    $scope.save = function(){
      $scope.photo.$save();
    }

  }
]);

Both methods require using some conditionals in your controller, but the second method is at least a bit clearer to read, because the exact action is resolved inside the routing mechanism, which takes some logic off of your busy controller.

Of course, in any real-world application you'll probably have many more methods defined inside the controller, in which case your controller might get quite unreadable. These examples use a simple $resource instance (Phones) which relies on a simple RESTfull backend API (Rails?). But, when your view logic becomes complex you will probably want to employ Angular services/factories in order to abstract some of the code in your controllers.

like image 33
Stewie Avatar answered Nov 06 '22 07:11

Stewie