Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keeping CRUD DRY with Restangular: directives, inheriting controllers or wrapping services?

I'm relatively new to AngularJS, and I've been experimenting with various ways to keep my AngularJS CRUD methods DRY using Restangular, so I'd appreciate any advice from the community.

I have a lot of API endpoints, and for each I'd like to show a list of items, allowing each item to be selected (in order to show that item's details) and deleted, as well as adding items to the list.

The approaches I've tried so far each end up either very non-DRY, not hooking into $scope properly, or being somewhat hacky...

Wrapping Restangular

Wrapping Restangular into a service, and using addElementTransformer to set additional methods on the returned collections seemed like a decent approach; the select and add methods are clean and easily implemented from the template, and automatically update the list when the promise is resolved. However, because the delete method is actually called from within the element scope (child of the collection), updating the list after remove requires a very dubious injection of $scope into the service - the best I could think of was as follows:

(Note: I've made the examples easier to read by illustrating just a single API endpoint/controller/template)

app/js/services.js

function ListTransformer(collection) {
    collection.selected = {}
    collection.assignedTo = {}
    collection.assignedName = ""

    collection.add = function(item) {
        collection.post(item)
        .then(function(response){
            collection.push(response);
        })
    };
    collection.delete = function(item, scope) {
        item.remove()
        .then(function(response){
            console.log(item)
            console.log(collection)
            collection.assignedTo[collection.assignedName] = _.without(collection, item);
        })
    };
    collection.select = function(item) {
        this.selected = item;
    };
    collection.assignTo = function(scope, name) {
        scope[name] = collection;
        collection.assignedTo = scope
        collection.assignedName = name
    };
    return collection;
};

angular.module('myApp.services', ['restangular'])
.factory('Company', ['Restangular',
    function(Restangular){
        var restAngular = Restangular.withConfig(function(Configurer) {
                Configurer.addElementTransformer('companies', true, ListTransformer);
        });

        var _companyService = restAngular.all('companies');

        return {
            getList: function() {
                return _companyService.getList();
            }
        }
    }
])

app/js/controllers.js

angular.module('myApp.controllers', [])
.controller('CompanyCtrl', ['$scope', 'Company', function($scope, Company) {

    Company.getList()
    .then(function(companies) {
        companies.assignTo($scope, 'companies'); // This seems nasty
    });
}]);

app/partials/companies.html

<ul>
    <li ng-repeat="company in companies">
      <a href="" ng-click="companies.select(company)">{{company.name}}</a> <a href="" ng-click="clients.delete(client)">DEL</a>
    </li>
</ul>
<hr>
{{companies.selected.name}}<br />
{{companies.selected.city}}<br />

Extending a Base Controller

Another approach I tried was to do the wiring of the template to the service in a base controller, that my other controllers inherit. However I still have the $scope issue here, and end up needing to pass the scope in from the template, which just doesn't seem right:

(edited down to just the delete method)

app/js/services.js

angular.module('myApp.services', ['restangular'])
.factory('ListController', function() {
    return {
        delete: function(scope, item, list) {
            item.remove();
            scope.$parent[list] = _.without(scope.$parent[list], item); 
        }
    };
});

app/js/controllers.js

.controller('CompanyCtrl', ['$scope', 'ListController', 'Restangular', function($scope, ListController, Restangular) {
    angular.extend($scope, ListController);

    $scope.companies = Restangular.all('companies').getList();
}])

app/partials/companies.html

<ul>
    <li ng-repeat="company in companies">
      <a href="" ng-click="companies.select(company)">{{company.name}}</a> <a href="" ng-click="delete(this, companies, company)">DEL</a>
    </li>
</ul>

Directives

I'm a bit new to directives, but after a lot of research it seems that it's probably the most AngularJS way to approach this, so I dived in. I've experimented a lot but please forgive any obvious errors with this; basically the problem is still that while I can delete the item from the list, unless I pass all the variables in to the directive I can't access the property on $scope that the list was assigned to originally, and so it doesn't update in the view.

app/js/directives.js

angular.module('myApp.directives', [])
.directive('delete', function() {
    return {
        restrict: 'E',
        replace: true,
        template: '<button>Delete</button>',
        link: function (scope, element, attrs) {
            element.bind('click', function() {
                var item = scope[attrs.item];
                item.remove();
                scope.$parent[attrs.from] = _.without(scope.$parent[attrs.from], item)
            });
        },

    };
});

app/partials/companies.html

<ul>
    <li ng-repeat="company in companies">
      <a href="" ng-click="companies.select(company)">{{company.name}}</a> <delete item="company" from="companies">
    </li>
</ul>

Basically, I can get everything to work, but it seems I either have to do a lot of repetitive code (which is plain wrong), or send my scope along with all my calls (which also seems very wrong), or initialise my service by passing the scope in (not the neatest solution for my controllers, but seems the most maintainable and least brittle way of achieving this.

Of course, I could just remove the functionality, or move it to a detail view or a checkbox/bulk action function, but I got engrossed in the problem and interested in finding out the best solution :)

Apologies if I'm missing something obvious!

like image 418
mkornblum Avatar asked Oct 21 '22 21:10

mkornblum


1 Answers

Starting with Directives am just jotting down notes:

Biggest learning curve is scope inheritance. General rule of thumb is if you don't add a scope property to object returned from directive, the directive inherits it's parent scope.

Directive you display for delete button therefore has same scope as CompanyCtrl since you haven't defined a scope object internal to directive.

That means you could write a delete function in either location and use it on your delete button. Stay away from creating own click handlers with element.bind('click'). Main reasons is when you manipulate scope items within it you generally have to call scope.$apply() to let angular know scope was changed from external event. Use ng-click since it binds directly to scope. For your delete function :

Slimmed down attributes. Is just a tag now that has a custom template and an angular scope bound click handler

<!-- company object comes from ng-repeat so can pass it right into a click handler as param-->
<delete ng-click="deleteCompany(company)">

Can write this function either in directive or in controller...since they share same scope.

.controller('CompanyCtrl', function($scope){
    $scope.deleteCompany=function(company){
        /* I've never used restangular...using native methods*/
        /* call my AJAX service and on resolve*/
        var idx= $scope.companies.indexOf(comapny);
        /* remove from array*/
        $scope.companies.splice(idx,1); 
    }
})

In directive would be identical function

link:function(scope,elem,attrs){
      /* call my AJAX service and on resolve*/
    var idx= scope.companies.indexOf(comapny);
    /* remove from array*/
    scope.companies.splice(idx,1); 
 }

I see you trying to share data between controllers by injecting one controller into another. First line of thinking would be for them to share a service, similarly with directives when needed such as deep nesting of directives.

Store the data in the service , inject service in controllers/directives that need it and so long as all data is stored as objects not as primitives( string or boolean variabes) the objects can be referenced anywhere in the app and share prototypical inheritance and be modified from anywhere regardless of scope nesting and angular object watch will change DOM anywhere that object is referenced.

Here's a little demo I was playing with earlier for someone who wanted to move from a form submit view...to another view with another controller and pass the data across the controllers. It basically takes the form data, passes it to shared service, makes a mock AJAX call , stores form data in service then changes path initializing new template and controller. Since data is stored in service it's ready when second view controller needs it. Might help you visualize what I'm talking about...maybe not. Also shows how automatic angular form validation kicks in

Switch views/controllers passing data through service demo

like image 95
charlietfl Avatar answered Nov 02 '22 09:11

charlietfl