I have defined one controller, and apply it to 2 views with small differences.
Angular code:
app.controller('MyCtrl', function($scope) {
$scope.canSave = false;
$scope.demo = {
files : [{
filename: 'aaa.html',
source: '<div>aaa</div>'
}, {
filename: 'bbb.html',
source: '<div>bbb</div>'
}]
}
$scope.newFile = function(file) {
$scope.demo.files.push(file);
}
$scope.$watch("demo.files", function(val) {
$scope.canSave = true;
}, true);
});
View 1:
<div ng-controller="MyCtrl"></div>
View 2:
<div ng-controller="MyCtrl"></div>
The sample code is very simple, but there are a lot of code and logic in my real project.
The View 1 and 2 have almost the same features, only with a few differences, but I do need to write some code for each of them in the controller.
I don't want to create 2 different controllers for them, because they have most of same logic. I don't want to move the logic to a service to share it between the 2 controllers, because the logic is not that common to be a service.
Is there any other way to do it?
Under the given conditions I might be doing something like
function MyCommonCtrl(type){
return function($scope, $http) {
$scope.x = 5;
if(type = 't1'){
$scope.domore = function(){
}
}
....
....
}
}
angular.module('ng').controller('Type1Ctrl', ['$scope', '$http', MyCommonCtrl('t1')]);
angular.module('ng').controller('Type2Ctrl', ['$scope', '$http', MyCommonCtrl('t2')]);
Then
<div ng-controller="Type1Ctrl"></div>
and
<div ng-controller="Type2Ctrl"></div>
I don't know your specific set-up but your 2 controllers could inherit from a common ancestor.
Type1Ctrl.prototype = new MyCtrl();
Type1Ctrl.prototype.constructor = Type1Ctrl;
function Type1Ctrl() {
// constructor stuff goes here
}
Type1Ctrl.prototype.setScope = function() {
// setScope
};
Type2Ctrl.prototype = new MyCtrl();
Type2Ctrl.prototype.constructor = Type2Ctrl;
function Type2Ctrl() {
// constructor stuff goes here
}
Type2Ctrl.prototype.setScope = function() {
// setScope
};
I also faced similar problem and scope inheritance solved my problem.
I wanted to "reuse" a controller to inherit common state/model ($scope) and functionality (controller functions attached to $scope)
As described in the "Scope Inheritance Example" I attach parent controller to an outer DOM element and child controller to the inner. Scope and functions of parent controller "merge" seamlessly into the child one.
Here is another option. Slightly modified from this blog post
app.factory('ParentCtrl',function(){
$scope.parentVar = 'I am from the parent'
};
});
app.controller('ChildCtrl', function($scope, $injector, ParentCtrl) {
$injector.invoke(ParentCtrl, this, {$scope: $scope});
});
here is a plunker
Related
I don't really understand how to inherit controller objects in angular (not using $scope), when we're surrounding them in a IIFE.
So assume we have 2 controller files:
Animal.js
(function() {
'use strict';
angular.module('myApp').controller('animalCtrl', function() {
this.strength = 3;
this.speed = 3;
});
})();
Beaver.js
(function() {
'use strict';
angular.module('myApp').controller('beaverCtrl', function() {
//How do I inherit the parent controllers stats?
});
})();
How can I inherit from animalCtrl using my beaverCtrl?
The best way to share data on any angularjs site is by using services. In this case I would create a factory that will store strength and speed and share it with beaverCtrl.
angular.module('<yourappname>', [])
.factory('dataService', function() {
$scope = this;
$scope.strength = null;
$scope.speed = null;
$scope.setStrength = function(value) {
$scope.strength = value;
}
$scope.setSpeed = function(value) {
$scope.speed = value;
}
return $scope;
})
Then in the controllers you would call the service like this
.controller('animalCtrl', function(dataService) {
this.strength = 3;
this.speed = 3;
dataService.setStrength(this.strength);
dataService.setSpeed(this.speed);
});
.controller('beaverCtrl', function(dataService) {
this.strength = dataService.strength;
this.speed = dataService.speed;
});
It's important to realize that an AngularJS controller is just a normal javascript "class". The only catch is invoking the base constructor with dependency injection.
// Create some base class.
BaseController = function($scope, $route) {
// Do some stuff here.
};
// The base class has some behavior.
BaseController.prototype.someMethod = function() { /* do something */ };
// Create some child class.
ChildController = function($scope, $injector) {
// Invoke the base constructor.
$injector.invoke(this, BaseController, {$scope: $scope});
};
// Inherit from the base class.
ChildController.prototype = Object.create(BaseController.prototype);
// Add more specific behavior.
ChildController.prototype.someChildMethod = function() { /* do something */ };
Then you can register your controller as
angular.module('myApp').controller('ChildController', ChildController);
Keep in mind that using inheritance like this will be problematic if overused. It should only be used for sharing behavior. Additionally, using composition of different "classes" is generally going to be much more flexible than using inheritance.
From a code organization standpoint, I would also recommend that you declare your controllers in one place (one file per controller) and register them onto modules in another place (one file per module).
Is it possible to instantiate a controller in AngularJS and pass arguments to its constructor like in OOP ? I can't figure out how to refactor 3 identical controller with just variables name and content which change...
Thanx.
If you have 3 separate sections on the page that have very similar controller code, it sounds like you should consider using a directive. Even if you don't need to control the DOM directly (which is the classic reason to use directive), and only need the standard Angular data-bindings, then this is a nice way to reuse controllers in different contexts by the attributes set on the directive.
You can see a working plunkr at
http://plnkr.co/edit/qclp6MOxGWP7Ughod4T8?p=preview
But the key point is directives can bind-to variables in their parent scope's controller. Say, in the parent scope you have 3 variables, so:
$scope.myVariable1 = 'Value 1';
$scope.myVariable2 = 'Value 2';
$scope.myVariable3 = 'Value 3';
Then you can setup 3 instances of the directive in the template:
<my-directive my-param="myVariable1"></my-directive>
<my-directive my-param="myVariable2"></my-directive>
<my-directive my-param="myVariable3"></my-directive>
Then each directive can use the variable in the 'my-param' attribute
scope: {
'myParam':'='
}
The '=' means that in the scope of the directive you have a variable, called 'myParam', that is equal (+ bound to) the variable specified by the 'my-param' attribute on the directive. So on the template of the directive, you can use:
<div>Value of parameter: {{myParam}}</div>
And in the controller of the directive, you have access to is as:
$scope.myParam
And should then be able to customise its behaviour based on that instance's myParam.
You can create services with a common interface and then pass the corresponding services to each controller through dependency injection. In my code this is the case of table controllers where the only thing that changes is the data source:
Imagine you have some code that looks like this
angular.module('myapp').controller('TableACtrl', ['$scope', function($scope) {
$scope.data = // get the data from some source
$scope.someFunction = function () { ... };
$scope.someOtherFunction = function () { ... };
}]);
angular.module('myapp').controller('TableBCtrl', ['$scope', function($scope) {
$scope.data = // get the data from some other source
$scope.someFunction = function () { ... };
$scope.someOtherFunction = function () { ... };
}]);
It looks something like:
var tableCtrl = function($scope, dataSource) {
$scope.data = dataSource.getData();
$scope.someFunction = function () { ... };
$scope.someOtherFunction = function () { ... };
};
angular.module('myapp')
.controller('TableACtrl', ['$scope', 'dataSourceA', tableCtrl])
.controller('TableBCtrl', ['$scope', 'dataSourceB', tableCtrl])
angular.module('myapp')
.service('dataSourceA', function() {
return {
getData: function() { ... }
};
});
angular.module('myapp')
.service('dataSourceB', function() {
return {
getData: function() { ... }
};
});
I would still put things within a module, so you don't pollute the global window. But that's a different issue.
I've created an angular app that has the following structure.
Application configuration, routes, directives, controllers and filters are all defined in index.js (I know this is not recommended). All of my general functions are in a controller called main.js, this is also the controller I am using in my main view in index.html. From then on the app consists of 10 different views, each has it's own controller.
main.js has become very difficult to maintain, so I would like to separate it into five external "utility" style files that contain the general function the application uses. These functions all use angular's $scope and must be able to be accessed by all the views and controllers that exist in the application.
For the past few days I've tried several different methods, such as defining the functions under angular's factory service, using angular's $provide method, defining a controller without a view and many others. None of them worked for me. What is the simplest way to separate the functions that exist in main.js to external js files without changing any code within the functions themselves. Let's pretend that the function cannot be turned into a directive.
Example -
Function that checks users name for 'guest' string and returns an image
main.js -
$scope.defaultpic = function(username) {
var guest = username;
if (guest.indexOf("guest") != -1){
{return {"background-image": "url('data:image/png;base64,chars"}}
}
}
in the view
<img ng-style="defaultpic(JSON.Value)" class="user_pic" ng-src="getprofilepic/{{JSON.Value}}"/>
Cheers,
Gidon
In order to use the function in markup, you still have to bind it to the scope. But, you can move the body of the function to a service:
angular.module('myapp').factory('picService',[ function () {
return {
defaultpic: function(username) {
var guest = username;
if (guest.indexOf("guest") != -1){
{return {"background-image": "url('data:image/png;base64,chars"}}
}
}
};
}]);
And then bind it up in the controller:
$scope.defaultpic = picService.defaultpic;
Refactor controller functions as services declared in different files
As you correctly stated, a great approach to refactor the functions is to put them into different services.
According to the angular Service docs:
Angular services are singletons objects or functions that carry out specific tasks common to web apps.
Here is an example:
Original code
Here we have a simple Hello World app, with a controller that has two functions: greet() and getName().
app.js
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.getName = function () {
return 'World';
}
$scope.greet = function (name) {
return 'Hello ' + name;
};
});
index.html
...
<div id="container" ng-controller="MainCtrl">
<h1>{{greet(getName())}}</h1>
</div>
...
We want to test that our scope always has both functions, so we know it is working as intended, so we are going to write two simple jasmine tests:
appSpec.js
describe('Testing a Hello World controller', function() {
var $scope = null;
var ctrl = null;
//you need to indicate your module in a test
beforeEach(module('plunker'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {
$scope: $scope
});
}));
it('should say hallo to the World', function() {
expect($scope.getName()).toEqual('World');
});
it('shuld greet the correct person', function () {
expect($scope.greet('Jon Snow')).toEqual('Hello Jon Snow');
})
});
Check it out in plnkr
Step 1: Refactor controller functions into separate functions
In order to start decoupling our controller to our functions we are going to make two individual functions inside app.js.
app.js
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.getName = getName;
$scope.greet = greet;
});
function getName() {
return 'World';
}
function greet(name) {
return 'Hello ' + name;
}
Now we check our test output and we see that everything is working perfectly.
Check out the plnkr for step 1
Step 2: Move functions to their own services
We will define a NameService and GreetService, put our functions in them and then define the services as dependencies in our controller.
app.js
var app = angular.module('plunker', []);
app.service('NameService', function () {
this.getName = function getName() {
return 'World';
};
});
app.service('GreetService', function() {
this.greet = function greet(name) {
return 'Hello ' + name;
}
});
app.controller('MainCtrl', ['$scope', 'NameService', 'GreetService', function($scope, NameService, GreetService) {
$scope.getName = NameService.getName;
$scope.greet = GreetService.greet;
}]);
We make sure that our tests are still green, so we can move on to the final step.
Have a look at step 2 in plunker
Final Step: Put our services in different files
Finally we will make two files, NameService.js and GreetService.js and put our services in them.
NameService.js
angular.module('plunker').service('NameService', function () {
this.getName = function getName() {
return 'World';
};
});
GreetService.js
angular.module('plunker').service('GreetService', function() {
this.greet = function greet(name) {
return 'Hello ' + name;
}
});
We also need to make sure to add the new scripts to our index.html
index.html
...
<script src="NameService.js"></script>
<script src="GreetService.js"></script>
...
This is how our controller looks like now, neat huh?
app.js
var app = angular.module('plunker', []);
app.controller('MainCtrl', ['$scope', 'NameService', 'GreetService', function($scope, NameService, GreetService) {
$scope.getName = NameService.getName;
$scope.greet = GreetService.greet;
}]);
Plunker for the final step.
And that's it! Our tests still pass, so we know everything works like a charm.
I'm working on an app the uses multiple (but similar) controllers (controllerA) on a page and another controller (controllerB) that could change some properties, but ONLY if the element of controllerA was previously selected.
(in controllerA some properties could have default values, that are passed and applied by using data- attributes)
In this stage, I can only modify the last color property with controllerB (which makes sense, because that's the active scope. My question is, how can I change controllerB scope to be the "active" scope-copy of the selected controllerA?
// Code goes here
var webApp = angular.module("webApp", []);
webApp.controller("controllerA", function($scope, $rootScope, $element, styleFactory){
$scope.selected = false;
var color = angular.element($element[0]).attr("data-style-color");
styleFactory.setColor(color);
$scope.data = styleFactory.getData();
$scope.select = function(){
$scope.selected = !$scope.selected;
}
});
webApp.controller("controllerB", function($scope, $rootScope, $element, styleFactory){
$scope.data = styleFactory.getData();
});
webApp.factory("styleFactory", function(){
var data = {"style":"color:yellow"}
return {
setColor: function(color){
data = {"style":"color:"+color};
},
getData: function(){
return data;
}
};
});
for full example:
http://plnkr.co/edit/He2m7ArfRSur9Igq2eur?p=preview
thanks,
--iM
You can see a modified plnkr that works as you described here.
Personally, I would restructure the architecture of the app and rethink the styleFactory implementation.
Some points for thought:
data = {"style":"color:"+color} replaces the whole data object, thus $scope.data = styleFactory.getData() works only for the last controllerA as the reference is lost for all others.
Try creating a parent controller that will be on top of controllerA and controllerB.
Try solving this problem using directives.
I recently chose AngularJS over ember.js for a project I am working on, and have been very pleased with it so far. One nice thing about ember is its built in support for "computed properties" with automatic data binding. I have been able to accomplish something similar in Angular with the code below, but am not sure if it is the best way to do so.
// Controller
angular.module('mathSkills.controller', [])
.controller('nav', ['navigation', '$scope', function (navigation, $scope) {
// "Computed Property"
$scope.$watch(navigation.getCurrentPageNumber, function(newVal, oldVal, scope) {
scope.currentPageNumber = newVal;
});
$scope.totalPages = navigation.getTotalPages();
}]);
// 'navigation' service
angular.module('mathSkills.services', [])
.factory('navigation', function() {
var currentPage = 0,
pages = [];
return {
getCurrentPageNumber: function() {
return currentPage + 1;
},
getTotalPages: function() {
return pages.length;
}
};
});
// HTML template
<div id=problemPager ng-controller=nav>
Problem {{currentPageNumber}} of {{totalPages}}
</div>
I would like for the UI to update whenever the currentPage of the navigation service changes, which the above code accomplishes.
Is this the best way to solve this problem in AngularJS? Are there (significant) performance implications for using $watch() like this? Would something like this be better accomplished using custom events and $emit() or $broadcast()?
While your self-answer works, it doesn't actually implement computed properties. You simply solved the problem by calling a function in your binding to force the binding to be greedy. I'm not 100% sure it'd work in all cases, and the greediness might have unwanted performance characteristics in some situations.
I worked up a solution for a computed properties w/dependencies similar to what EmberJS has:
function ngCreateComputedProperty($scope, computedPropertyName, dependentProperties, f) {
function assignF($scope) {
var computedVal = f($scope);
$scope[computedPropertyName] = computedVal;
};
$scope.$watchCollection(dependentProperties, function(newVal, oldVal, $scope) {
assignF($scope);
});
assignF($scope);
};
// in some controller...
ngCreateComputedProperty($scope, 'aSquared', 'a', function($scope) { return $scope.a * $scope.a } );
ngCreateComputedProperty($scope, 'aPlusB', '[a,b]', function($scope) { return $scope.a + $scope.b } );
See it live: http://jsfiddle.net/apinstein/2kR2c/3/
It's worth noting that $scope.$watchCollection is efficient -- I verified that "assignF()" is called only once even if multiple dependencies are changed simultaneously (same $apply cycle).
"
I think I found the answer. This example can be dramatically simplified to:
// Controller
angular.module('mathSkills.controller', [])
.controller('nav', ['navigation', '$scope', function (navigation, $scope) {
// Property is now just a reference to the service's function.
$scope.currentPageNumber = navigation.getCurrentPageNumber;
$scope.totalPages = navigation.getTotalPages();
}]);
// HTML template
// Notice the first binding is to the result of a function call.
<div id=problemPager ng-controller=nav>
Problem {{currentPageNumber()}} of {{totalPages}}
</div>
Note that with ECMAScript 5 you can now also do something like this:
// Controller
angular.module('mathSkills.controller', [])
.controller('nav', function(navigation, $scope) {
$scope.totalPages = navigation.getTotalPages();
Object.defineProperty($scope, 'currentPageNumber', {
get: function() {
return navigation.getCurrentPageNumber();
}
});
]);
//HTML
<div ng-controller="nav">Problem {{currentPageNumber}} of {{totalPages}}</div>