Organizing a AngularJS Controller - javascript

I'm new to the AngularJS world and come from a Backbone background. So far I'm loving it but there is quite a big difference in terms of architecture practices between the two ( someone should write an article on this lol ).
I'm starting to structure some quite large controllers and it just doesn't feel right. For instance, this is a basic control that deals with executing a search and populating the ng-grid control and infinite scrolling this grid.
var ctrl = ['$scope', 'model', '$modal', function ($scope, model, $modal) {
$scope.page = 0;
$scope.loading = true;
$scope.mySelections = [];
$scope.rows = [];
$scope.columnDefs = [{
field: 'Checked',
width: "50",
sortable: false,
headerCellTemplate: '<input class="ngSelectionHeader" type="checkbox" ng-show="multiSelect" ng-model="allSelected" ng-change="toggleSelectAll(allSelected)"/>',
cellTemplate: '<div class="ngSelectionCell"><input tabindex="-1" class="ngSelectionCheckbox" type="checkbox" ng-checked="row.selected" /></div>'
}];
$scope.gridOptions = {
data: 'rows',
columnDefs: "columnDefs",
enableColumnResize: true,
selectedItems: $scope.mySelections,
};
/**
* Pages the grid and returns a $.deferred
*/
var pageGrid = function () {
return model.ExecuteSearchForReport("4146", ++$scope.page)
.done(function (records) {
$.each(records, function (i, record) {
var fields = {};
$.each(record.Value, function (ii, field) {
var fieldKey = field.Key.replace(/\s/g, '');
fields[fieldKey] = field.Value;
});
$scope.rows.push(fields)
});
$scope.$digest();
});
};
/**
* Page the grid initally.
*/
pageGrid().done(function (records) {
createColumns(records);
$scope.loading = false;
// if the grid height
var gridHeight = $('.ngGrid').height();
var repage = function () {
if ($('.ngCanvas').height() < gridHeight) {
pageGrid().done(function () {
repage();
});
}
};
repage();
$scope.$digest();
});
/**
* Creates the columns for the grid based on the records.
*/
var createColumns = function (records) {
if (records.length) {
$.each(records[0].Value, function (ii, field) {
var fieldKey = field.Key.replace(/\s/g, '');
var col = {
field: fieldKey,
displayName: field.Key,
resizable: true
};
// all the other columns are small
if (fieldKey !== "FileName") {
col.width = "100";
}
$scope.columnDefs.push(col);
});
}
};
/**
* List for `ngGridEventScroll` event to page the data set.
*/
$scope.$on('ngGridEventScroll', function () {
pageGrid();
});
/**
* Move the secure button was clicked, load next screen.
*/
$scope.moveToSecure = function () {
$scope.loading = true;
model.GetSecureDetails().done(function (data) {
$scope.loading = false;
var modalInstance = $modal.open({
templateUrl: 'Views/Modal.html',
controller: ModalInstanceCtrl,
resolve: {
data: function(){
return {
header: "Move To Secure",
body: "Views/Move.html",
lists: data
};
}
}
});
modalInstance.result.then(function (formData) {
var defs = [];
$.each($scope.mySelections, function (i, sel) {
defs.push(model.MoveToSecure({
Id: sel.EventID.substring(4, sel.EventID.length),
Filename: sel.FileName
}));
});
$.when.apply($, defs).done(function () {
alert('Move completed');
});
});
});
};
}];
I know, its a lot and it feels unstructured somewhat to me, between all the variable initialization, private methods, and general initialization methods. It doesn't feel concise and deterministic to me, which is something I liked about Backbone. Anyone got any feedback on a better way to do this? Thanks!

You need to move most of that stuff into 'concise' directives. If you don't master directives, your controllers will always end up overburdened and crazy like this. Ideally your controller pretty much only takes data back and forth between your scope and services.

Related

How to properly bind object in 'bindings' using $compile in angular.js 1?

I want to dynamically compile component for inserting this to specific DOM element (DOM also dynamically created by 3rd party library).
So, I use $compile, $scope.
https://jsbin.com/gutekat/edit?html,js,console,output
// ListController $postLink life cycle hook
function $postLink() {
...
$timeout(function () {
ctrl.items.splice(0, 1);
$log.debug('First item of array is removed');
$log.debug(ctrl.items);
}, 2000);
}
but below $onChanges life cycle hook in ListItemController isn't executed.
// ListItemController $onChanges life cycle hook
function $onChanges(changes) {
if (!changes.item.isFirstChange()) {
$log.debug(changes); // Not executed
}
}
I guess that angular.merge to pass item before ListItemController controller instance initialization is a major cause.
var itemScope = $scope.$new(true, $scope);
itemScope = angular.merge(itemScope, {
$ctrl: {
item: item
}
});
I modified you code a bit to demonstrate what is going on w/ the one way binding.
angular.module('app', [
'list.component',
'list-item.component'
]);
/**
* list.component
*/
angular
.module('list.component', [])
.component('list', {
controller: ListController,
template: '<div id="list"></div>'
});
ListController.$inject = ['$compile', '$document', '$log', '$scope', '$timeout'];
function ListController($compile, $document, $log, $scope, $timeout) {
var ctrl = this;
ctrl.$onInit = $onInit;
ctrl.$postLink = $postLink;
function $onInit() {
ctrl.items = [
{
id: 0,
value: 'a'
},
{
id: 1,
value: 'b'
},
{
id: 2,
value: 'c'
}
];
}
function $postLink() {
var index = 0;
// Not entirely sure what you need to do this. This can easily be done in the template.
/** ie:
* template: '<div id="list" ng-repeat="item in $ctrl.items"><list-item item="item"></list-item></div>'
**/
var iElements = ctrl.items.map(function (item) {
var template = '<list-item item="$ctrl.items[' + (index) + ']"></list-item>';
index++;
// you don't want to create an isolate scope here for the 1 way binding of the item.
return $compile(template)($scope.$new(false));
});
var listDOM = $document[0].getElementById('list');
var jqListDOM = angular.element(listDOM);
iElements.forEach(function (iElement) {
jqListDOM.append(iElement);
});
$timeout(function () {
// this will trigger $onChanges since this is a reference change
ctrl.items[0] = { id: 3, value: 'ss' };
// this however, will not trigger the $onChanges, if you need to use deep comparison, consider to use $watch
ctrl.items[1].value = 's';
ctrl.items[2].value = 's';
}, 2000);
}
}
/**
* list-item.component
*/
angular
.module('list-item.component', [])
.component('listItem', {
bindings: {
item: '<'
},
controller: ListItemController,
template: '<div class="listItem">{{ $ctrl.item.value }}</div>'
});
ListItemController.$inject = ['$log'];
function ListItemController($log) {
var ctrl = this;
ctrl.$onChanges = $onChanges;
function $onChanges(changes) {
if (!changes.item.isFirstChange()) {
$log.debug(changes); // Not executed
}
}
}

bind controllers to service with ajax promise

I'm new to angularjs. In my webapp I'm trying to work with user contacts as follows.
SERVICE
app.service('Contacts', function ($http,$timeout,$q) {
return {
getData: function() {
var defer = $q.defer();
$http.get('../ListContacts')
.success(function(data) {
defer.resolve(data);
});
return defer.promise;
}
}
});
ContactsController, OtherControllers
$scope.contactsBook = {};
...
Contacts.getData().then(function(data) {
$scope.contactsBook = data;
});
I found the above method somewhere in SO itself. I used it because I don't want to use separate module for Contacts.
I can get data at page load. I can update my contacts at server through ajax posts (from ContactsController). Now I only need a way to update(/refresh) the list automatically in all controllers. How can I achieve that.
I found these three links related but being a newbie I'm unable to figure my way out.
While it is understandable that you may not want to update your current architecture, it may be necessary to adjust your calls slightly if you want to be able to easily share data between controllers via a service.
One flexible approach is to store the data in your service and register watchers in each controller. This allows you to call the service update from one controller (the Contacts controller) and have the change be reflected in all consuming controllers. Note the service is mocked.
You can find the working plunker example here.
Contacts Service:
var app = angular.module('app', []);
app.service('contactsService', function ($http) {
var contacts = [];
return {
loadData: function() {
var mockGet = $q.defer();
var data = [
{ id: 1, name: 'Jack' },
{ id: 2, name: 'Jill' }
];
contacts = data;
mockGet.resolve(contacts);
return mockGet.promise;
},
retrieveNewData: function() {
var mockGet = $q.defer();
var data = [
{ id: 1, name: 'Jack' },
{ id: 2, name: 'Jill' },
{ id: 3, name: 'Bob' },
{ id: 4, name: 'Susan' }
];
contacts = data;
mockGet.resolve(contacts);
return mockGet.promise;
},
getContacts: function () {
return contacts;
}
}
});
Contacts Controller:
app.controller('ContactsCtrl', ['$scope', 'contactsService',
function ($scope, contactsService) {
var vm = this;
vm.contacts = [];
vm.loadData = loadData;
vm.retrieveNewData = retrieveNewData;
$scope.$watch(angular.bind(contactsService, function () {
return contactsService.getContacts();
}), function (newVal) {
vm.contacts = newVal;
});
function loadData() {
contactsService.loadData();
}
function retrieveNewData() {
contactsService.retrieveNewData();
}
}
]);
Other Controller:
app.controller('OtherCtrl', ['$scope', 'contactsService',
function($scope, contactsService) {
var vm = this;
vm.contacts = [];
$scope.$watch(angular.bind(contactsService, function () {
return contactsService.getContacts();
}), function (newVal) {
vm.contacts = newVal;
});
}
]);
You can do
Contacts.getData().then(function(data) {
$scope.contactsBook = data;
$scope.$emit('contacts:updated', data);
});
And then, where you need to notify the controller about the update:
$rootScope.$on('contacts:updated', function(e, contacts) {
$scope.contacts = contacts;
});
Another approach
The service is holding the current contacts list
app.service('Contacts', function ($http,$timeout,$q) {
this.currentList = [];
this.getData = function() {
var defer = $q.defer();
$http.get('../ListContacts')
.success(function(data) {
defer.resolve(data);
});
return defer.promise;
}
});
In your controller:
Contacts.getData().then(function(data) {
$scope.contactsBook = data;
Contacts.currentList = data;
});
In other controller:
controller('AnotherController', function($scope, Contacts) {
$scope.contacts = Contacts.currentList;
});
If you are going to return an object literal you will need to turn your .service() into a .factory() module . In this case I'll be using a service module .
Example
Your service .
app.service('Contacts', function ($http,$timeout,$q) {
var Contacts = this;
contacts.getData = function() {
var defer = $q.defer();
$http.get('../ListContacts')
.success(function(data) {
defer.resolve(data);
});
return defer.promise;
}
}
return Contacts;
});
You will then need to inject this server into your ContactsController .
app.controller('ContactsController', function(Contacts){
$scope.data = null;
$scope.init = function(){
Contacts.getData().then(function(response){
$scope.data = response;
})
}
})
now data can be used in dom
Example
<li ng-repeat="x in data">{{x.name}}</li>

Angular Modal Service unknown provider

I am trying to get a angular modal form working but I am always getting an unknown provider error. I think I have included all the necessary files?
Here is my code for calling the service:
function deleteConfirm() {
var modalOptions = {
closeButtonText: 'Cancel',
actionButtonText: 'Delete Supplier',
headerText: 'Delete ' + supplierName + '?',
bodyText: 'Are you sure you want to delete this supplier?'
};
modalService.showModal({}, modalOptions).then(function(result) {
if (result === 'ok') {
alert("ok");
}
}, function(error) {
alert("Error deleting");
});
}
And here is the code for the service:
(function() {
'use strict';
modalService.$inject = '$uibModal';
angular.module('plunker').factory('modalService', modalService);
function modalService($uibModal) {
var injectParams = ['$uibModal'];
//var modalService = function($uibModal) {
var modalDefaults = {
backdrop: true,
keyboard: true,
modalFade: true,
templateUrl: 'modal.html'
};
var modalOptions = {
closeButtonText: 'Close',
actionButtonText: 'OK',
headerText: 'Proceed?',
bodyText: 'Perform this action?'
};
this.showModal = function(customModalDefaults, customModalOptions) {
if (!customModalDefaults) customModalDefaults = {};
customModalDefaults.backdrop = 'static';
return this.show(customModalDefaults, customModalOptions);
};
this.show = function(customModalDefaults, customModalOptions) {
//Create temp objects to work with since we're in a singleton service
var tempModalDefaults = {};
var tempModalOptions = {};
//Map angular-ui modal custom defaults to modal defaults defined in this service
angular.extend(tempModalDefaults, modalDefaults, customModalDefaults);
//Map modal.html $scope custom properties to defaults defined in this service
angular.extend(tempModalOptions, modalOptions, customModalOptions);
if (!tempModalDefaults.controller) {
tempModalDefaults.controller = function($scope, $uibModalInstance) {
$scope.modalOptions = tempModalOptions;
$scope.modalOptions.ok = function(result) {
$uibModalInstance.close('ok');
};
$scope.modalOptions.close = function(result) {
$uibModalInstance.close('cancel');
};
};
tempModalDefaults.controller.$inject = ['$scope', '$uibModalInstance'];
}
return $uibModal.open(tempModalDefaults).result;
};
}
}());
http://plnkr.co/edit/xNpbI42UJm8acODSOimR
Thanks for any help
angular.module('plunker').factory('modalService', modalService);
modalService.$inject = ['$uibModal'];
Try this!
Add to main app.js dependencies ['ui.bootstrap']
var app = angular.module('plunker', ['ui.bootstrap']);
You forgot to include "ui.bootstrap" into your app.
Simply bootstrap your app like this to correct the issue :
var app = angular.module('plunker', ["ui.bootstrap"]);

AngularJS sharing data form AJAX $http request

I have the following controllers:
angular.module('app').controller('ParentCtrl', function(Service) {
var list = []
var init = function () {
Service.query().success(function() {
list = Service.getList();
});
}
});
angular.module('app').controller('ChildCtrl', function(Service) {
var list = []
var init = function () {
list = Service.getList();
}
});
angular.module('app').factory('Service', function($http) {
list = []
return {
query : function() {
$http.get('/path/to/my/api').success(function(data){
list = data
})
},
getList: function() {
return list;
}
}
});
My HTML is as follows:
<div ng-controller="ParentCtrl as parent">
<div ng-controller="ChildCtrl as child">
</div>
</div>
So basically when I receive the AJAX request I want both the controllers to get updated with the data
The best way would be to return the promise from $http.get:
angular.module('app').factory('Service', function($http) {
var promise;
return {
getList: function() {
if (promise) {
return promise;
}
promise = $http.get('/path/to/my/api');
return promise;
}
}
});
angular.module('app').controller('ParentCtrl', function(Service) {
var list = [];
var init = function () {
Service.getList().then(function(response) {
list = response.data;
});
}
});
angular.module('app').controller('ChildCtrl', function(Service) {
var list = [];
var init = function () {
Service.getList().then(function(response) {
list = response.data;
});
}
});
You can broadcast custom message to rootScope and Your controllers will get this message and handle it.
angular.module('app').controller('ParentCtrl', function($rootScope, $scope, Service) {
$scope.list = [];
$scope.$on('Service:list', function(event, data){
$scope.list = data;
});
});
angular.module('app').controller('ChildCtrl', function($rootScope, $scope, Service) {
$scope.list = [];
$scope.$on('Service:list', function(event, data){
$scope.list = data;
});
Service.query(); // run once, get in both controllers
});
angular.module('app').factory('Service', function($rootScope, $http) {
var list;
return {
query : function() {
$http.get('/path/to/my/api').success(function(data){
list = data;
$rootScope.$broadcast('Service:list', list);
})
},
getList: function() {
return list;
}
}
});
You could handle it in many ways. One simple way is to cache the promise and return it.
Example:-
angular.module('app').factory('Service', function($http, $q) {
var listPromise;
return {
getList: function() {
//If promise is already present then
return listPromise || (listPromise = $http.get('/path/to/my/api').then(function(response){
return response.data;
})
//Use the below 2 blocks (catch and finally) only if you need.
//Invalidate in case of error
.catch(function(error){
listPromise = null;
return $q.reject(error);
})
//Use finally to clean up promise only if you only need to avoid simultaneous request to the server and do not want to cache the data for ever.
.finally(function(){
listPromise = null;
}));
}
}
});
and in controller:
angular.module('app').controller('ParentCtrl', function(Service) {
var list = [];
var init = function () {
Service.getList().then(function(data) {
list = data;
});
}
});
angular.module('app').controller('ChildCtrl', function(Service) {
var list = [];
var init = function () {
Service.getList().then(function(data) {
list = data;
});
}
});
Caching a promise will make sure that it really does not matter who makes the first call and you always make the same service call to get the data and service will manage data caching via promise and prevent any duplicate calls.
Another practice is to implement a one-way data flow using flux pattern. Where you create stores that maintains data and it will make ajax call via a dispatcher and emits event to the subscribers of change event. There is an angular library (flux-angular) that can be used as well to implement this pattern. This will really help synchronize data between multiple components regardless of whether they are parent/child or siblings and regardless of who makes the first call etc..

AngularJS common $destroy for all scopes

I've created an Angular service which serves as a simple mechanism to handle success/warning/error/info alerts to the user in a common place throughout my app (code below). These alerts are bound to an Angular-UI alert element, listing all the alerts. My controller handles the plumbing.
So my question is how can I cause every controller in my app to call $alert.clear() upon the controller's destruction? I believe I can do this the hard way by calling something like this from every single controller:
$scope.$on('$destroy', function(){
$alerts.clear();
});
However, I don't really want that boilerplate stuff sprinkled everywhere. I'd like to be able to control that behavior common to ALL controllers in my app once and forget about it.
Thanks in advance for any gentle nudge or violent thwack in the right direction!
HTML snippet
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{alert.msg}}</alert>
service.alert.js
app.factory('$alert', function() {
var alerts = [];
var clearAlerts = function() {
alerts = [];
};
var closeAlert = function(index, clearOthers) {
alerts.splice(index, 1);
};
var createAlert = function(type, message, clearOthers) {
if (clearOthers)
alerts = [];
alerts.push({type: type, msg: message});
};
var alertSuccess = function(message, clearOthers) {
clearOthers = clearOthers || true;
createAlert('success', message, clearOthers);
};
var alertInfo = function(message, clearOthers) {
clearOthers = clearOthers || true;
createAlert('info', message, clearOthers);
};
var alertWarning = function(message,clearOthers) {
clearOthers = clearOthers || true;
createAlert('warning', message, clearOthers);
};
var alertDanger = function(message, clearOthers) {
clearOthers = clearOthers || true;
createAlert('danger', message, clearOthers);
};
return {
$alerts: function() { return alerts; },
$success: function(message, clearOthers) { return alertSuccess(message, clearOthers); },
$info: function(message, clearOthers) { return alertInfo(message, clearOthers); },
$warning: function(message, clearOthers) { return alertWarning(message, clearOthers); },
$danger: function(message, clearOthers) { return alertDanger(message, clearOthers); },
$clear: function() { return clearAlerts(); },
$close: function(index) { return closeAlert(index); }
};
});
You can inherit a controller in all of your controllers.
It still involves you making all your controllers children of a parent controller but other than that it will work flawlessly.
The controller inheritence is done like this:
app.controller('ParentCtrl', function ($scope) {
"use strict";
$scope.$on("$destroy", function (event, val) {
alert("Controller Destoryed");
});
});
app.controller('ChildCtrl', ['$scope', '$controller', function ($scope, $controller) {
"use strict";
$controller('ParentCtrl', {$scope: $scope});
}]);
Here is a plunkr demo (notice that a $destory event is broadcast on the $scope so this example works exactly as if a $destroy event was broadcast)
This is may helps you
clearAlerts: function() {
for(var x in this.alerts) {
delete this.alerts[x];
}
}
Please take a look at this Demo

Categories

Resources