Code structure for re-usable and testable code Angular JS - angularjs

I have an Angular JS app that is communicating with a REST API. The app has an admin part that let a user create, read, update and delete a resource.
There's several different resources, one partial per resource where I define each member as an input field of some sort. I have created a directive that creates a two way binding for whats common for the templates.
I have also created a ngResource for each of my resources.
Now to my problem. I have the controller for one resource working, and it mostly contains just functions that are called directly from the partials (e.g submit() ), and communicates with the ngResource.
The code itself can, with a few exceptions, be copied to all of the other controllers and just change the resource name. This is what I want to avoid.
How can I create an abstract ParentCtrl and somehow inject the resource I need at that time?
Is this testable? Is this a bad approach of the problem?
Normally I would have tried to abstract a model instead of a controller, but I can't really see how in this case.
Edit: Added code
Resource:
angular.module('experienceServices', ['ngResource'])
.factory('Experience', function($resource){
return $resource('/api/experience/:id',
{'id': '#_id'}, {edit: { method: 'PUT' }});
});
Controller:
App.controller('ExperienceCtrl', function( $scope, Experience ) {
$scope.resources = [];
$scope.currentItem = {};
$scope.updateResources = function(){
$scope.resources = Experience.query();
};
$scope.findById = function(id){
for (var i = 0; i < $scope.resources.length; i++){
if ($scope.resources[i]._id === id){
return $scope.resources[i];
}
}
};
$scope.showItem = function(id){
$scope.currentItem = findById(id);
};
$scope.create = function (experience){
Experience.save(angular.copy(experience), function(){
$scope.updateResources();
});
};
$scope.editItem = function (experience){
var item = angular.copy(experience);
if (!item.hasOwnProperty('_id')){
alert('Can not modify non existing item');
}
Experience.edit(item, function(res){
$scope.updateResources();
});
};
$scope.edit = function(id){
$scope.experience = $scope.findById(id);
};
$scope.del = function(id){
Experience.remove({ id: id }, function(){
$scope.updateResources();
});
};
$scope.updateResources();
});
Directive:
App.directive('resources', function (){
return {
scope: {
list: '=',
display: '#',
edit: '&',
del: '&'
},
template: '<div ng-repeat="elem in list">Name: {{ elem[display] }}' +
' <button ng-click="edit({ item: elem._id })">Edit</button> ' +
' <button ng-click="del({ item: elem._id })">Delete</button> ' +
'</div>'
};
});

what about wrapping your entire API to a service and accessing it from the controllers ? you can pass the resource type as an argument to the functions

Related

value in directive not changing even after its input changes

my main controller code is as follows
var self = this;
self.info = {};
Restangular.all('detail').post(this.postbody).then(function(data){
// this.items = data.tiles;
self.items = Restangular.copy(data);
self.info = self.items.pd;
console.log(self.info);
});
the template for my main controller is as follows
<div ng-controller="prddetail as prd">
<pdinfotile dat=prd.info></pdinfotile>
<div>
In my directive I have used # for one way binding and displayed the value but when new values gets updated it is not getting displayed after call from restangular and these values are not updated in directives attribute. How do I proceed thanks in advance.
Note:
angular.module('brandmarshal')
.directive('pdinfotile',function () {
return{
scope:{
dat:'#'
},
restrict : 'E',
templateUrl:'../../templates/pd/pd_info.html',
controllerAs:'ctrl',
bindToController:true,
controller: function() {
var self = this;
self.obj = JSON.parse(self.dat);
console.log(self.obj);
}
}
});
You have the wrong binding value setup.
# is for passing in a string, etc.
= is for two way binding - kind of hacky considered now with enforcing 1 way flow.
< is the single way binding you are looking for!
for example:
angular.module('brandmarshal')
.directive('pdinfotile',function () {
return{
scope:{
dat:'<' //<------single way binding or try a = for 2 way binding
},
restrict : 'E',
templateUrl:'../../templates/pd/pd_info.html',
controllerAs:'ctrl',
bindToController:true,
controller: function() {
var self = this;
self.obj = JSON.parse(self.dat);
console.log(self.obj);
}
}
});

How do you get an Angular directive to auto-refresh when loading data asynchronously?

I've been creating a number of small directives and using hard-coded arrays for testing while I build out functionality. Now that I've got some of that done, I went back and created a service to load the data from a website via JSON; it returns a promise and when it's successful I update the property my template is based off of. Of course, as soon as I did that my directive stopped rendering correctly.
What is the preferred way of binding my directive to asynchronously loaded data so that when the data finally comes back my directive renders?
I'm using Angular 1.4.7.
Here's a simple example of my hard-coded version.
angular
.module('app', []);
angular.module('app').controller('test', function(){
var vm = this;
vm.inv = 'B';
vm.displayInv = function () {
alert('inv:' + vm.inv);
};
});
angular.module('app')
.directive('inventorytest', function () {
return {
restrict: 'E',
template: '<select ng-model="ctrl.selectedOption" ng-options="inv.code as inv.desc for inv in ctrl.invTypes"></select>{{ctrl.sample}}. Selected: {{ctrl.selectedOption}}',
scope: { selectedOption: '='},
bindToController: true,
controller: function () {
this.invTypes = [
{ code: 'A', desc: 'Alpha' },
{ code: 'B', desc: 'Bravo' },
{ code: 'C', desc: 'Charlie' },
];
this.sample = 'Hello';
},
controllerAs: 'ctrl'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="app" ng-controller="test as vm">
<inventorytest selected-option='vm.inv'></inventorytest>
<br/>
Controller: {{vm.inv}}
</div>
My service is essentially just a thin wrapper around an $http call, ex:
return $http({ method: 'GET', url: 'https://myurl.com/getinfo' });
And I had tried modifying my code to do something like:
this.invTypes = [ { code: '', desc: '' }];
ctrService.getInfo()
.then(function successCallback(response) {
this.invTypes = response.data;
}, function errorCallback(response) {
// display error
});
Like I said, that doesn't work since it seems Angular isn't watching this property.
Within the callback this has a different context and isn't what you want it to be.
You need to save a reference to the controller this and use that within any callbacks
// store reference to `this`
var vm = this;
vm.invTypes = [ { code: '', desc: '' }];
ctrService.getInfo()
.then(function successCallback(response) {
// must use reference to maintain context
vm.invTypes = response.data;
}, function errorCallback(response) {
// display error
});

angularjs - Sharing data between controllers through service

I have a 2 controllers [FirstController,SecondController] sharing two arrays of data (myFileList,dummyList) through a service called filecomm.
There is one attribute directive filesread with isolated scope that is bound to a file input in order to get the array of files from it.
My problem is that myFileList array in my service never gets updated when I select the files with the input. However, dummyList array gets updated immediately in the second div (inner2). Does anybody know why is this happening?
For some reason in the second ngrepeat when I switch from (fi in secondCtrl.dummyList) to (fi in secondCtrl.myFileList) it stops working.
Any help would be greatly appreciated.
Markup
<div ng-app="myApp" id="outer">
<div id="inner1" ng-controller="FirstController as firstCtrl">
<input type="file" id="txtFile" name="txtFile"
maxlength="5" multiple accept=".csv"
filesread="firstCtrl.myFileList"
update-data="firstCtrl.updateData(firstCtrl.myFileList)"/>
<div>
<ul>
<li ng-repeat="item in firstCtrl.myFileList">
<fileuploadrow my-file="item"></fileuploadrow>
</li>
</ul>
</div>
<button id="btnUpload" ng-click="firstCtrl.uploadFiles()"
ng-disabled="firstCtrl.disableUpload()">Upload
</button>
</div>
<div id="inner2" ng-controller="SecondController as secondCtrl">
<ul ng-repeat="fi in secondCtrl.dummyList">
<li>Hello</li>
</ul>
</div>
</div>
JS
angular.module('myApp', [])
.controller('FirstController',
['$scope','filecomm',function ($scope,filecomm) {
this.myFileList = filecomm.myFileList;
this.disableUpload = function () {
if (this.myFileList) {
return (this.myFileList.length === 0);
}
return false;
};
this.uploadFiles = function () {
var numFiles = this.myFileList.length;
var numDummies = this.dummyList.length;
filecomm.addDummy('dummy no' + numDummies + 1);
console.log('Files uploaded when clicked:' + numFiles);
console.log('dummy is now:'+ this.dummyList.length);
};
this.updateData = function(newData){
filecomm.updateData(newData);
console.log('updated data first controller:' + newData.length);
};
this.dummyList = filecomm.dummyList;
console.log('length at init:' + this.myFileList.length);
}]) //FirstController
.controller('SecondController',
['$scope', 'filecomm', function($scope,filecomm) {
var self = this;
self.myFileList = filecomm.myFileList;
self.dummyList = filecomm.dummyList;
console.log('SecondController myFileList - length at init:' +
self.myFileList.length);
console.log('ProgressDialogController dummyList - length at init:' +
self.dummyList.length);
}]) //Second Controller
.directive('filesread',[function () {
return {
restrict: 'A',
scope: {
filesread: '=',
updateData: '&'
},
link: function (scope, elm, attrs) {
scope.$watch('filesread',function(newVal, oldVal){
console.log('filesread changed to length:' +
scope.filesread.length);
});
scope.dataFileChangedFunc = function(){
scope.updateData();
console.log('calling data update from directive:' +
scope.filesread.length);
};
elm.bind('change', function (evt) {
scope.$apply(function () {
scope.filesread = evt.target.files;
console.log(scope.filesread.length);
console.log(scope.filesread);
});
scope.dataFileChangedFunc();
});
}
}
}]) //filesread directive
.directive('fileuploadrow', function () {
return {
restrict: 'E',
scope: {
myFile: '='
},
template: '{{myFile.name}} - {{myFile.size}} bytes'
};
}) //fileuploadrow directive
.service('filecomm', function FileComm() {
var self = this;;
self.myFileList = [];
self.dummyList = ["item1", "item2"];
self.updateData = function(newData){
self.myFileList= newData;
console.log('Service updating data:' + self.myFileList.length);
};
self.addDummy = function(newDummy){
self.dummyList.push(newDummy);
};
}); //filecomm service
Please see the following
JSFiddle
How to test:
select 1 or more .csv file(s) and see each file being listed underneath.
For each file selected the ngrepeat in the second div should display Hello. That is not the case.
Change the ngrepat in the second div to secondCtrl.dummyList
Once you select a file and start clicking upload, you will see that for every click a new list item is added to the ul.
Why does dummyList gets updated and myFileList does not?
You had a couple of issues.
First, in the filecomm service updateData function you were replacing the list instead of updating it.
Second, the change wasn't updating the view immediately, I solved this by adding $rootScope.$apply which forced the view to update.
Updated JSFiddle, let me know if this isn't what you were looking for https://jsfiddle.net/bdeczqc3/76/
.service('filecomm', ["$rootScope" ,function FileComm($rootScope) {
var self = this;
self.myFileList = [];
self.updateData = function(newData){
$rootScope.$apply(function(){
self.myFileList.length = 0;
self.myFileList.push.apply(self.myFileList, newData);
console.log('Service updating data:' + self.myFileList.length);
});
};
}]); //filecomm service
Alternately you could do the $scope.$apply in the updateData function in your FirstController instead of doing $rootScope.$apply in the filecomm service.
Alternate JSFiddle: https://jsfiddle.net/bdeczqc3/77/
this.updateData = function(newData){
$scope.$apply(function(){
filecomm.updateData(newData);
console.log('updated data first controller:' + newData.length);
});
};

using ng-include for a template that has directives that use data retrieved by XHR

I am using ng-include in order to include a persistent menu, that exists in all of the views of my SPA.
The problem is that I want to display different options and content in this menu per each user type(admin, guest, user etc.), and this requires the service function authService.loadCurrentUser to be resolved first.
For the purpose of managing this content easily and comfortably, I have created a simple directive, that takes an attribute with the required access level, and at the compile phase
of the element, if the permissions of the given user are not sufficient, removes the element and it's children.
So after failing miserably at trying to make the ng-include go through the routeProvider function, I've tried to use ng-init, but nothing seems to work, the user role remain undefined at the time that I am logging it out.
I am thinking about trying a new approach, and making the entire menu a directive that includes the template that is suitable for each user type, but first I would like to try and solve this matter.
Directive:
'use strict';
/* Directives */
angular.module('myApp.directives', []).
directive('restrict', function(authService){
return{
restrict: 'A',
prioriry: 100000,
scope: {
// : '#'
},
link: function(){
// alert('ergo sum!');
},
compile: function(element, attr, linker){
var user = authService.getUser();
if(user.role != attr.access){
console.log(attr.access);
console.log(user.role);//Always returns undefined!
element.children().remove();
element.remove();
}
}
}
});
Service:
'use strict';
/* Services */
angular.module('myApp.services', []).
factory('authService', function ($http, $q) {
var authServ = {};
var that = this;
that.currentUser = {};
authServ.authUser = function () {
return $http.head('/users/me', {
withCredentials: true
});
},
authServ.getUser = function () {
return that.currentUser;
},
authServ.setCompany = function (companyId) {
that.currentUser.company = companyId;
},
authServ.loadCurrentUser = function () {
var defer = $q.defer();
$http.get('/users/me', {
withCredentials: true
}).
success(function (data, status, headers, config) {
console.log(data);
that.currentUser.company = {};
that.currentUser.company.id = that.currentUser.company.id ? that.currentUser.company.id : data.main_company;
that.currentUser.companies = [];
for (var i in data.roles) {
that.currentUser.companies[data.roles[i]['company']] = data.roles[i]['company_name'];
if (data.roles[i]['company'] == that.currentUser.company.id){
that.currentUser.role = data.roles[i]['role_type'];
that.currentUser.company.name = data.roles[i]['company_name'];
// console.log(that.currentUser.role);
}
}
// defer.resolve(data);
defer.resolve();
}).
error(function (data, status, headers, config) {
that.currentUser.role = 'guest';
that.currentUser.company = 1;
defer.reject("reject");
});
return defer.promise;
}
return authServ;
});
Menu controller:
angular.module('myApp.controllers', []).
controller('menuCtrl', function($scope, $route, $location, authService){
//TODO: Check if this assignment should be local to each $scope func in order to be compliant with 2-way data binding
$scope.user = authService.getUser();
console.log($scope.user);
// $scope.companies = $scope.user.companies;
$scope.companyOpts = function(){
// var user = authService.getUser();
if(typeof $scope.user.company == 'undefined')
return;
var companies = [];
companies[$scope.user.company.id] = $scope.user.company.name;
for(var i in $scope.user.companies){
if(i != $scope.user.company.id){
companies[i] = $scope.user.companies[i];
}
}
// console.log(companies);
// if(nonCurrentComapnies.length > 0){
console.log(companies);
return companies;
// }
}
$scope.$watch('user.company.name', function(company){
for(var i in $scope.user.companies)
if(company == $scope.user.companies[i].id)
authService.setCompany(i);
});
$scope.$watch(function(){return authService.getUser().company; }, function(company){
//Refresh the page on company change here, first time, and each time the user changes the select
// $scope.companyOpts();
// $scope.currentComapany = company;
})
;})
Main SPA HTML page:
<div ng-init="authservice.loadCurrentUser" ng-include src="'partials/menu.html'"></div>
menu element that should be visible only to the admin:
<ul class="left" restrict access="admin">
<li>You are the admin!</li>
</ul>
Thanks in advance for any assistance!
I personally would do the "reverse" way. Which mean: I will add the menu in when the user role is "admin", or "user", etc...
This way, you can do something like this in the "restrict" directive:
...
var roleListener = $scope.$watch('user.role', function (newVal, oldVal) {
if (newVal == $scope.access) {
// add the menu items
// supposed that loadcurrentuser be called only once
// we should clear the watch
roleListener();
} else {
// personally, I would remove the item here too
// so the menu would be added or removed when user.role update
}
});
...
One more thing, for just display menu base on the user role, you can use ngSwitch, something like this:
<ul class="left" ng-switch="user.role">
<li ng-switch-when="admin">You are the admin!</li>
<li ng-switch-when="user">You are the user!</li>
<li ng-switch-default><img src="some-thing-running.gif"/>Your menu is loading, please wait...</li>
</ul>
And let the magical AngularJS binding render up the menus for you!
The call to authServ.getUser should also return a promise by calling internally
authServ.loadCurrentUser
which should be modified a bit to check if the user context exists to avoid making another API call and always returning resolve with the user context:
defer.resolve(that.currentUser);
Loading the user context should also be done early on as this enables the authorization of the app. The app.run function can be used for this purpose.
hope it helps others.

Write to chosen scope variable with a service method

If 'User' is a service and 'User.get' is a method that doing a request.
<button class="btn" ng-click="User.get(users,'id,uname,email,pw')">
<p>{{users}}</p>
How do I assign the request data to the scope variable 'users' with that code example?
I like that code style but I do not know how to implement it. Any ideas?
example on the service provider:
angular.module('User',[]).
provider('User', function() {
var path = 'user.php';
this.$get = function($http) {
var r = {};
r.get = function(variable,fields){
$http.get(path+'?fields='+fields).
success(function(data){
//variable = data;
});
}
return r;
};
this.setPath = function(p){path = p;};
});
Use a controller and write the handler for the ng-click inside it. Here's an example:
Controller:
function Ctrl($scope) {
$scope.getUsers = function (fields) {
$scope.users = User.get(fields);
}
}
HTML:
<div ng-controller="Ctrl">
<button class="btn" ng-click="getUsers('id,uname,email,pw')">
<p>{{users}}</p>
</div>
Another way to do it is to pass the this reference (scope) and the property name ('users') into the service:
<button class="btn" ng-click="User.get(this, 'users', 'id,uname,email,pw')">
And then on the User service, after getting the result, do something like this:
r.get = function(scope, prop, fields){
$http.get(path+'?fields='+fields).
success(function(data){
scope[prop] = data;
});
}
where scope = this and prop = 'users'.
IMHO, the first approach is the best one, the second one sounds a little hacky.

Resources