I'm having a problem which I'm not sure whether is a down to a limitation of Angular (possibly) or a limitation of my knowledge of Angular (probably).
I am trying to take an array of controllers, and dynamically create/load them. I have a prototype working to the point where the controllers run and the root scope can be accessed, but I cannot dynamically attach ng-controller to divs in order to encapsulate the controllers into their own local scopes.
The problem is that the templates are bound to the root scope but not to their own scopes.
My example will hopefully explain my quandary better.
JSFiddle: http://jsfiddle.net/PT5BG/22/ (last update 16:30 BST)
It may not make sense why I am doing it this way, but I have pulled this concept out of a larger system I am creating. In case you have other suggestions, these are the laws by which I am bound:
Controllers cannot be hard-coded, they must be built from an array
Scopes cannot be shared between controllers, they must have their own scopes
The docs on AngularJS are not exactly comprehensive so I'm hoping someone here can help!
You can just pass the controller name through and use the $controller service and pass the locals through to it. You'll need some sort of ModuleCtrl thing to co-ordinate all this. Here is a basic example that does what you want.
http://jsfiddle.net/PT5BG/62/
angular.module('app', [])
.controller('AppCtrl', function ($scope, $controller) {
$scope.modules = [
{ name: "Foo", controller: "FooCtrl" },
{ name: "Bar", controller: "BarCtrl" }]
})
.controller('ModuleCtrl', function ($scope, $rootScope, $controller) {
$controller($scope.module.controller, { $rootScope: $rootScope, $scope: $scope });
})
.controller('FooCtrl', function ($rootScope, $scope) {
$rootScope.rootMessage = "I am foo";
$scope.localMessage = "I am foo";
console.log("Foo here");
})
.controller('BarCtrl', function ($rootScope, $scope) {
$rootScope.rootMessage = "I am bar";
$scope.localMessage = "I am bar";
console.log("Bar here");
});
The way I finally got around this was quite simple, it was just a case of working it out.
So I have a list of modules, that I get from an API, and I want to instantiate them. I include the template file by building the path via convention, like so:
<!-- the ng-repeat part of the code -->
<div ng-repeat="module in modules">
<ng-include src="module.name + '.tpl.html'"></ng-include>
</div>
In each of the modules template files, I then declare the ng-controller and I declare a method to fire in ng-init. As the template is still within the ng-repeat loop, it has access to module, which has the data we want to pass to the child controller. ng-init runs on the local scope, so we pass in the module object:
<!-- the template of the module -->
<div ng-controller="ModuleCtrl" ng-init="init(module)">
...
</div>
And then we store it on the local scope and there you go, injected the object.
/* the controller of the module */
.controller('ModuleCtrl', function ($scope) {
$scope.init = function(module) {
this.module = module;
};
// this.module is now available inside the controller
});
It took a bit of hacking but it works perfectly for now.
Related
I have two directives: A and B. They're very similar. I want directive B to inherit the controller in directive A.
In other words, the same function used for controller: in A's directive definition object needs to also be the controller: function used in B's directive definition object.
Aside from a copy/paste of the controller: function, how I use the same function in both A and B's definition?
Controllers are just regular JS functions, so, you can use prototyping:
function BaseController(){
this.commonFunct = function(){
...
}
}
function CtrlA(){
}
CtrlA.prototype = BaseController
function CtrlB(){
}
CtrlB.prototype = BaseController
this works with controllerAs syntax, when your controller is exposed to scope under some name, say ctrl. Then $scope.ctrl.commonFunct (more generic, works from any place of controller) or this.commonFunct (can be used in controller's instance methods, where this is controller itself) can be used to refer the function.
That works if you declare both controllers in one module as named functions. If they are declared in different modules, you can use mixin-like way with $controller:
// Base module
(function() {
'use strict';
angular.module('Base', []);
function BaseController($scope, <injectables>, that) {
that.commonFunct = function() {
};
}
angular.module('Base').controller('BaseController',
['$scope', '...', BaseController]);
})();
// Module that inherits functionality
(function() {
'use strict';
angular.module('Derived', ['Base']);
function DerivedController($scope, $controller, ...) {
$controller('BaseController', {
'$scope' : $scope,
...
'that' : this
});
// this.commonFunct is available
}
angular.module('Derived').controller('DerivedController',
['$scope', '$controller', '...', DerivedController]);
})();
MHO: I suggest to use named functions for declaring controllers / services and directives as it is more natural, JS way of doing things. Also, I like controllerAs syntax much as it helps to distinguish data, stored directly in scope (like $scope.data) with controller's methods (they all are stored in one scope's named object, like $scope.ctrl).
If I understand correct, you don't really want to inherit controllers. You want to use one controller in 2 different directives.
If that's the case, just declare the controller function, and pass it to both directive definition objects as a function or a string.
I am migrating an AngularJS multiple-page app to a single-page app, and I am having some trouble to replicate the following behaviour:
Each HTML file has a different base controller and a ng-view. For example, file1.html looks like this:
<body ng-controller="BaseCtrl1">
<!-- Routes to /view11, /view12, etc. with their corresponding controllers -->
<div ng-view></div>
</body>
<script src="file1.js"></script>
file2.html uses BaseCtrl2, routing to /views21, /view22 and so on. Each of this controllers initialize the scope, and the corresponding subset of views share this part of the model:
file1.js:
module.controller('BaseCtrl1', function($scope, ServiceA, ServiceB, ServiceC) {
// Populate $scope with ServiceN.get() calls
ServiceA.get(function(response) {
$scope.foo = response.results;
});
});
// ...
file2.js:
module.controller('BaseCtrl2', function($scope, ServiceX, ServiceY) {
// Populate $scope with ServiceN.get() calls
});
// ...
However, with a single-page app I cannot use a fixed parent controller (declared in the body element) for each different group of views. I have tried using the $controller service like in the answer of this question, but I need to inject all the dependencies of the parent in the child controller, and does not look like a neat solution at all:
module.controller('View11Ctrl', function($scope, ServiceA, ServiceB, ServiceC) {
$controller('BaseCtrl1', {/* Pass al the dependencies */});
});
module.controller('View12Ctrl', function($scope, ServiceA, ServiceB, ServiceC) {
$controller('BaseCtrl1', {/* Pass al the dependencies */});
});
I would like to know if there is a way to replicate the original behaviour by initializing a "common" part of the scope of a group of views, and maintain it when changing the route.
You can use $injector.invoke() service method to achieve this.
module.controller('View11Ctrl', function($scope, ServiceA, ServiceB, ServiceC) {
$injector.invoke(BaseCtrl1, this, { $scope: $scope });
}
The third argument is defined as:
If preset then any argument names are read from this object first,
before the $injector is consulted.
This way you only need to pass the locals to your base controller that is specific to your child controller, and all other base controller dependencies will be resolved using the normal $injector DI.
I have this piece of layout html:
<body ng-controller="MainController">
<div id="terminal"></div>
<div ng-view></div>
<!-- including scripts -->
</body>
Now apparently, when I try to use $routeParams in MainController, it's always empty. It's important to note that MainController is supposed to be in effect in every possible route; therefore I'm not defining it in my app.js. I mean, I'm not defining it here:
$routeProvider.when("/view1", {
templateUrl: "partials/partial1.html"
controller: "MyCtrl1"
})
$routeProvider.when("/view2", {
templateUrl: "partials/partial2.html"
controller: "MyCtrl2"
})
// I'm not defining MainController here!!
In fact, I think my problem is perfectly the same as this one: https://groups.google.com/forum/#!topic/angular/ib2wHQozeNE
However, I still don't get how to get route parameters in my main controller...
EDIT:
What I meant was that I'm not associating my MainController with any specific route. It's defined; and it's the parent controller of all other controllers. What I'm trying to know is that when you go to a URL like /whatever, which is matched by a route like /:whatever, why is it that only the sub-controller is able to access the route parameter, whereas the main controller is not? How do I get the :whatever route parameter in my main controller?
The $routeParams service is populated asynchronously. This means it will typically appear empty when first used in a controller.
To be notified when $routeParams has been populated, subscribe to the $routeChangeSuccess event on the $scope. (If you're in a component that doesn't have access to a child $scope, e.g., a service or a factory, you can inject and use $rootScope instead.)
module.controller('FooCtrl', function($scope, $routeParams) {
$scope.$on('$routeChangeSuccess', function() {
// $routeParams should be populated here
});
);
Controllers used by a route, or within a template included by a route, will have immediate access to the fully-populated $routeParams because ng-view waits for the $routeChangeSuccess event before continuing. (It has to wait, since it needs the route information in order to decide which template/controller to even load.)
If you know your controller will be used inside of ng-view, you won't need to wait for the routing event. If you know your controller will not, you will. If you're not sure, you'll have to explicitly allow for both possibilities. Subscribing to $routeChangeSuccess will not be enough; you will only see the event if $routeParams wasn't already populated:
module.controller('FooCtrl', function($scope, $routeParams) {
// $routeParams will already be populated
// here if this controller is used within ng-view
$scope.$on('$routeChangeSuccess', function() {
// $routeParams will be populated here if
// this controller is used outside ng-view
});
);
As an alternate to the $timeout that plong0 mentioned...
You can also inject the $route service which will show your params immediately.
angular.module('MyModule')
.controller('MainCtrl', function ($scope, $route) {
console.log('routeParams:'+JSON.stringify($route.current.params));
});
I have the same problem.
What I discovered is that, $routeParams take some time to load in the Main Controller, it probably initiate the Main Controller first and then set $routeParams at the Child Controller. I did a workaround for it creating a method in the Main Controller $scope and pass $routeParams through it in the Child Controllers:
angular.module('MyModule')
.controller('MainController', ["$scope", function ($scope) {
$scope.parentMethod = function($routeParams) {
//do stuff
}
}]);
angular.module('MyModule')
.controller('MyCtrl1', ["$scope", function ($scope) {
$scope.parentMethod($routeParams);
}]);
angular.module('MyModule')
.controller('MyCtrl2', ["$scope", function ($scope) {
$scope.parentMethod($routeParams);
}]);
had the same problem, and building off what Andre mentioned in his answer about $routeParams taking a moment to load in the main controller, I just put it in a timeout inside my MainCtrl.
angular.module('MyModule')
.controller('MainCtrl', function ($scope, $routeParams, $timeout) {
$timeout(function(){
// do stuff with $routeParams
console.log('routeParams:'+JSON.stringify($routeParams));
}, 20);
});
20ms delay to use $routeParams is not even noticeable, and less than that seems to have inconsistent results.
More specifically about my problem, I was confused because I had the exact same setup working with a different project structure (yo cg-angular) and when I rebuilt my project (yo angular-fullstack) I started experiencing the problem.
You have at least two problems here:
with $routeParams you get the route parameters, which you didn't define
the file where you define a main controller doesn't really matter. the important thing is in which module/function
The parameters have to be defined with the $routeProvider with the syntax :paramName:
$routeProvider.when("/view2/name1/:a/name2/:b"
and then you can retrieve them with $routeParams.paramName.
You can also use the query parameters, like index.html?k1=v1&k2=v2.
app.js is the file where you'd normally define dependencies and configuration (that's why you'd have there the app module .config block) and it contains the application module:
var myapp = angular.module(...);
This module can have other modules as dependencies, like directives or services, or a module per feature.
A simple approach is to have a module to encapsulate controllers. An approach closer to your original code is putting at least one controller in the main module:
myapp.controller('MainCtrl', function ($scope) {...}
Maybe you defined the controller as a global function? function MainCtrl() {...}? This pollutes the global namespace. avoid it.
Defining your controller in the main module will not make it "to take effect in all routes". This has to be defined with $routeProvider or make the controller of each route "inherit" from the main controller. This way, the controller of each route is instantiated after the route has changed, whereas the main controller is instantiated only once, when the line ng-controller="MainCtrl" is reached (which happens only once, during application startup)
You can simply pass values of $routeParams defined into your controller into the $rootScope
.controller('MainCtrl', function ($scope, $routeParams, MainFactory, $rootScope) {
$scope.contents = MainFactory.getThing($routeParams.id);
$rootScope.total = MainFactory.getMax(); // Send total to the rootScope
}
and inject $rootScope in your IndexCtrl (related to the index.html)
.controller('IndexCtrl', function($scope, $rootScope){
// Some code
});
What is correct way of passing variables from web page when initializing $scope?
I currently know 2 possibilities:
ng-init, which looks awful and not recommended (?)
using AJAX request for resource, which requires additional request to server which I do not want.
Is there any other way?
If those variables are able to be injected through ng-init, I'm assuming you have them declared in Javascript.
So you should create a service (constant) to share these variables:
var variablesFromWebPage = ...;
app.constant('initValues', variablesFromWebPage);
With this service, you don't need to add them to the scope in the app start, you can use it from any controller you have, just by injecting it (function MyCtrl(initValues) {}).
Althouhg, if you do require it to be in the scope, then this is one of the main reasons what controllers are meant for, as per the docs:
Use controllers to:
Set up the initial state of a scope object.
Add behavior to the scope object.
Just add this cotroller to your root node:
app.controller('InitCtrl', function($rootScope, initValues) {
$rootScope.variable1 = initValue.someVariable;
$rootScope.variable2 = initValue.anotherVariable;
});
#Cunha: Tnx.
Here some more details how I did it:
In the Webpage:
<script type="text/javascript">
var variablesFromWebPage = {};
variablesFromWebPage.phoneNumber = '#Model.PhoneNumber';
</script>
In the angular application file, registering the service:
var module= angular.module('mymodule', ['ngRoute', 'ngAnimate']);
module.factory('variablesFromWebPage', function () {
return variablesFromWebPage
});
And then the controller:
module.controller('IndexController',
function ($scope, $location, $http, $interval, variablesFromWebPage) {
$scope.phoneNumber = variablesFromWebPage.phoneNumber;
}
);
The seed app uses routes, which reference controllers, and the controllers are defined like this:
function MyCtrl1() {}
MyCtrl1.$inject = [];
Looking for a better example (ie) showing injection, maybe an HTTP get and update to scope?
Thanks.
I'm not sure exactly what you want, but here is a more complex example.
Controller:
function MyCtrl1 ( $scope, $http ) {
$http.get( '/some/location' ).success( function ( data ) {
$scope.items = data;
});
}
MyCtrl1.$inject = [ '$http' ];
View:
<div ng-controller="MyCtrl1">
<ul>
<li ng-repeat="item in items">{{item.name}}</li>
</ul>
</div>
Commentary:
In a real world scenario, the $http calls would be in your own service that would be injected into the controller instead. Also, I recommend against defining controllers in the global space. A better way to define the same controller would be like this:
angular.module('myApp', [])
.controller( 'MyCtrl', [ '$http', function MyCtrl1 ( $scope, $http ) {
$http.get( '/some/location' ).success( function ( data ) {
$scope.items = data;
});
}]);
Update:
Controllers are useless without a scope - the really couldn't do anything - so Angular automatically injects $scope into every controller. Every other service must be requested to be injected. The MyCtrl1.$inject and the array syntax are both only necessary to still remain functional after minification. If you create a sample file with my code and delete the $inject line, it will still work. But when you minify the Javascript, variables names get reduced, so we put the important information in strings.
I recommend going through the tutorial as well as watching some of the videos on the AngularJS YouTube channel, like this one.