Angular variable manipulation from different directives - angularjs

I am a bit struggling with infinite scroll in angular. I have one object array where all items are stored in. This object is part of directive controller.
Now when I am trying to implement infinite scroll I use separate directive to calculate offsets. I would like to access from this scroll directive variable from the other directive where object array is defined.
How can I do this? What would be the easiest way here? I am searching for week and can't find anything easy enough to implement to my solution.
Thank you

You could either use the directive's require property to get the $scope of another directive's controller, or use the parent controller of the directives to pass in a shared value. Here's an example using require (live demo).
<div ng-app="myApp">
<foo></foo>
<bar></bar>
</div>
angular.module('myApp', [])
.directive('foo', function() {
return {
controller: function($scope) {
$scope.foo = 123;
}
};
})
.directive('bar', function() {
return {
require: '^foo',
controller: function($scope) {
console.log($scope.foo);
}
};
})
;
The timing for this next example may not be what you want. They are sharing the same variable, but changes to $scope in the first directive's controller won't be applied until after the second directive's controller has already run. (live demo).
<div ng-app="myApp" ng-controller="MyCtrl">
{{sharedValue}}
<foo shared-value="sharedValue"></foo>
<bar shared-value="sharedValue"></bar>
</div>
angular.module('myApp', [])
.controller('MyCtrl', function($scope) {
$scope.sharedValue = 'abc';
})
.directive('foo', function() {
return {
scope: {
sharedValue: "="
},
controller: function($scope) {
console.log($scope.sharedValue); // abc
$scope.sharedValue = 123;
}
};
})
.directive('bar', function() {
return {
scope: {
sharedValue: '='
},
controller: function($scope) {
console.log($scope.sharedValue); // still abc, will update later
}
};
})
;

Related

Angular - changes to directive controller's scope aren't reflected in view

Changes to my scope variable foo are getting updated in the html. When that value is change inside the scope of a directive's controller, it isn't updating in the html.
What do I need to do to make it update?
I have a simple example:
app.js
var app = angular.module('app', []);
app.controller('ctrl', function($scope) {
$scope.foo = 99;
$scope.changeValue = function() {
$scope.foo = $scope.foo + 1;
}
});
app.directive('d1', function(){
return {
restrict: 'E',
scope: {
theFoo: '='
},
templateUrl: 'd1.html',
controller: 'd1Ctrl',
}
});
app.controller('d1Ctrl', function($scope) {
$scope.test = $scope.theFoo;
});
d1.html
<div>
<p>The value of foo is '{{theFoo}}'.</p>
<p>The value of test is '{{test}}'.</p>
</div>
inside index.html
<d1 the-foo='foo'>
</d1>
<button ng-click='changeValue()'>change value</button>
So in summary, {{theFoo}} is updating, but {{test}} isn't. Why?
The reason is that $scope.foo value is a primitive.
In the directive controller you only assign $scope.test once when controller initializes. Primitives have no inheritance the way objects do so there is nothing that would change $scope.test after that initial assignment
If you used an object instead to pass in ... inheritance would be in effect and you would see changes...otherwise you would need to watch $scope.theFoo and do updates to $scope.test yourself
The code you have in your controller only initializes to that value if it is indeed set at the time the controller is linked. Any subsequent changes are not going to work.
If you want to bind any subsequent changes, then you need to set a $watch statement either in your controller or a link function.
$scope.$watch( 'theFoo', function(val){ $scope.test = val; })
updated plunker - http://plnkr.co/edit/eWoPutIJrwxZj9XJu6QG?p=preview
here you have isolated the scope of the directive, so test is not visible to the d1.html, if you need to change test along with the theFoo you must first make it visible to the directive by
app.directive('d1', function(){
return {
restrict: 'E',
scope: {
theFoo: '=',
test : '=' //getting test as well
},
templateUrl: 'd1.html',
controller: 'd1Ctrl',
}
});
and in index.html you should pass the value to the test by
<d1 the-foo='foo' test='foo'></d1>
in the above code your controller is not much of a use , code will work fine even without this part controller: 'd1Ctrl'.
with this example you dont have to use $watch.

changing the value of object in attrs inside controller of a directive

I have a directive as follows
<div ng-controller=prdController as prd>
<my-dir data=prd.data ng-click=stateChanged()></my-dir>
</div>
where prd.data is an object. In my directive I did the following
app.directive('myDir',function(){
return {
scope:{
data:'=data'
},
templateUrl: './templates/testtemplate.html',
controllerAs:'bd',
controller:function($scope,$attrs){
this.stateChanged = function (value) {
$attrs.data = { 'fd','sdfs'};
}
}
});
I am unable to modify the data value within the controller how do I proceed thanks in advance.
I think the problem could be in the way you trying to get data object in your controller.
You should try to get it by $scope.data. All directive inputs getting attached to your internal scope.
$attrs - is a hash object with key-value pairs of normalized attribute names and their corresponding attribute values. According to AngularJS docs.
Try this
var app = angular.module("myApp", []);
app.controller("prdController", function($scope) {
$scope.data = "krupesh";
$scope.stateChanged = function() {
$scope.data = "kotecha";
}
});
app.directive('myDir', function() {
return {
restrict: 'E',
template: "<div>{{data}}</div>"
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller=prdController as prd>
<my-dir data="prd.data" ng-click=stateChanged()></my-dir>
</div>

Can I require a controller, in a directive, set with ng-controller?

I have (sort of) the following html:
<div ng-controller="MyController">
<my-sub-directive></my-sub-directive>
</div>
how the controller looks is not important:
app.controller("MyController", function($scope) {
$scope.foo = "bar";
})
and my directive looks like this:
function mySubDirective() {
return {
restrict: "E",
templateUrl:"aTemplate.html",
require: "^MyController",
link: function($scope, element) {
}
};
}
app.directive("mySubDirective", mySubDirective);
In the documentation they always specify another directive in the require-property, but it says that it means you require the controller. So I wanted to try this solution. However I get the error
"Controller 'MyController', required by directive 'mySubDirective', can't be found".
Is it not possible to require a controller from the directive if it is set by ng-controller?
You can only do:
require: "^ngController"
So, you can't be more specific than that, i.e. you can't ask for "MainCtrl" or "MyController" by name, but it will get you the controller instance:
.controller("SomeController", function(){
this.doSomething = function(){
//
};
})
.directive("foo", function(){
return {
require: "?^ngController",
link: function(scope, element, attrs, ctrl){
if (ctrl && ctrl.doSomething){
ctrl.doSomething();
}
}
}
});
<div ng-controller="SomeController">
<foo></foo>
</div>
I don't think, though, that this is a good approach, since it makes the directive very dependent on where it is used. You could follow\ the recommendation in the comments to pass the controller instance directly - it makes it somewhat more explicit:
<div ng-controller="SomeController as ctrl">
<foo ctrl="ctrl"></foo>
</div>
but it still is a too generic of an object and could easily be misused by users of your directive.
Instead, expose a well-defined API (via attributes) and pass references to functions/properties defined in the controller:
<div ng-controller="SomeController as ctrl">
<foo do="ctrl.doSomething()"></foo>
</div>
You can use element.controller() in the directive link function to test the closest controller specified by ngController. A limitation of this method is that it doesn't tell you which controller it is. There are probably several ways you can do it, but I'm opting to name the controller constructor, and expose it in the scope, so you can use instanceof
// Deliberately not adding to global scope
(function() {
var app = angular.module('my-app', []);
// Exposed in so can do "instanceof" in directive
function MyController($scope) {}
app.controller('MyController', MyController);
app.directive("foo", function(){
return {
link: function($scope, $element){
var controller = $element.controller();
// True or false depending on whether the closest
// ngController is a MyController
console.log(controller instanceof MyController);
}
};
})
})();
You can see this at http://plnkr.co/edit/AVmr7Eb7dQD70Mpmhpjm?p=preview
However, this won't work if you have nested ngControllers, and you want to test for one that isn't necessarily the closest. For that, you can defined a recursive function to walk up the DOM tree:
app.directive("foo", function(){
function getAncestorController(element, controllerConstructor) {
var controller = element.controller();
if (controller instanceof controllerConstructor) {
return controller;
} else if (element.parent().length) {
return getAncestorController(element.parent(), controllerConstructor);
} else {
return void(0); // undefined
}
}
return {
link: function(scope, element){
var controller = getAncestorController(element, MyController);
// The ancestor controller instance, or undefined
console.log(controller);
}
};
})
You can see this at http://plnkr.co/edit/xM5or4skle62Y9UPKfwG?p=preview
For reference the docs state that the controller function can be used to find controllers specified with ngController:
By default retrieves controller associated with the ngController directive

AngularJS can't pass in different function when directive coupled with controller

I have a directive which I want to tightly couple with a controller as a component. I assumed I was following best practice by explicitly passing ion my functions even though I was declaring the controller to use. Here is an example:
app.js
var app = angular.module('plunker', [])
app
.controller('myCtrl', function($scope) {
$scope.output = '';
$scope.foo = function () {
$scope.output = 'foo';
}
$scope.bar = function () {
$scope.output = 'bar';
}
})
.directive('myDirective', function() {
return {
scope: {
output: '=',
foo: '&',
},
templateUrl: 'template.html',
replace: true,
controller: 'myCtrl',
};
})
template.html
<div>
<button ng-click="foo()">Click Foo</button>
<p>You clicked: <span style="color:red">{{output}}</span></p>
</div>
index.html
<body>
<my-directive
output="output"
foo="bar()"> <!-- pass in the *bar* function instead of the *foo* function -->
</my-directive>
</body>
Plunkr: http://plnkr.co/edit/Y4lhxuXbK9YbjAklR7v1?p=preview
Here, even though I'm passing in the bar() function, the output is 'foo' when the button is clicked. If I uncouple the controller by commenting out controller: 'myCtrl' in the directive, the output becomes 'bar'.
I thought I could declare the controller but still be free to pass in which functions I desire to the directive. It also seems that explicitly passing these functions in is a little redundant if the directive just looks up to the controller to find it (I can pass nothing into the directive and it still works).
This is especially problematic when testing as I would like to pass in my own stub functions to the directive, which at the moment I cannot do.
Is there some way to achieve what I want or am I doing something fundamentally wrong?
EDIT I meant to not have the controller declared in the HTML.
Remove the controller property on the directive:
.directive('myDirective', function() {
return {
scope: {
output: '=',
foo: '&',
},
templateUrl: 'template.html',
replace: true,
// controller: 'myCtrl',
};
})
You're wiring up the same controller to the directive as the parent, which is overwriting all the properties you're trying to pass in via isolate scope. The controller is wired up twice, once on the parent scope and then again on the directive. Removing this will allow you to pass in the function bar() and it will not be overwritten.
Here's the Plunker Demonstration
When running inside a directive, the $scope is initialized with output and foo variables before the controller constructor is called. Your controller is essentially overwriting these properties.
A simple check in your controller
if(!$scope.foo)
{
$scope.foo = function () {
$scope.output = 'foo';
}
}
Would work.
PS. I'm assuming your example is a simplification of your problem. If it's not, then the other answer's advice to simply remove the controller from the directive is the best approach.

Extending a controller function within directive

I have a cancel function in my controller that I want to pass or bind to a directive. This function essentially clears the form. Like this:
app.controller('MyCtrl', ['$scope', function($scope){
var self = this;
self.cancel = function(){...
$scope.formName.$setPristine();
};
}]);
app.directive('customDirective', function() {
return {
restrict: 'E'
scope: {
cancel : '&onCancel'
},
templateUrl: 'form.html'
};
});
form.html
<div>
<form name="formName">
</form>
</div>
However, the $setPristine() don't work as the controller don't have access on the form DOM. Is it possible to extend the functionality of controller's cancel within the directive so that I will add $setPristine()?
Some suggested using jQuery to select the form DOM, (if it's the only way) how to do that exactly? Is there a more Angular way of doing this?
Since the <form> is inside the directive, the controller should have nothing to do with it. Knowing it would break encapsulation, i.e. leak implementation details from the directive to the controller.
A possible solution would be to pass an empty "holder" object to the directive and let the directive fill it with callback functions. I.e.:
app.controller('MyCtrl', ['$scope', function($scope) {
var self = this;
$scope.callbacks = {};
self.cancel = function() {
if( angular.isFunction($scope.callbacks.cancel) ) {
$scope.callbacks.cancel();
}
};
});
app.directive('customDirective', function() {
return {
restrict: 'E'
scope: {
callbacks: '='
},
templateUrl: 'form.html',
link: function(scope) {
scope.callbacks.cancel = function() {
scope.formName.$setPristine();
};
scope.$on('$destroy', function() {
delete scope.callbacks.cancel;
});
}
};
});
Use it as:
<custom-directive callbacks="callbacks"></custom-directive>
I'm not sure I am OK with this either though...

Resources