Reusing controller function + extending it - angularjs

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.

Related

What is key benefits of using controllerAS in angularJS

ng-controller="invoiceController as invoice" create new invoiceController constructor and assigned the same to scope behind the scene, which the similar thing $scope injection does in controller function parameter
am i right for the above point.
if yes then how scope is related to this.
Please help.
-- AngularJS Developer Guide - Conceptual Overview
"as" tells what "this" is supposed to point to. This way, you can create more than one instance of the controller in different scopes, without getting them confused thanks to their different names.
It allows you to use your controller as a class or prototype, where you expose class/prototype methods and properties to your templates rather than your controller adding methods and properties to the scope object.
So with ES2015 or ES5 you could do either:
export class SomeController {
someProperty = true;
someMethod() {
return 'foo';
}
}
Or
function SomeController() {}
SomeController.prototype.someProperty = true;
SomeController.prototype.someMethod = function() { return 'foo'; }
Now if you provide one of these to your template as SomeController as ctrl you will then be able to access these as ctrl.someProperty and ctrl.someMethod(). The ctrl instance of your controller is added to the $scope for you.
Another benefit is memory footprint. Monkey patching functions onto $scope is wasteful. Class and prototypes allow the same method implementation to be shared, while keeping each instance separate. This adds up for components with many instances such as a list-item, for example.

Is there a way for directives to inherit behavior from another directive within an AngularJs application?

I need to create a dashboard like page with Angular and as soon as I started to code the widgets from the main page, it was obvious I was repeating code between them.
Basically, all widgets have some "data", a template for presenting that data and a "loading" property (true while request is ongoing to display an animated icon inside its container).
However, templates itself are different amongst widgets. Since templates, are different, data handling and (possibly) manipulation is also different.
Ok, so, this means different controllers per widget. But can I share the basic behavior between them? Like executing a request, setting "loading" to "true", handle/manipulate response, set loading to false, present template.
How scenarios like that work with Angular 1.x?
I appreciate any help figuring out the topics I need to look into for that to work.
Thanks for helping.
The way I do it is to have a base controller that is vm that contains all the base functionality, and then there will be a child View that is the directive and usually does not have an isolate scope, so it can see vm and use its functionality (but you can also pass it in as part of the directive definition).
If you imagine it without directives for a second, it might look something like this:
<div ng-controller="parent as vm">
&ltdiv ng-controller="child">...&lt/div>
</div>
And the controllers might look like:
app.module('someModule').controller('parent', ['dataService', parent])
.controller('child', ['$scope', child]);
function parent(dataService) {
var vm = this;
var dataCollection = dataService.collection;
vm.findTheData = function(value) {
return dataCollection.indexOf(value);
}
}
function child($scope) {
$scope.dataIndex = -1;
$scope.onButtonClicked = function(value) {
if ($scope.vm) $scope.dataIndex = vm.findTheData(value);
}
}

Is access to a service inherited by child controllers?

If a parent controller has a service injected, do the controller's children inherit the service implicitly or do they have to have the service injected explicitly?
You need to inject the service explicitly. The child controller inherits the scope, but of course the service is not on the scope object (unless you put it there)
see this jsfiddle to prove it: http://jsfiddle.net/HB7LU/11596/
var myApp = angular.module('myApp',[]);
myApp.factory('serviceId', function() {
var a = 1;
return {
a: a
};
});
myApp.controller('Ctrl',['$scope', 'serviceId' ,function($scope, serviceId) {
$scope.name = 'Superhero' + serviceId.a;
}])
myApp.controller('ChildCtrl',['$scope',function($scope) {
// the next line throws an undefined error because of serviceId
$scope.name = 'Superhero' + serviceId.a;
}])
If you do prototypical inheritance then you can have a field from the parent controller as something that can be used within the derived controller. In order to make angular js controllers inheritable you have to do a few things. Hopefully the following explanation will be more clear.
Prototypical inheritance in JavaScript
So to start off with lets define how we are going to prototypically inherit from an object. In javascript prototypical inheritance can be done as follows:
function Foo(some_param)
{
this.some_param = some_param
}
function Bar(some_param, some_specialized_param)
{
/**
* Call the super class constructor
*/
Foo.call(this, some_param);
this.some_specialized_param = some_specialized_param;
}
/**
* Now that you have defined the constructor, you need to
* define the prototypical inheritance
*/
Bar.prototype = Object.create(Foo.prototype);
/**
* Now you have to set the constructor
*/
Bar.prototype.constructor = Bar;
So using this example you can create angularjs controllers. There is one problem however. If for example The class Bar is in a different module or directory, you need to import the definition. You could use some other tool like RequireJS to load the module/controller/class definition in the file where Bar is defined and then inherit as follows. You could however also use angularjs constructs as follows:
Controller inheritance in AngularJS
So you could use two angularjs constructs to define a controller's instantiation point (angular.module(..).constructor) and the definition of a controller (angular.module(..).factory). AngularJS factories can be used for controller's class definition.
angular.module('some_module')
.factory('BaseCtrlClass', ['aService', function(aService)
{
function BaseCtrl($scope)
{
/**
* Assign a reference to "aService"
* which is injected whithin this factory
*/
this.aService = aService;
this.$scope = $scope;
}
return BaseCtrl;
}]);
Now you can define a controller definition so that you can instantiate it using ngController like:
angular.module('some_module')
.controller('BaseCtrl', [$scope, 'BaseCtrlClass', function($scope, BaseCtrlClass)
{
return new BaseCtrlClass($scope);
}]);
Notice that in the factory I have defined it as BaseCtrlClass because that is the class definition. In the controller construct I have defined BaseCtrl which is something that you can use externally as mentioned using ngController. The BaseCtrlClass name should be used internally in derived controllers as we will see below. So as I mentioned the problem comes when you are trying to import the definition of BaseCtrlClass in another file/module ideally a file defining a DerivedCtrl class. Lets define the DerivedCtrl Class as follows:
angular.module('a_different_module')
.factory('DerivedCtrlClass', ['BaseCtrlClass', function(BaseCtrlClass)
{
function DerivedCtrl($scope, aDifferentService)
{
BaseCtrlClass.call(this, $scope);
this.aDifferentService = aDifferentService //Specialized resource
}
DerivedCtrl.prototype = Object.create(BaseCtrlClass.prototype);
DerivedCtrl.prototype.constructor = DerivedCtrl;
return DerivedCtrl;
}])
.controller('DerivedCtrl', ['$scope', 'aDiferentService', 'DerivedCtrlClass', function($scope, aDifferentService, DerivedCtrlClass)
{
return new DerivedCtrlClass($scope, aDifferentService);
}]);
So here you can see that I have defined DerivedCtrl in a factory called DerivedCtrlClass. This essentially defines the controller class definition. So any other module or class trying to inherit from this can utilise this factory. In the constructor I have called the BaseCtrlClass's constructor passing "this" and "$scope" as arguments as it requires. Later on I defined the prototype constructions and default constructor similar to the javascript example I mentioned earlier. For convenience as well I have defined a controller construct which can be used with ngController as well.
The main point that I want to highlight is that an instance of DerivedCtrl can now use
this.aService
which is something that is defined in BaseCtrl. Basically anything that is defined within the scope of BaseCtrl and DerivedCtrl can now be used within an instance of DerivedCtrl. This works quite well in html when you are trying to define a DerivedCtrl you can just do
<div ng-controller="DerivedCtrl">
......
</div>
Scoped inheritance
If you want to however have scoped inheritance you can use something like this:
<div ng-controller="BaseCtrl">
<div ng-controller="DerivedCtrl">
...
</div>
</div>
The problem with this is that you need to communicate fields using $scope. In the constructor for BaseCtrl you need to do the following:
$scope.aService = aService
And then from DerivedCtrl you can use $scope.aService. This doesn't work that well because it breaks encapsulation. For maintainability you might want to not export this service and make it pseudo protected only to be accessed by classes that inherit from BaseCtrl. In that case the solution I provided works great, at least I have been using that way.
Hope this helps.

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