Inheritance w/ Angular's 'Controller As' vs. $scope - angularjs

tl:dr;
I want to consider the 'controller as' approach to simplify my inheritance process however I see some pitfalls around dependency injection and use in directives.
preface
I've been working on a pretty big Angular application for the last 6 months. I come from a more OOP language background but feel I've adapted fairly well to some of the quirkiness of javaScript and Angular.
With an application of this complexity, I think it necessitates great architectural solutions. I've elected to have baseController classes that UI controllers can inherit from using the current approach below.
current approach
All the current UI views/components/etc. are utilizing the $scope as the view model approach, aka. the classic approach. I achieved the controller inheritance like so:
NxBaseUiController
var NxBaseUiController = function($scope)
{
$scope.isLoggedIn = function(){}
$scope.doSomethingElse = function(){}
}
Here's a subclass base controller and it's inheritance
NxBaseListUiController
var NxBaseListUiController = function($scope)
{
var NxBaseUiController = require( './NxBaseUiController' )
NxBaseUiController.apply( this, arguments )
NxBaseListUiController .prototype = Object.create( NxBaseUiController.prototype );
$scope.query = require( './QueryModel' ).new()
}
What's crazy about this approach is that I'm having to maintain 2 series of inheritance all while keeping in mind of the existing NG scope inheritance that takes place in the view hierarchy. You can see this in the following UML diagram:
src - http://i.imgur.com/LNBhiVA.png
This all works so far, but moving onto a new section of the app, I want to start looking at the 'Controller As' syntax to see if there can be any simplification of the above approach.
questions
In all the examples I've seen of the 'controller as' approach, any methods attached are not defined on the prototype but rather the actual constructor like so:
function SomeController($scope)
{
this.foo = function(){};
}
Is there a reason why you wouldn't want to define the controller methods on the prototype?
The only reason I see attaching to the prototype as being problematic is you lose reference to your injected dependencies.
I realize I am coming at this from a very OO approach, however w/ an application of this size, being able to draw upon development patterns like inheritance seems necessary to solve code duplication.
Am I approaching this completely backwards? Are there any proven methods to achieving some semblance of inheritance in the controllers that doesn't require some wonky hack to work w/ the $scope?

There is no reason you can't use the prototype so long as you are OK with losing the ability to have actual private data vs having conventionally private data.
Let me explain with code:
function MyClass($http){
var privateData = 123,
$this = this;
$this.getAnswer = function(){
return privateData;
};
}
MyClass.$inject = ['$http'];
In that example you can't actually change privateData once the class has been instantiated.
function MyClass($http){
this._privateData = 123;
this.$http = $http;
}
MyClass.$inject = ['$http'];
MyClass.prototype = {
getAnswer: function(){
return this._privateData;
},
callHome: function(){
var _this = this;
_this.$http.get('/api/stuff')
.success(function(data){
_this.stuff = data;
});
}
};
In that example I am simply using a convention of prefixing my properties with an underscore (_) to signal that they should be treated as private.
Unless you are building a re-usable library however, this might not be a big deal.
Mind you some of this get's much simpler with ES6, so I would look into something like 6to5 or TypeScript if I were you. It might gel a little better with your OO background to write code like this:
//100% valid ES6
class MyClass extends BaseClass {
constructor(something){
super(something, 'foo');
}
someMethod(){
return this.foo;
}
}

Related

Reusing controller function + extending it

I have a page that consists of several panels that are very similar in their structure. They use one controller each. But due to their similarity i reuse the controller function like so:
function panelController($scope) {
...
}
angular.module('myApp.controllers')
.controller('panel1Controller', panelController);
angular.module('myApp.controllers')
.controller('panel2Controller', panelController);
The result is that panel1 and panel2 have their own distinct scopes but will look the same in terms of what the view can bind to.
Now however I am at a point where I want to use the same pattern for panel3 but with a slight extension. That is, I have something I want to include only in the $scope for panel3 only. So ideally I would like to be able to do something like this:
function panel3ControllerExtension($scope) {
$scope.panel3Field = "I must only live in panel3";
}
angular.module('myApp.controllers')
.controller('panel3Controller', panelController, panel3ControllerExtension);
But that's not possible. Are there any good patterns for this out there?
Edit:
The similar panels are similar only in what they expect the $scope to contain. Specifically the expect the scope to contain a customer object. So e.g. panel1 binds to $scope.customer.name and panel2 to $scope.customer.phone. ...So since they look different and behave different I don't think making a directive of them are the way to go. Correct me if I'm wrong.
Controllers in Angular are used effectively as constructors. So the rules for "inheritance" in Javascript apply to them. Some methods for extension:
apply/call the "base" function:
function panel3Controller($scope) {
// add the functionality of `panelController` to this scope
// in OO terms this can be thought of as calling the `super` constructor
panelController.call(this, $scope);
// add panel3 specific functionality
$scope.panel3SpecificThing = "...";
}
// just register the new controller
angular.module('myApp.controllers')
.controller('panel3Controller', panel3Controller);
This method will probably get you what you want with the minimum modifications to your code.
Use JS inheritance: Make the controller a JS "class" and let the child controller prototypically inherit from it. You may also want to use this in conjuction with the controller as syntax:
function PanelController($scope) {
this.$scope = $scope;
this.something = '...';
}
PanelController.prototype.someMethod = function() {
...
}
function Panel3Controller($scope) {
PanelController.call(this, $scope);
this.somethingElse = '...';
}
Panel3Controller.prototype = new PanelController();
Panel3Controller.prototype.constructor = Panel3Controller;
Panel3Controller.prototype.panel3SpecificMehod = function() {
...
};
If you are using ES2015, the above can be simplified:
class PanelController {
constructor($scope) {
...
}
...
}
class Panel3Controller extends PanelController {
constructor($scope) {
super($scope);
...
}
...
}
Again, you just register the new controller alone:
angular.module('myApp.controllers')
.controller('panel3Controller', Panel3Controller);
If the properties and methods are placed in the controller, as shown here, use the controller as syntax, i.e. in the HTML:
<div ng-controller="panel3Controller as p3ctrl">
<span>{{ p3ctrl.somethingElse }}</span>
Having a module system in place makes this pattern really useful.
Depending on the exact functionality of the controllers and, as pointed out in a comment, you may be able to extract the functionality of the controller(s) in one or more services. Then the controllers will be thin wrappers for these services. Again whether this is a good idea or not depends on the exact functionality of the controller(s).
As for directives: they are always the way to go :) And you can reuse your code as the controller of the directive instead of using it with ng-controller. You can even use two directives with different templates (the customer.name and customer.phone binding for example) and the same controller.

In Angular, what's the point of a service?

In the Angular documentation for services, I came across this code:
angular.
module('myServiceModule', []).
controller('MyController', ['$scope','notify', function ($scope, notify) {
$scope.callNotify = function(msg) {
notify(msg);
};
}]).
factory('notify', ['$window', function(win) {
var msgs = [];
return function(msg) {
msgs.push(msg);
if (msgs.length == 3) {
win.alert(msgs.join("\n"));
msgs = [];
}
};
}]);
My question is, why not do it much simpler and just define the function notify inside the $scope.callNotify function?
If services are just functions defined elsewhere, aren't there much simpler ways of accomplishing the same thing?
Just think about resuing that code in another controller; you will not be able to do it.
But if you place it in a service, it can be injected and then reused everywhere.
The primary reason is the idea is that there should not be any business logic present in the controller. It should just act as the glue between your scope and the model.
Another reasons are code reusability, single responsibility principle, better testability, the list is endless.
Overall it is a good programming practice to break your app in smaller pieces which are easily testable on their own, which improves overall maintainability and testability of your app.

AngularJS, sharing data between controllers

So, this isn't the typical question of HOW to do it. I know it can be done with a service or a factory but I was wondering if someone could share what the advantages/disadvantages would be of just creating a basic service, injecting it into each controller, and then extending the service with functions from each controller. Something similar to the following example..
app.service('HelperService', function() {
return {};
});
app.controller('Controller1', function($scope, HelperService) {
$scope.somefunc = function() {
//do stuff here
};
HelperService.somefunc = $scope.somefunc;
});
app.controller('Controller2', function($scope, HelperService) {
HelperService.somefunc();
});
This works, and works well. I feel a bit stupid for asking this but it just seems like I'm missing something here as to why this isn't used or recommended?
It may work, but its a bad idea.
Controller2 HelperService.somefunc() won't exist until Controller1 has been instantiated. So theres an implicit dependency of Controller2 on Controller1
the code on HelperService isn't in one place where it can be understood together
if you are doing some sort of data manipulation in that function, it really should be operating on data encapsulated by the HelperService.
The service is a singleton and it will be instantiated once calling new on the function itself -- the function you pass in is essentially a constructor. This will create the empty object you are returning for use everywhere, but if you want to return an object in such a way it makes more sense to use .factory, but this is not a big deal.
At any rate, you can consider your code to conceptually do this:
var HelperService = function () {}
var helperService = new HelperService;
function Controller1() {
helperService.someFunc = function () {}
}
function Controller2() {
helperService.someFunc();
}
I would consider this a dangerous thing to do for a couple of reasons:
Controller1 must be instantiated before Controller2 or else somefunc won't be available to Controller2. Ideally the controllers would have no knowledge of each other.
You are coupling Controller/ViewModel (since you're using scope) with service level logic, but these should be decoupled. HelperService shouldn't know about the controllers either. Instead, you should be injecting a service that has an API that the controllers expect to use. This doesn't always have to be HelperService, it just has to look like HelperService to the controllers and its API shouldn't change.
Without knowing specifics about what you're trying to do, it's hard to advise. In general you may rethink what you want to do, but you can extend functionality of services with other services. Consider services to be in their own layer.

How to provide initial data to Angular's controller/$scope?

There seems to be no way to provide data to an Angular controller other than through attributes in the DOM handled by directives (of which ngInit is a handy example).
I'd like to provide other "constructor" data, e.g. objects with functions to my
$scope.
Background: We have an existing dashboard-style single page application,
where each widget manages a <div>, and widget-instance-specific data
is provided as an object along with support functions, etc.. This object data
doesn't fit nicely into DOM attributes or ngInit calls.
I can't really come up with a better way to it than to have a global hash, and use an instance-specific unique key. Before calling angular.bootstrap(domElement, ['myApp']), we set up all "constructor" parameters in this global hash under the key and then use
<div ng-init='readInitialValuesFromHash("uniqueKey")'>...</div>
where readInitialValuesFromHash gets all its data from
globalHash["uniqueKey"] and stores what it needs it in $scope (possibly
just the "uniqueKey").
(What seems like an alternative is to use a directive and jQuery.data(), but jQuery.data uses a global hash behind the scenes)
Of course I can hide the global data in a function, but fundamentally still use
a singleton/global variable. This "global hash and pass key as param to ng init
trick" just seems like such a hack...
Am I missing something? Is there a better way, given that the
widget-instance-specific data is actually more complicated than suitable for
inserting in the DOM via directives/attributes due to the legacy dashboard
framework?
Are there dangers when putting complicated objects in the $scope as long as they aren't referenced by directives, {{}} or $scope.$watch() calls?
Angular's front page says:
Add as much or as little of AngularJS to an existing page as you like
So in light of that, how should I proceed?
EDIT: Comments have asked to make my question more clear. As an example of a non-trivial constructor parameter, assume I want to give this myObj to the controller, prototypical inheritance, function and all:
var proto = {
p1: "p1",
pf: function() {
return "proto"
}
};
function MyObj(ost) {
this.ost = ost;
}
MyObj.prototype=proto;
var myObj = new MyObj("OST");
So I have myObj, and I have a string:
<div ng-app='myApp' ng-controller="MyCtrl">....</div>
I put the string in the DOM, and call angular.bootstrap().
How to I get the real myObj object into MyCtrl's $scope for this <div>, not a serialized/deserialized version/copy of it?
Services is what you are looking for.
You can create your own services and then specify them as dependencies to your components (controllers, directives, filters, services), so Angular's dependency injection will take care of the rest.
Points to keep in mind:
Services are application singletons. This means that there is only one instance of a given service per injector. Since Angular is lethally allergic to global state, it is possible to create multiple injectors, each with its own instance of a given service, but that is rarely needed, except in tests where this property is crucially important.
Services are instantiated lazily. This means that a service will be created only when it is needed for instantiation of a service or an application component that depends on it. In other words, Angular won't instantiate services unless they are requested directly or indirectly by the application.
Services (which are injectable through DI) are strongly preferred to global state (what isn't), because they are much more testable (e.g. easily mocked etc) and "safer" (e.g. against accidental conflicts).
Relevant links:
Understanding Angular Services
Managing Service Dependencies
Creating Angular Services
Injecting Services into Controllers
Testing Angular Services
About Angular Dependency Injection
Example:
Depending on your exact requirements, it might be better to create one service to hold all configuration data or create one service per widget. In the latter case, it would probably be a good idea to include all services in a module of their own and specify it as a dependency of your main module.
var services = angular.module('myApp.services', []);
services.factory('widget01Srv', function () {
var service = {};
service.config = {...};
/* Other widget01-specific data could go here,
* e.g. functionality (but not presentation-related stuff) */
service.doSomeSuperCoolStuff = function (someValue) {
/* Send `someValue` to the server, receive data, process data */
return somePrettyInterestingStuff;
}
...
return service;
}
services.factory('widget02Srv', function () {...}
...
var app = angular.module('myApp', ['myApp.services']);
app.directive('widget01', function ('widget01Srv') {
return function postLink(scope, elem, attrs) {
attrs.$set(someKey, widget01Srv.config.someKey);
elem.bind('click', function () {
widget01Srv.doSomeSuperCoolStuff(elem.val());
});
...
};
});
ExpertSystem's answer gave me the hint that I needed. A separate controller instance for each widget. Note how the constructorParameter (==myObj) gets inserted into the controller.
function creatWidgetInstance(name) {
....
var controllerName = name + 'Ctrl';
// myObj comes from the original question
var constructorParameter = myObj;
widgetApp.controller(controllerName, function($scope) {
$scope.string = constructorParameter;
});
....
newWidget = jQuery(...);
newWidget.attr('ng-controller', controllerName);
angular.bootstrap(newWidget[0], ['widgetApp']);
....
}
See it working in a plunker
Perhaps a more beautiful solution is with a separate service too, as in:
function creatWidgetInstance(name) {
....
var controllerName = name + 'Ctrl';
var serviceName = name + 'Service';
// myObj comes from the original question
var constructorParameter = myObj;
widgetApp.factory(serviceName, function () {
return { savedConstructorParameter: constructorParameter };
});
widgetApp.controller(controllerName,
[ '$scope', serviceName, function($scope, service) {
$scope.string = service.savedConstructorParameter;
}
]
);
....
newWidget = jQuery(...);
newWidget.attr('ng-controller', controllerName);
angular.bootstrap(newWidget[0], ['widgetApp']);
....
}
See this in a working Plunker
The answer to the question requires backtracking a few assumptions. I thought that the only way to setup $scopewas to do it on a controller. And so the question revolves around how to "provide data to an Angular controller other than through attributes in the DOM handled by directives".
That was misguided.
Instead, one can do:
var scope = $rootScope.$new();
// Actually setting scope.string=scope makes for a horrible example,
// but it follows the terminology from the rest of the post.
scope.string = myObj;
var element = $compile(jQuery('#widgetTemplate').html())(scope);
jQuery('#widgets').append(element);
See this Plunker for a working example.

Inheriting AngularJS directives to create reusable components

I have been working on AngularJS for a while and have researched quite a lot. I am working on building reusable custom components/widgets using AngularJS directives. I have been quite successful at it. However, I want to adhere to inheritance while doing the same.
Let me explain with an example.
I have created a directive myButton that creates a button with all the styles & functionality. Now I would like to extend/inherit this myButton to create a myToggleButton with some added features & functionality. I do not wish to rewrite myButton features again.
I have explored various options.
As suggested in https://gist.github.com/BrainCrumbz/5832057, I created a factory/service and injected it into the directive. But this is not allowing me to take full benefit of the inheritance. I am still having to rewrite most of the properties.
I tried using plain object-oriented JavaScript for inheritance but in that case I would not be using AngulrJS directives. I want to follow Angular concepts strictly.
So any suggestions would be most welcome.
I have also found most inheritance examples less than ideal but I have come up with a solution I think is clean and allows for fully inheritance.
As services and directives do not have prototype information available in them and extending Object directly is not good you will want to create a high level base class which could contain constants or very simple generic logic.
var BaseService = function() {};
BaseService.prototype.toast = "french";
BaseService.prototype.halloween = "scary";
Next lets create an abstract service (same logic for a directive) that can be extended.
module.factory('AbstractDirective', function(
$http, $q, $rootScope, $compile, $timeout) {
$.extend(this, new BaseService);
// Additional logic and methods should be appended onto 'this'
this.doStuff = function() {
alert("abstract function called");
};
this.halloween = 'fun';
// If adding a variable to the prototype of this extended class is desired
// then this function would need to be extracted to its own variable
// where the prototype values can be set before the function
// is passed to the factory.
return this;
}
Now lets create an actual implementation:
module.directive('DirectiveImpl', ['AbstractDirective', function(AbstractDirective) {
$.extend(this, AbstractDirective);
// A great part about this implementation pattern is that
// DirectiveImpl does not need to pass anything to construct AbstractDirective.
// Meaning changes to AbstractDirective will have less impacts
// on implementing classes.
this.doStuff = function () {
// Call
AbstractDirective.doStuff();
// Implement some logic additional
alert(this.toast + "Toast\nHalloween is " + this.halloween );
}
return this;
}]);
for services use
module.factory
instead of
module.directive
When the doStuff function is call for DirectiveImpl you will get 2 alerts:
abstract function called
then
French Toast
Halloween is fun
A similar pattern can be followed to allow full inheritance for controllers as well but there is a bit more to get that to work.
I used this implementation (based on Enzey's model) to get my directive to work as intended.
module.directive('DirectiveImpl', ['AbstractDirective', function(AbstractDirective) {
return {
controller: ['$scope','$element', function( $scope, $element ) {
$.extend($scope, AbstractDirective);
// A great part about this implementation pattern is that
// DirectiveImpl does not need to pass anything to construct
// AbstractDirective.
// Meaning changes to AbstractDirective will have less impacts
// on implementing classes.
$scope.doStuff = function () {
// Call
AbstractDirective.doStuff();
// Implement some logic additional
alert($scope.toast + "Toast\nHalloween is " + $scope.halloween );
}
},
link: function( scope, element, opts ) {
scope.doStuff();
}
}
}]);

Resources