AngularJS: dynamically assign controller from ng-repeat - javascript

I'm trying to dynamically assign a controller for included template like so:
<section ng-repeat="panel in panels">
<div ng-include="'path/to/file.html'" ng-controller="{{panel}}"></div>
</section>
But Angular complains that {{panel}} is undefined.
I'm guessing that {{panel}} isn't defined yet (because I can echo out {{panel}} inside the template).
I've seen plenty of examples of people setting ng-controller equal to a variable like so: ng-controller="template.ctrlr". But, without creating a duplicate concurrant loop, I can't figure out how to have the value of {{panel}} available when ng-controller needs it.
P.S. I also tried setting ng-controller="{{panel}}" in my template (thinking it must have resolved by then), but no dice.

Your problem is that ng-controller should point to controller itself, not just string with controller's name.
So you might want to define $scope.sidepanels as array with pointers to controllers, something like this, maybe:
$scope.sidepanels = [Alerts, Subscriptions];
Here is the working example on js fiddle http://jsfiddle.net/ADukg/1559/
However, i find very weird all this situation when you might want to set up controllers in ngRepeat.

To dynamically set a controller in a template, it helps to have a reference to the constructor function associated to a controller. The constructor function for a controller is the function you pass in to the controller() method of Angular's module API.
Having this helps because if the string passed to the ngController directive is not the name of a registered controller, then ngController treats the string as an expression to be evaluated on the current scope. This scope expression needs to evaluate to a controller constructor.
For example, say Angular encounters the following in a template:
ng-controller="myController"
If no controller with the name myController is registered, then Angular will look at $scope.myController in the current containing controller. If this key exists in the scope and the corresponding value is a controller constructor, then the controller will be used.
This is mentioned in the ngController documentation in its description of the parameter value: "Name of a globally accessible constructor function or an expression that on the current scope evaluates to a constructor function." Code comments in the Angular source code spell this out in more detail here in src/ng/controller.js.
By default, Angular does not make it easy to access the constructor associated to a controller. This is because when you register a controller using the controller() method of Angular's module API, it hides the constructor you pass it in a private variable. You can see this here in the $ControllerProvider source code. (The controllers variable in this code is a variable private to $ControllerProvider.)
My solution to this issue is to create a generic helper service called registerController for registering controllers. This service exposes both the controller and the controller constructor when registering a controller. This allows the controller to be used both in the normal fashion and dynamically.
Here is code I wrote for a registerController service that does this:
var appServices = angular.module('app.services', []);
// Define a registerController service that creates a new controller
// in the usual way. In addition, the service registers the
// controller's constructor as a service. This allows the controller
// to be set dynamically within a template.
appServices.config(['$controllerProvider', '$injector', '$provide',
function ($controllerProvider, $injector, $provide) {
$provide.factory('registerController',
function registerControllerFactory() {
// Params:
// constructor: controller constructor function, optionally
// in the annotated array form.
return function registerController(name, constructor) {
// Register the controller constructor as a service.
$provide.factory(name + 'Factory', function () {
return constructor;
});
// Register the controller itself.
$controllerProvider.register(name, constructor);
};
});
}]);
Here is an example of using the service to register a controller:
appServices.run(['registerController',
function (registerController) {
registerController('testCtrl', ['$scope',
function testCtrl($scope) {
$scope.foo = 'bar';
}]);
}]);
The code above registers the controller under the name testCtrl, and it also exposes the controller's constructor as a service called testCtrlFactory.
Now you can use the controller in a template either in the usual fashion--
ng-controller="testCtrl"
or dynamically--
ng-controller="templateController"
For the latter to work, you must have the following in your current scope:
$scope.templateController = testCtrlFactory

I believe you're having this problem because you're defining your controllers like this (just like I'm used to do):
app.controller('ControllerX', function() {
// your controller implementation
});
If that's the case, you cannot simply use references to ControllerX because the controller implementation (or 'Class', if you want to call it that) is not on the global scope (instead it is stored on the application $controllerProvider).
I would suggest you to use templates instead of dynamically assign controller references (or even manually create them).
Controllers
var app = angular.module('app', []);
app.controller('Ctrl', function($scope, $controller) {
$scope.panels = [{template: 'panel1.html'}, {template: 'panel2.html'}];
});
app.controller("Panel1Ctrl", function($scope) {
$scope.id = 1;
});
app.controller("Panel2Ctrl", function($scope) {
$scope.id = 2;
});
Templates (mocks)
<!-- panel1.html -->
<script type="text/ng-template" id="panel1.html">
<div ng-controller="Panel1Ctrl">
Content of panel {{id}}
</div>
</script>
<!-- panel2.html -->
<script type="text/ng-template" id="panel2.html">
<div ng-controller="Panel2Ctrl">
Content of panel {{id}}
</div>
</script>
View
<div ng-controller="Ctrl">
<div ng-repeat="panel in panels">
<div ng-include src="panel.template"></div>
</div>
</div>
jsFiddle: http://jsfiddle.net/Xn4H8/

Another way is to not use ng-repeat, but a directive to compile them into existence.
HTML
<mysections></mysections>
Directive
angular.module('app.directives', [])
.directive('mysections', ['$compile', function(compile){
return {
restrict: 'E',
link: function(scope, element, attrs) {
for(var i=0; i<panels.length; i++) {
var template = '<section><div ng-include="path/to/file.html" ng-controller="'+panels[i]+'"></div></section>';
var cTemplate = compile(template)(scope);
element.append(cTemplate);
}
}
}
}]);

Ok I think the simplest solution here is to define the controller explicitly on the template of your file. Let's say u have an array:
$scope.widgets = [
{templateUrl: 'templates/widgets/aWidget.html'},
{templateUrl: 'templates/widgets/bWidget.html'},
];
Then on your html file:
<div ng-repeat="widget in widgets">
<div ng-include="widget.templateUrl"></div>
</div>
And the solution aWidget.html:
<div ng-controller="aWidgetCtrl">
aWidget
</div>
bWidget.html:
<div ng-controller="bWidgetCtrl">
bWidget
</div>
Simple as that! You just define the controller name in your template. Since you define the controllers as bmleite said:
app.controller('ControllerX', function() {
// your controller implementation
});
then this is the best workaround I could come up with. The only issue here is if u have like 50 controllers, u'll have to define them explicitly on each template, but I guess u had to do this anyway since you have an ng-repeat with controller set by hand.

Related

Angular js : resolve dependency route

I have $scope.question which has questions for all the page.
I want to loop through page wise questions. for this I wrote a function questionsCtrl. This function I am calling in config while setting route.
but here I am getting undefined.
Please suggest how to get data for page from $scope.questions.
app.js
(function () {
"use strict";
var app = angular.module("autoQuote",["ui.router","ngResource"]);
app.config(["$stateProvider","$urlRouterProvider", function($stateProvider,$urlRouterProvider){
$urlRouterProvider.otherwise("/");
$stateProvider
.state("step1", {
url : "",
controller: "questionsCtrl",
resolve: {
pageQuestion: $scope.questions
}
})
.state("step2", {
url : "/step2",
controller: "questionsCtrl",
})
}]
);
}());
questionCtrl.js
(function () {
"use strict";
angular
.module("autoQuote")
.controller("questionsCtrl",["$scope","$state","$q",questionsCtrl]);
function questionsCtrl($scope,$state,$q) {
console.log('here in get question for page: '+$state.current.name);
//var deferred = $q.defer();
if($state.current.name == "" || $state.current.name == "step1")
{
$scope.pageQuestion = $scope.RC1_Cars_v2;
}
else if($state.current.name == "step2")
{
$scope.pageQuestion = $scope.RC1_Drivers_v2;
}
//return deferred.promise;
console.log($scope);
}
}());
html
<div class="container-fluid col-md-8">
<div ng-controller="autoQuoteCtrl">
<div ng-repeat="que in pageQuestion">
<!-- pagequestion will be in loop here -->
</div>
</div>
</div>
here is my plunker http://plnkr.co/edit/kLc6DPqzQgLNpUBaLtZi?p=preview for complete code.
The controller for a view contains view specific logic. If you add a template to your states via template or templateUrl you will have access to your controller's scope within that template. As far as I understand your problem you want to provide data to your content before the state is being displayed. If you take a look at the documentation the resolve property is exactly what you are looking for:
You can use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.
However, you are using it incorrectly. You won't have access to $scope in a config block. What you have to do is provide a function to your resolve property as follows:
resolve: {
pageQuestion: function(myService) {
return myService.getData();
}
}
As you can see in the code snippet above, I am using a function which injects a service called myService. The service is responsible for getting your data. In the example I assume that the service has a method getData which returns a promise. The ui-router will now wait for all promises to be resolved before showing the state. Once you have created such function, you can use the name of your resolve property (in this case pageQuestion) to inject your data into your view controller. So your question controller would look like this:
function questionsCtrl(pageQuestion) {
this.pageQuestion = pageQuestion;
}
You simply inject your resolve into your controller and assign it to a view variable.
Notice, that I am assuming the controllerAs syntax for your state, so I can ommit the $scope and directly use this inside the controller. All you have to do is to add a property controllerAs to your state configuration as follows:
.state("step1", {
...
controller: "questionsCtrl",
controllerAs: 'vm'
})
This has the advantage, that you won't run into scope problems (this has something to do with scope inheritance) in the long run. This is a great article on scopes.
Your view should look like this now:
<div class="container-fluid col-md-8">
<div ng-repeat="que in vm.pageQuestion">
<!-- pagequestion will be in loop here -->
</div>
</div>
Here you access your data via vm, which comes from the controllerAs property in your state config. I would also suggest to use the template for your state and remove occurrences of ng-controller. You won't be needing this because you have a view controller declared for your state already. Additionally, ng-controller's make it difficult to propertly manage your state. You end up with a scope soup, and this is something you definietly want to avoid.

How to add a consistent variable or function to every AngularJS $scope?

I made an App (build using AngularJS 1.X) that has a lot of directives, most of which have there own isolated scope. So, for every new $scope i assign Underscore.js + Underscore.string variables _ and s to them, like so:
controller: function($scope){
$scope._ = _;
$scope._s = s;
$scope.foo = 'my-example-here';
}
Then do some cool stuff like this within there isolated scope templates:
<div ng-bind='s.humanize(some_id)'></div> # output: "My example here"
Unfortunately you cannot access global variables in these scopes.
So, ultimately, how to add a consistent variable or function to every AngularJS $scope?
Try this:
app.controller('MyCtrl', function($rootScope) {
$rootScope.derp = "foo!";
});
app.directive('myDir', function($rootScope) {
return {
scope { ... },
controller: function($rootScope) {
console.log($rootScope.derp);
}
};
});
Make sure to put ng-controller="MyCtrl" directive somewhere on the parent DOM element of your custom directive like so:
<div ng-controller="MyCtrl">
<div my-dir></div>
</div>
Try it please.
EDIT
Also, you can use constants as described by #georgeawg in comment.

AngularJS directive not showing up on template

I've got a tiny problem with an angular directive that's now working and I don't know why. I think it's a fairly simple issue that I'm overlooking, maybe you can help me out.
Directive is defined like this:
angular.module('directives', [])
.directive('my-directive', function () {
return {
restrict: 'AE',
scope: {
name: '=name'
},
template: '<h1>{{name}}</h1>'
};
});
Then index.cshtml:
<my-directive name="test"></my-directive>
Application.js:
var app = angular.module('MyApp', [
...,
'directives'
]);
And here's controllers.js
angular.module('controllers', ['apiServices', 'directives'])
.controller('homecontroller', function($scope, $resource, webApiService, $log, $translate, $localStorage, $sessionStorage) {
Ok confirmed that directives.js is loaded, otherwise application.js nags about 'unknown module'. There are no error messages in the console, the thing just doesn't show. Any ideas?
EDIT
So as pointed out, I changed the directive name to camelCase, but still no luck:
<my-directive name="John Doe"></my-directive>
And
.directive('myDirective', function () {
But nothing is showing yet.
EDIT
Problem is that angular expects an object to be passed into the attribute, not a string literal. If you create an object person = { name: 'John' }, pass the person in, then write {{ person.name }} ( assuming we named the attribute person + scope var person too ).
During normalization, Angular converts - delimited name to camelCase.
So use camelCase while specifying the directive inside JS:
.directive('myDirective', function () {
Fiddle
I'm sure you've figured this out already, but if you change your scope definition for name to be
scope: {
name: '#'
}
you will then be able to pass a string. The '#' interpolates the attribute while the '=' binds it. Additionally, you don't need to include an attribute name if it is the same as the scope variable.
The problem appears to be in the directive definition. You note in your question that Angular expects an object; this is true for the "=" scope, but not for the "#" scope. In the "#" scope, Angular expects a string only. I have created a snippet below.
Too many modules
Unless you are reusing the directive in multiple applications, do not create a new module for it. Add the directive definition to the module that you created for the application. In my example below, I called the module back by using "angular.module( moduleName )"... When only one argument is used, Angular returns the existing object rather than creating a new one. This is how we can separate the code into many files.
Things to Note
You will notice the following:
You do not need to load the module into the app variable. Calling the Singleton each time is actually safer and easier on memory management.
The directive is in camel case, as you have already noted.
I am setting the name attribute to a string value and not an object; this works because of the "#" scope setting.
The div is set to ng-app='MyApp'. This usually is set to the html element, but I did not want to mess with the DOM on Stack Exchange. The ng-app directive can be set on any element, and the directives associated with that module will be applied on all elements that are within that element's scope. Without the ng-app directive, Angular does not know which module to run on the page.
//app.js - this defines the module, it uses two parameters to tell the injector what to do.
angular.module('MyApp',[]);
//directive.js stored elsewhere
//this calls back the module that has been created. It uses one parameter because the injector is no longer needed.
angular.module('MyApp').directive('myDirective', function () {
return {
restrict: 'AE',
scope: {
name: '#'
},
template: '<h1>{{name}}</h1>'
};
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="MyApp">
<h1>Successful Load</h1>
<my-directive name="test"></my-directive>
<p>By applying the directive definition to the MyApp module, the MyApp module knows to activate the directive within this scope. In this form, it does not get injected.</p>
</div>
Using Injection
When you have a different module for each and every directive or controller, each one must be injected into the application's module definition. This leaves a lot of room for error. As a best practice, only create a new module when necessary, and make the module a container for a group of related functionality and not a single item.
The code below demonstrates proper injection.
angular.module( "MyApp", ['ReusableDirectives'] );
angular.module( "MyApp" ).directive( "myDirective", function(){
return {
restrict: "AE",
scope: { name: "#" },
template: "<p>This is the directive I defined in the example above. It uses <u>the same module</u> as my main application, because it is not going to be reused over and over again. In fact, I will need it just for this application, so I don't need to complicate things with a new module. This directive takes an attribute called 'name' and if it is a string allows me to manipulate the string within my templates scope to do things like this: {{'hello ' + name + '!'}}</p>"
};
} );
angular.module( "ReusableDirectives", [] );
angular.module( "ReusableDirectives" ).directive("reusableDirective", function(){
return {
restrict: "E",
template: "<p>This is a directive that I intend to use in many, many applications. Because I will reuse it so much, I am putting it in a separate module from my main application, and I will inject this directive. This is the only reason that this directive is not in the same module as the one I defined above.</p>"
};
} ).directive("reusableDirective2", function(){
return {
restrict: "E",
template: "<p>This is a second directive that I intend to use in multiple applications. I have stored it in a module with the first directive so that I can freely inject it into as many apps as I like.</p>"
};
} )
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="MyApp">
<h1>Successful Load</h1>
<my-directive name="Johnny"></my-directive>
<p>By applying the directive definition to the MyApp module, the MyApp module knows to activate the directive within this scope. In this form, it does not get injected.</p>
<h3>Injected Directives</h3>
<reusable-directive></reusable-directive>
<reusable-directive2></reusable-directive2>
</div>
Keep it simple. Define your directives on a single module for your application. Once you have that done and working, if you need the directives again in another application, refactor and experiment with injections at that time after you have some more Angular practice under your belt.
You have a bright future with Angular, keep up the good work!
Your directive must be camel-cased
.directive('myDirective', function () {
then in your html, your are free whether to call it my-directive or myDirective
Both are valid
<my-directive name="test"></my-directive>
<myDirective name="test"></myDirective>
Just to follow up on this, I had to use the following way to get my directive to work.
<my-directive name="test"></my-directive>

Instantiate custom angular controller

How to instantiate a custom controller from code and preserve scope inheritance. In other words I want to do something like this:
var controller = 'myCtrl';
var html = '<p>{{value}}</p>';
var validScope= $scope.$new({
value : 'Hello, custom controllers'
}); // Or something like this to get valid scopes inheritance
$(document.body).append(instantiate(controller, html, validScope));
So i need two answers: how to instantiate custom controller and how to do it like angular do.
UPD. I've tried do it this way:
$compile('<div ng-controller="myCtrl">'+html+'</div>')(validScope);
Controller was instantiated. But placeholded values was not binded.
I do not know the context of where you are trying to all this controller from but I am going to assume you are wither in another controller, a service, or a directive.
The code below will show how to create a controller from a service.
The example may cover more than what you would need to do but this is a pattern that will work.
Create an abstract controller, this sets the constructor parameters of the controller and insulates the rest of the dependencies.
module.factory('AbstractCtrl', ['dependencies...', function (dependencies...) {
var ctrl = function($scope) {
// Do controller setup.
};
return ctrl;
}]);
Now create a controller implementation based on the abstract
module.controller('CtrlImpl', ['$scope', 'AbstractCtrl', function ($scope, AbstractCtrl) {
// Initialize the parent controller and extend it.
var AbstractCtrlInstance = new AbstractCtrl($scope);
$.extend(this, AbstractCtrlInstance);
// … Additional extensions to create a mixin.
}]);
Now that you have a controller with a minimally defined constructor to create an instance of the controller you just need to call inject the $controller and do the following:
$controller('CtrlImpl', {$scope: $scope}));
I think that the best approach is to expose a function on the scope for retrieving your controller. (ngController can take a string or a function) Lets say you have different values which need different constructors... something vaguely like this:
<div ng-repeat="item in items">
<div ng-controller="controllerFor(item)">
// whatever
</div>
</div>
That controllerFor function will know how to do the mapping for you. Hopefully, you can avoid using $compile all together.

how to access controller scope from a 2 level directive

I am trying to build generic code as much as possible.
So I'm having 2 directives, one nested inside the other while I want the nested directive to call a method on the main controller $scope.
But instead it requests the method on the parent directive, I want to know how to execute a method against the main controller scope instead of the parent directive.
Here is a sample code for my issue
My HTML should look something like this:
<div ng-controller='mainctrl'>
<div validator>
<div datepicker select-event='datepickerSelected()'/>
</div>
</div>
Javascript:
var app = angular.module("app",[]);
var mainctrl = function($scope){
$scope.datepickerSelected = function(){
//I WANT TO ACCESS THIS METHOD
}
}
app.directive("validator",function(){
return {
scope : {
//the datepicker directive requests a datepickerSelected() method on this scope
//while I want it to access the mainctrl scope
}
link: function(scope){
//some code
}
}
});
app.directive("datepicker", function(){
return{
scope: {
selectEvent: '&'
}
link: function(scope, elem){
//example code
$(elem).click(scope.selectEvent); //want this to access mainctrl instead validator directive
}
}
});
Simply remove the validator directive's scope property, thus eliminating its isolated scope. That means that validator will have the same scope that it is nested in (your controller) and datepicker will use that scope.
Another option if you want both to have isolated scopes (doesn't sound like you do) is to pass the function through to "validator's" scope.

Categories

Resources