Assigning a controller from a directive angularjs 1.2 - javascript

I'm switching from angularJS 1.0 to 1.2 and I'm having some trouble assigning a controller to a directive with a clean scope, without defining a controller in my html with ng-controller.
consider the following scenario:
var myApp=angular.module('myApp',[])
.controller('parentCtrl',function($scope,$element,$attrs) {
$scope.helloworld = function(){
console.log('hello world from ParentCtrl controller')
}
})
.controller('childCtrl',function($scope,$element,$attrs) {
$scope.helloworld = function(){
console.log('hello world from ChildCtrl controller')
}
})
.directive('ngParent', function() {
return {
controller:'ParentCtrl'
}
})
.directive('ngChild', function() {
return {
controller:'ChildCtrl',
scope:{},
link:function(scope,element,attrs){
}
}
})
And HTML:
<body ng-app="myApp" ng-parent>
<button ng-click="helloworld()">Parent Scope click </button>
<div ng-child>
<button ng-click="helloworld()">Child Scope click </button>
</div>
</body>
Right now, both buttons will log "hello world from ParentCtrl controller". However, if I change the parameter scope:{} to scope:true in the ngChild directive, it works and the buttons are calling their respective scopes' helloworld function. But that doesn't make a clean scope like I want it to.
How do I make a clean scope in a directive with angular 1.2, without assigning the controller in my view?
Thanks,
Andreas

because you're not creating a new scope both directives are using the same scope and the definition of "helloworld" function gets overwritten. so either you need to change the name of the function or create a new scope,
and by the way i think you're using directives wrong.
instead of defining directives and assign controllers to them you should use ng-controller and for each of controllers set the helloworld function in their own scopes. and write a directive (if it's really needed) to get that function and call it when you need to.

Related

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.

Isolated Scope View binding

I have been working with isolated scope directives for a little time and a question came in mind watching it's behavior:
Why can't i bind variables that i define inside the directive inherited scope directly to the view?
Let me show an example on this code pen:
http://codepen.io/anon/pen/VLKjrv
When i create a new $scope variable inside the directive controller and i try to bind it on the view, it does not works.
By the other hand, when i bind that variable on a html that comes from the template directive attribute, it does works.
Check out the code:
<body ng-app="isolated-test-app">
<section ng-controller="isolatedTestCtrl">
<article>
<h1>test1</h1>
<div isolated-directive binding-from-attr="test">
<span ng-bind="test"></span>
<span ng-bind="test2"></span>
</div>
<h1>test2</h1>
<div isolated-directive-number-two binding-from-attr="test">
</div>
</article>
</section>
angular.module('isolated-test-app', [])
.controller('isolatedTestCtrl', function isolatedTestCtrl($scope){
$scope.test = 'Binded from parent controller';
})
.directive('isolatedDirective', function isolatedDirective(){
var directive = {
scope: {
bindingFromAttr: '=',
},
controller: function directiveController($scope){
$scope.test2 = 'Binded from directive controller!';
},
};
return directive;
})
.directive('isolatedDirectiveNumberTwo', function isolatedDirective2(){
var directive = {
scope: {
bindingFromAttr: '=',
},
template:'<span ng-bind="bindingFromAttr"></span>\
<span ng-bind="test2"></span>',
controller: function directiveController($scope){
$scope.test2 = 'Binded from directive controller!';
},
};
return directive;
})
test1
Binded from parent controller
test2
Binded from parent controller
Binded from directive controller!
I was expecting the result of test2 on test1.
Why does that happens?
There is a difference between directive template and the directive's element's contents with regards to what scope applies.
In isolate scope (scope: {}) directives, the isolate scope applies to the template, but not to the contents. The contents have the same scope as the directive's parent. Also, note, that the contents would be replaced by the template, if the template is defined. To use the contents in addition to the template requires "transcluding" (transclude: true) (this is, however, outside of scope for this answer).
If you are confused, you could always check $scope.$id to see which scope applies:
<div>parent scope: {{$id}} (will be 2)</div>
<isolate-scope>
contents of isolate scope directive: {{$id}} (will also be 2)
</isolate-scope>
<isolate-scope-with-template>
contents will be replaced with the template
</isolate-scope-with-template>
.directive("isolateScopeWithTemplate", function(){
return {
scope: {},
template: "template: {{$id}} (will NOT be 2)"
}
})
(of course, the actual $id could be different)
In child scope (scope: true) directives, the scope that applies to the content is actually the same that would have applied to the template (same here - the template would replace the contents if it exists, unless you transclude).
Now, to answer your question:
The first <span ng-bind="test2"></span> binds to a non-existent $scope.test2 in the parent scope and so it is empty.
But the <span ng-bind="test2"></span> in the template of isolatedDirectiveNumberTwo binds to the isolate scope of that directive, which defines $scope.test2 = 'Binded from directive controller!'.
This is my guess base on experiment in http://codepen.io/anon/pen/MwjjBw
so for test 1, the directive scope doesnt have test/test2 since the dom object is belong to controller. Hence in order to update it you have to use
$scope.$parent.test2 = "" ;
and for test 2, as the template is created as part of directive hence the dom object is belong to directive and also accessible by controller ( I guess $compile adding this into controller scope/watch).
You can also see that test1 doesnt have any watcher as there is no binding happen.
Now i get the whole picture, as New Dev's answer stated on his answer
In isolate scope (scope: {}) directives, the isolate scope applies to the template, but not to the contents. The contents have the same scope as the directive's parent.
So i have learned that there is a difference between directive contents and template and how the scope is inherited on isolated scopes.
For my application setting the scope to true solved entirely my problem.
Also, kwan245's solution is a real good work-around of this issue.
Both answers cleared my mind, many thanks to New Dev and kwan245 :)

How to trigger methods on my custom directive?

I have a custom directive that I'm using in my templates. It does a bit of DOM work for me. I would like the host view/controller that I'm using the directive in to be able to run methods on my directive (and it's controller). But I'm not sure how best to call into the directives scope.
Example Fiddle
My view code:
<div ng-app="app">
<div ng-controller="MainCtrl">
<h3>Test App</h3>
<button ng-click="scopeClear()">Parent Clear</button>
<div my-directive string="myString"></div>
</div>
</div>
Here is the custom directive:
angular.module('components', []).directive('myDirective', function() {
function link(scope, element, attrs) {
scope.string = "";
scope.$watch(attrs.string, function(value) {
scope.string = value;
});
}
return {
controller: function($scope, $element) {
$scope.reset = function() {
$scope.string = "Hello";
}
$scope.clear = function() {
$scope.string = "";
}
},
template:
"<button ng-click='reset()'>Directive Reset</button>" +
"<button ng-click='clear()'>Directive Clear</button><br/>" +
"<input type='text' ng-model='string'>",
link: link
}
});
And controller:
angular.module('app', ['components']).controller('MainCtrl', function($scope) {
$scope.myString = "Hello";
$scope.scopeClear = function() {
// How do I get this to call the clear() method on myDirective
}
});
The workaround I found is jQuery('#my_directive').scope().myMethod(); But this seems wrong, like I'm missing some better part of angular to do this.
It also seems like and $emit isn't right here since I want a targeted method so it won't trigger on additional instances of the directive I have on the same page.
How would I access the directives methods from my parent controller?
I'm not sure I fully understand your objective here, and it's possible you could find a better pattern completely. Typically, directives display the state of the scope which is either an isolate scope (if they are self-sufficient) or a shared scope. Since you are not creating an isolate scope then they inherit the scope from the controller. If they are displaying data inherited from the controller then you don't want your controller calling into the directive, rather the directive will simply "redraw" itself whenever the properties in the controller change.
If you, instead, are looking to recalculate some stuff in your directives based on events from outside the directive you don't want any tight coupling - especially if building an entirely separate module. In that case, you might simply want to use $broadcast from the $scope within MainCtrl to broadcast an event that you may care about, and then your directive can provide the $on('eventName') handler. This way it's portable to any controller/scope that will fire such an event.
If you find yourself needing to know the exact properties in the controller or the exact functions within the directive then I would suggest that you have too-tightly coupled these pieces and they don't belong in separate modules since they could never be reused. Angular directives and controllers are not objects with functions, but objects that create scope and update frequently via $digest calls whenever properties in that scope change. So you may be able to find a way to better model the data, objects, and properties you are displaying. But I can't say without greater context.

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.

AngularJS: dynamically assign controller from ng-repeat

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.

Categories

Resources