Angular project architecture. Controllers. Directives. Style guides - javascript

We currently have a project that we are going to scale up soon. I am in the process of refactoring the project to make sure it's a bit more maintainable in the long term, and isn't a complete headache for anyone new to the project.
Please ignore any syntactical errors below, I had to change a bit of the code to try and best illustrate the architectural issue.
We have tried to follow the john papa style guide as closely as possible.
currently our setup is as follows:
PageController
(function() {
'use strict';
angular
.module('front')
.controller('PageController', PageController)
/** #ngInject */
function PageController($rootScope, $scope, $timeout, toastr, dataService, allowed, Pusher) {
var vm = this;
vm.allowed = allowed;
vm.widget = {
};
activate();
function activate(){
getOurWidgetData();
} // end activate()
/* get widget data */
function getOurWidgetData() {
dataService.getData(
$rootScope.wf.api + '/endpoint',
function(response) {
vm.widget.data = response.data.data;
vm.widgetlastModified = new Date(response.data.timestamp * 1000);
},
function(error) {
vm.widget.error = error;
}
);
}
vm.getOurWidgetData = getOurWidgetData;
}
})();
In our PageController view:
<div class="col-xs-12 col-md-4">
<div class="panel">
<div class="panel__header">
<h2><span>Widget</span>
<last-updated last-updated="main.widget.lastModified" lu-error="main.widget.error" label="As at"></last-updated>
</h2>
</div>
<div class="panel__body">
<div our-widget="" our-data="main.widget.data"></div>
</div>
</div>
</div>
and then in OurWidget:
(function() {
'use strict';
angular
.module('front')
.directive('ourWidget', ourWidget);
/** #ngInject */
function ourWidget() {
var directive = {
restrict: 'A',
templateUrl: 'app/components/ourWidget/ourWidget.html',
scope: {
aumData: '='
},
controller: ourWidgetController,
controllerAs: 'ow',
bindToController: true
};
return directive;
/** #ngInject */
function ourWidgetController() {
var vm = this;
activate();
function activate() {
}
}
}
})();
Now, this directive / component is quite empty. My proposal at this point is to move the dataService call from the PageController and into the component.
So, the ourWidget directive looks like this:
(function() {
'use strict';
angular
.module('front')
.directive('ourWidget', ourWidget);
function ourWidget() {
var directive = {
restrict: 'E',
templateUrl: 'app/components/ourWidget/ourwidget.html',
scope: {
ourModel: '='
},
controller: ourWidgetController,
controllerAs: 'ow',
bindToController: true
};
return directive;
/** #ngInject */
function ourWidgetController($rootScope, dataService, constants, endpoints, $log) {
var vm = this;
vm.OurWidget = {};
activate();
function activate() {
getOurWidgetData();
} // end activate()
function getOurWidgetData() {
dataService.getData(
constants.apiv2 + endpoints.OurWidget,
function(response) {
$log.log(response);
vm.OurWidget.data = response.data.data;
vm.OurWidget.lastModified = new Date(response.data.timestamp * 1000);
},
function(error) {
vm.OurWidget.error = error;
}
);
}
vm.getOurWidgetData = getOurWidgetData;
}
}
})();
I have prototyped this and it works flawlessly, meaning I have a html tag like:
<our-widget></our-widget>
That I can put anywhere in the application I need it. This directive is now responsible for its data, and means we aren't copy pasting dataService calls across any page controller that this component would be needed.
My question is, is this a better way of doing it, having the dataService call inside the component / directive instead of the PageController? I'd say it is, as our previous method goes directly against the DRY principle. I'm getting a bit of resistance though as some suggest it is going against the Angular style guide, though I haven't seen any examples or suggestion not to do it in this way, or in fact anything alluding to this kind of project architecture besides having a dataService handle all our http request, which we already have.
And yes, I've noticed we're using $rootScope incorrectly, I am attempting to fix these things as I'm going along :)

Related

Binding a Directive Controller's method to its parent $scope

I will explain what exactly I'm trying to do before explaining the issue. I have a Directive which holds a form, and I need to access that form from the parent element (where the Directive is used) when clicking on a submit button to check fi the form is valid.
To do this, I am trying to use $scope.$parent[$attrs.directiveName] = this; and then binding some methods to the the Directive such as this.isValid which will be exposed and executable in the parent.
This works fine when running locally, but when minifying and building my code (Yeoman angular-fullstack) I will get an error for aProvider being unknown which I traced back to a $scopeProvider error in the Controller.
I've had similar issues in the past, and my first thought was that I need to specifically say $inject for $scope so that the name isn't lost. But alas.....no luck.
Is something glaringly obvious that I am doing wrong?
Any help appreciated.
(function() {
'use strict';
angular
.module('myApp')
.directive('formDirective', formDirective);
function formDirective() {
var directive = {
templateUrl: 'path/to/template.html',
restrict: 'EA',
scope: {
user: '='
},
controller: controller
};
return directive;
controller.$inject = ['$scope', '$attrs', 'myService'];
function controller($scope, $attrs, myService) {
$scope.myService = myService;
// Exposes the Directive Controller on the parent Scope with name Directive's name
$scope.$parent[$attrs.directiveName] = this;
this.isValid = function() {
return $scope.myForm.$valid;
};
this.setDirty = function() {
Object.keys($scope.myForm).forEach(function(key) {
if (!key.match(/\$/)) {
$scope.myForm[key].$setDirty();
$scope.myForm[key].$setTouched();
}
});
$scope.myForm.$setDirty();
};
}
}
})();
Change the directive to a component and implement a clear interface.
Parent Container (parent.html):
<form-component some-input="importantInfo" on-update="someFunction(data)">
</form-component>
Parent controller (parent.js):
//...
$scope.importantInfo = {data: 'data...'};
$scope.someFunction = function (data) {
//do stuff with the data
}
//..
form-component.js:
angular.module('app')
.component('formComponent', {
template:'<template-etc>',
controller: Controller,
controllerAs: 'ctrl',
bindings: {
onUpdate: '&',
someInput: '<'
}
});
function Controller() {
var ctrl = this;
ctrl.someFormThing = function (value) {
ctrl.onUpdate({data: value})
}
}
So if an event in your form triggers the function ctrl.someFormThing(data). This can be passed up to the parent by calling ctrl.onUpdate().

Passing data from controller to directive

I 'm trying to handle data that is coming from my database in a directive , however these data are being pulled by a controller and being assigned to scope like this:
Calendar Controller:
'use strict';
var CalendarController = ['$scope', 'EventModel', function(scope, EventModel) {
scope.retrieve = (function() {
EventModel.Model.find()
.then(function(result) {
scope.events = result;
}, function() {
});
}());
}];
adminApp.controller('CalendarController', CalendarController);
Calendar Directive:
'use strict';
var calendarDirective = [function($scope) {
var Calendar = {
init: function(events) {
console.log(events);
}
};
return {
link: function(scope) {
Calendar.init(scope.events);
}
};
}];
adminApp.directive('calendarDirective', calendarDirective);
But the data is undefined in the directive, and in the controller the data appears to be ok.
Thanks!
This is a common error for people starting out with AngularJS. This is a load order issue. The events scope variable is not defined when the directive link function is executed. One solution is to use a watch on the variable passed into the directive and load once it is defined.
return {
link: function(scope) {
scope.$watch('events', function() {
if(scope.events === undefined) return;
Calendar.init(scope.events);
});
}
};
In the above scenario controller and directives have isolated scope. One solution will be to use factory/service to communicate with the server and inject the service to controller and directive. Since service/factory is singleton you can cache the data & share is between controllers & directives.
try this way.
Directive html
<calendar-directive objEvent="events" ></calendar-directive >
JS :
'use strict';
var calendarDirective = [function($scope) {
return {
scope : { objEvent : '=objEvent'}
template: '<div>{{ objEvent }}</div>',
};
}];
Use angular isolated scopes.

Getting injected service from controller prototype

I'm trying to optimize my code by using prototype inheritance in a controller used by a directive. Since directives can be invoked several times, I thought it might be a good idea to do this. But my controllers prototype is depending on injected services, which angular complains it does not have access to.
(function (angular) {
'use strict';
angular.module('myModule', [])
.factory('MyService', MyService)
.controller('myController', MyController);
// My service
function MyService() {
return {
getMyStuff: getMyStuff
}
function getMyStuff() {
return 'stuff';
}
}
// My controller
MyController.$inject = ['MyService'];
function MyController(MyService) {
var ctrl = this;
// Provide some value to the template
}
MyController.prototype = {
getStuff: function () {
return MyService.getMyStuff(); // This does not work. MyService is not available from the prototype. Why??
}
}
})(angular);
How can I make the MyService available to the prototype?
I've even tried to use the $injector service in order to get it, but
getStuff: function () {
return angular.injector(['myModule']).get('MyService').getMyStuff();
}
complains even more...

Compile directives via service in angularjs

I'm trying to compile directive via angular service but unfortunately it doesn't work.
The idea is to show errors in popups.
I've modified $exceptionHandler service:
crm.factory('$exceptionHandler', function(popup) {
return function(exception) {
popup.open({message: exception});
}
});
The popup service looks like this:
crm.factory('popup', function ($document) {
return {
open: function (data) {
var injector = angular.element(document).injector(),
$compile = injector.get('$compile'),
template = angular.element('<popup></popup>');
// var ctmp = $compile(template.contents());
$compile(template.contents());
$document.find('body').append(template);
}
};
});
And I don't think that this was a good idea to hard code $compile service (but I haven't any ideas how to realize this in angular):
$compile = injector.get('$compile')
Popup directive:
crm.directive('popup', function () {
return {
restrict: 'E',
replace: true,
templateUrl: '/public/js/templates/common/popup.html',
link: function() {
console.log('link()');
},
controller: function () {
console.log('ctrl()');
}
};
});
May be there are some other ways to do this?
Thanks.
You can inject $compile directly into your service, also you're not quite using $compile correctly:
//commented alternative lines for allowing injection and minification since reflection on the minified code won't work
//crm.factory('popup', ['$document', '$compile', function ($document, $compile) {
crm.factory('popup', function ($document, $compile) {
return {
open: function (data) {
var template = angular.element('<popup></popup>'),
compiled = $compile(template);
$document.find('body').append(compiled);
}
};
});
//closing bracket for alternative definition that allows minification
//}]);

What's the recommended way to extend AngularJS controllers?

I have three controllers that are quite similar. I want to have a controller which these three extend and share its functions.
Perhaps you don't extend a controller but it is possible to extend a controller or make a single controller a mixin of multiple controllers.
module.controller('CtrlImplAdvanced', ['$scope', '$controller', function ($scope, $controller) {
// Initialize the super class and extend it.
angular.extend(this, $controller('CtrlImpl', {$scope: $scope}));
… Additional extensions to create a mixin.
}]);
When the parent controller is created the logic contained within it is also executed.
See $controller() for for more information about but only the $scope value needs to be passed. All other values will be injected normally.
#mwarren, your concern is taken care of auto-magically by Angular dependency injection. All you need is to inject $scope, although you could override the other injected values if desired.
Take the following example:
(function(angular) {
var module = angular.module('stackoverflow.example',[]);
module.controller('simpleController', function($scope, $document) {
this.getOrigin = function() {
return $document[0].location.origin;
};
});
module.controller('complexController', function($scope, $controller) {
angular.extend(this, $controller('simpleController', {$scope: $scope}));
});
})(angular);
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.js"></script>
<div ng-app="stackoverflow.example">
<div ng-controller="complexController as C">
<span><b>Origin from Controller:</b> {{C.getOrigin()}}</span>
</div>
</div>
Although $document is not passed into 'simpleController' when it is created by 'complexController' $document is injected for us.
For inheritance you can use standard JavaScript inheritance patterns.
Here is a demo which uses $injector
function Parent($scope) {
$scope.name = 'Human';
$scope.clickParent = function() {
$scope.name = 'Clicked from base controller';
}
}
function Child($scope, $injector) {
$injector.invoke(Parent, this, {$scope: $scope});
$scope.name = 'Human Child';
$scope.clickChild = function(){
$scope.clickParent();
}
}
Child.prototype = Object.create(Parent.prototype);
In case you use the controllerAs syntax (which I highly recommend), it is even easier to use the classical inheritance pattern:
function BaseCtrl() {
this.name = 'foobar';
}
BaseCtrl.prototype.parentMethod = function () {
//body
};
function ChildCtrl() {
BaseCtrl.call(this);
this.name = 'baz';
}
ChildCtrl.prototype = Object.create(BaseCtrl.prototype);
ChildCtrl.prototype.childMethod = function () {
this.parentMethod();
//body
};
app.controller('BaseCtrl', BaseCtrl);
app.controller('ChildCtrl', ChildCtrl);
Another way could be to create just "abstract" constructor function which will be your base controller:
function BaseController() {
this.click = function () {
//some actions here
};
}
module.controller('ChildCtrl', ['$scope', function ($scope) {
BaseController.call($scope);
$scope.anotherClick = function () {
//other actions
};
}]);
Blog post on this topic
Well, I'm not exactly sure what you want to achieve, but usually Services are the way to go.
You can also use the Scope inheritance characteristics of Angular to share code between controllers:
<body ng-controller="ParentCtrl">
<div ng-controller="FirstChildCtrl"></div>
<div ng-controller="SecondChildCtrl"></div>
</body>
function ParentCtrl($scope) {
$scope.fx = function() {
alert("Hello World");
});
}
function FirstChildCtrl($scope) {
// $scope.fx() is available here
}
function SecondChildCtrl($scope) {
// $scope.fx() is available here
}
You don't extend controllers. If they perform the same basic functions then those functions need to be moved to a service. That service can be injected into your controllers.
Yet another good solution taken from this article:
// base controller containing common functions for add/edit controllers
module.controller('Diary.BaseAddEditController', function ($scope, SomeService) {
$scope.diaryEntry = {};
$scope.saveDiaryEntry = function () {
SomeService.SaveDiaryEntry($scope.diaryEntry);
};
// add any other shared functionality here.
}])
module.controller('Diary.AddDiaryController', function ($scope, $controller) {
// instantiate base controller
$controller('Diary.BaseAddEditController', { $scope: $scope });
}])
module.controller('Diary.EditDiaryController', function ($scope, $routeParams, DiaryService, $controller) {
// instantiate base controller
$controller('Diary.BaseAddEditController', { $scope: $scope });
DiaryService.GetDiaryEntry($routeParams.id).success(function (data) {
$scope.diaryEntry = data;
});
}]);
You can create a service and inherit its behaviour in any controller just by injecting it.
app.service("reusableCode", function() {
var reusableCode = {};
reusableCode.commonMethod = function() {
alert('Hello, World!');
};
return reusableCode;
});
Then in your controller that you want to extend from the above reusableCode service:
app.controller('MainCtrl', function($scope, reusableCode) {
angular.extend($scope, reusableCode);
// now you can access all the properties of reusableCode in this $scope
$scope.commonMethod()
});
DEMO PLUNKER: http://plnkr.co/edit/EQtj6I0X08xprE8D0n5b?p=preview
You can try something like this (have not tested):
function baseController(callback){
return function($scope){
$scope.baseMethod = function(){
console.log('base method');
}
callback.apply(this, arguments);
}
}
app.controller('childController', baseController(function(){
}));
You can extend with a services, factories or providers. they are the same but with different degree of flexibility.
here an example using factory : http://jsfiddle.net/aaaflyvw/6KVtj/2/
angular.module('myApp',[])
.factory('myFactory', function() {
var myFactory = {
save: function () {
// saving ...
},
store: function () {
// storing ...
}
};
return myFactory;
})
.controller('myController', function($scope, myFactory) {
$scope.myFactory = myFactory;
myFactory.save(); // here you can use the save function
});
And here you can use the store function also:
<div ng-controller="myController">
<input ng-blur="myFactory.store()" />
</div>
You can directly use $controller('ParentController', {$scope:$scope})
Example
module.controller('Parent', ['$scope', function ($scope) {
//code
}])
module.controller('CtrlImplAdvanced', ['$scope', '$controller', function ($scope, $controller) {
//extend parent controller
$controller('CtrlImpl', {$scope: $scope});
}]);
You can use Angular "as" syntax combined with plain JavaScript inheritance
See more details here
http://blogs.microsoft.co.il/oric/2015/01/01/base-controller-angularjs/
I wrote a function to do this:
function extendController(baseController, extension) {
return [
'$scope', '$injector',
function($scope, $injector) {
$injector.invoke(baseController, this, { $scope: $scope });
$injector.invoke(extension, this, { $scope: $scope });
}
]
}
You can use it like this:
function() {
var BaseController = [
'$scope', '$http', // etc.
function($scope, $http, // etc.
$scope.myFunction = function() {
//
}
// etc.
}
];
app.controller('myController',
extendController(BaseController,
['$scope', '$filter', // etc.
function($scope, $filter /* etc. */)
$scope.myOtherFunction = function() {
//
}
// etc.
}]
)
);
}();
Pros:
You don't have to register the base controller.
None of the controllers need to know about the $controller or $injector services.
It works well with angular's array injection syntax - which is essential if your javascript is going to be minified.
You can easily add extra injectable services to the base controller, without also having to remember to add them to, and pass them through from, all of your child controllers.
Cons:
The base controller has to be defined as a variable, which risks polluting the global scope. I've avoided this in my usage example by wrapping everything in an anonymous self-executing function, but this does mean that all of the child controllers have to be declared in the same file.
This pattern works well for controllers which are instantiated directly from your html, but isn't so good for controllers that you create from your code via the $controller() service, because it's dependence on the injector prevents you from directly injecting extra, non-service parameters from your calling code.
I consider extending controllers as bad-practice. Rather put your shared logic into a service. Extended objects in javascript tend to get rather complex. If you want to use inheritance, I would recommend typescript. Still, thin controllers are better way to go in my point of view.

Categories

Resources