I have a directive and a controller. The directive defines a function in its isolate scope. It also references a function in the controller. That function takes a callback. However, when I call it from the directive and pass in a callback, the callback is passed through as undefined. The code below will make this more clear:
Directive
directive('unflagBtn', ["$window", "api",
function($window, api) {
return {
restrict: "E",
template: "<a ng-click='unflag(config.currentItemId)' class='btn btn-default'>Unflag</a>",
require: "^DataCtrl",
scope: {
config: "=",
next: "&"
},
controller: ["$scope",
function($scope) {
$scope.unflag = function(id) {
$scope.next(function() { //this callback does not get passed
api.unflag(id, function(result) {
//do something
return
});
});
};
}
]
};
}
]);
Controller
controller('DataCtrl', ['$rootScope', '$scope', 'api', 'dataManager', 'globals',
function($rootScope, $scope, api, dataManager, globals) {
...
$scope.next = function(cb) { //This function gets called, but the callback is undefined.
// do something here
return cb ? cb() : null;
};
}
]);
HTML
<unflag-btn config="config" next="next(cb)"></unflag-btn>
I've read here How to pass argument to method defined in controller but called from directive in Angularjs? that when passing parameters from directives to controller functions, the parameters need to be passed in as objects. So I tried something like this:
$scope.next({cb: function() { //this callback does not get passed still
api.unflag(id, function(result) {
//do something
return
});
}});
But that did not work. I am not sure if this matters, but I should note that the directive is placed inside a form, which in its place is inside a controller. Just to illustrate the structure:
<controller>
<form>
<directive>
<form>
<controller>
Hope this is clear and thanks in advance!
Try this
controller: ["$scope",
function($scope) {
$scope.unflag = function(id) {
$scope.next({
cb: function() { //this callback does not get passed
api.unflag(id, function(result) {
//do something
return;
});
}
});
};
}
]
So I unintentionally figured out whats wrong after not being able to pass an object back to the controller as well. What happened, (and what I probably should have mentioned in the question had I known that its relevant) is that the parent scope of this directive unflagbtn is actually the scope of another directive that I have, call it secondDirective. In its turn the secondDirective is getting its scope from "DataCtrl". Simplified code looks like this:
directive("secondDirective", [function(){
require: "^DataCtrl" // controller
scope: {
next: "&" // function that I was trying to call
}
...
// other code
...
}]);
directive("unflagbtn", [function(){
require: "^DataCtrl" // controller
scope: {
next: "&"
},
controller: ["$scope", function($scope){
$scope.unflag = function(){
$scope.next({cb: {cb: callBackFunctionIWantedToPass}); // this is what worked
}
}
}]);
So passing a callback in that manner solved my problem as it made its way back to the controller. This is ugly most likely due to my poor understanding of angular, so I apologize as this is most likely not the correct way to do this, but it solved my problem so I though I'd share.
Cheers,
Related
I wonder how I can encapsulate functionality inside of an angular directive according to Rober C. Martin's "Clean Code" book. I want to omit comments and use functions with speaking names instead.
Imagine this code:
app.directive('myDirective' function() {
return {
link: function(scope) {
// initialize visual user state
scope.visualUserState = {
// some very detailed state initialization
}
}
})
To encapsulate the load functionality, I would like to replace this code like this:
app.directive('myDirective' function() {
return {
link: function(scope) {
scope.initializeVisualUserState = function() {
scope.visualUserState = {
// some very detailed state initialization
}
}
scope.initializeVisualUserState();
}
})
What I do not like on the second approach is that "loadDataFromServer" is some functionality that is only used by the link function and not by the view, so I violate the rule that scope should only hold data and functions that is used to interact with the view.
Also the code is not very readable I think.
As the functionality handles very private stuff of the directive, I think that using and injecting a service is not the right way to go.
What would be a better practice to encapsulate this functionality?
You should use a controller to add logic to your directive. In your controller you can inject Services. It's best to write a service for a single purpose, and simply let your controller call the services.
In fact you should only use the link function if you need to have your DOM-node, which is actually pretty close to never.
Read the styleguide by John Papa
angular.module('myModule', []);
// Controller
(function() {
angular
.controller('myModule')
.controller('MyController', ['$scope', 'DataService', function($scope, DataService) {
DataService
.retrieveData()
.then(function(data) {
$scope.visualUserState = data;
});
}]);
})();
// Directive
(function() {
angular
.module('myModule')
.directive('myDirective', function() {
return {
'restrict': 'E',
'scope': true,
'controller': 'MyController',
'controllerAs': '$ctrl'
};
});
})();
(function(module){
module.directive('myDirective', myDirective);
function myDirective(){
var directive = {
link : link,
scope : {state : '='},
restrict : 'EA',
template : //Some template which uses state
}
return directive;
function link(){
}
};
module.controller('myController', myController);
myController.$inject = ['$scope', 'OtherService'];
function myController(scope, service){
//service is used to pull the data and set to $scope.userState.
}
})(angular.module('myApp'))
And your directive will be :
<myDirective state="userState"></myDirective>
Let me know if this helps.
I have a directive defined that contains a function binding:
angular
.module('my.module')
.directive("myDirective", [
function () {
return {
controller: 'MyDirectiveController',
templateUrl: 'controls/myDirective/myDirective.html',
replace: true,
restrict: 'E',
scope: {
openFunction: '&'
}
};
}]);
in my source html, I'm defining the directive like so:
<my-directive
open-function="openDrawer(open)"
</my-directive>
Then, in my directive controller, I'm calling it like this:
$scope.openFunction({
open: function() {
doSomething()
.then(function () {...})
.finally(function () {...});
}
});
And here's the parent controller openDrawer function:
$scope.openDrawer = function (open) {
$scope.alerts = null;
$scope.showActions = false;
if (service.editing) {
service.closeAndSave();
openDrawerAfterDelay(open);
} else if (otherService.editing) {
otherService.commit();
openDrawerAfterDelay(open);
} else {
open();
}
};
The problem is, when my directive controller calls the $scope.openFunction() function, nothing happens. Am I able to pass a function, to the bound function like this?
This appears to be something unrelated. I've reproduced the code in plunker here: http://plnkr.co/edit/SuzCGw5WVRClEwEcA17l?p=preview
Possible leads that I can see without seeing the entire code are:
I'm mocking the service and otherService, so the if conditions are always false - the problem may lie in the calls within the if blocks.
I'm unsure of what the doSomething promise is doing, this may be a cause.
I'm using AngularUI router (0.2.13) and have a state defined as such
.state('foo', {
template:'<div some-directive></div>',
resolve: {
foo: function(SomeService) {
return SomeService.something().promise;
}
}
})
and a directive like this:
app.directive('someDirective', function(){
return {
controller: function(data) {
// I want `data` to be injected from the resolve...
// as it would if this was a "standalone" controller
}
}
})
but this doesn't work - the data parameter causes an UnknownProvider error. Defining the directive controller independently and setting it by name in the directive has the same result.
I more or less get why this is happening, but have two questions:
is there a way to do what I'm trying to do?
should I be trying to do it, or have I slipped into an antipattern here?
You can't use resolves in directives, but you can pass the result resolved in the state down to the directive which I think accomplishes what you're looking for.
You'd want to update your state definition to include a controller and set a parameter on the directive:
.state('foo', {
template:'Test<div some-directive something="foo"></div>',
url: 'foo',
resolve:{
foo:function(SomeService){
return SomeService.something();
}
},
controller: function($scope, foo){
$scope.foo = foo;
}
})
Then update the directive to use this parameter:
.directive('someDirective', function(){
return {
controller: function($scope) {
// I want `data` to be injected from the resolve...
// as it would if this was a "standalone" controller
console.log('$scope.something: '+ $scope.something);
},
scope: {
something: '='
}
};
})
Here's a sample plunker: http://plnkr.co/edit/TOPMLUXc7GhXTeYL0IFj?p=preview
They simplified the api. See this thread:
https://github.com/angular-ui/ui-router/issues/2664#issuecomment-204593098
in 0.2.19 we are adding $resolve to the $scope, allowing you to do "route to component template" style
template: <my-directive input="$resolve.simpleObj"></my-directive>,
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.
How does one abstract a directive properly?
As a really basic example, let's say I have this:
http://plnkr.co/edit/h5HXEe?p=info
var app = angular.module('TestApp', []);
app.controller('testCtrl', function($scope) {
this.save = function() {
console.log("hi");
}
this.registerListeners = function() {
console.log('do stuff to register listeners');
}
this.otherFunctionsNotToBeChangedWithDifferentInstances() {
console.log('these should not change between different directives')
}
return $scope.testCtrl = this;
});
app.directive("tester", function() {
return {
restrict: 'A',
controller: 'testCtrl',
template: '<button ng-click="testCtrl.save()">save</button>'
};
});
The tester directive has some methods on it, but only two will be changed or used depending on where the directive is placed. I could pass in the function as a directive attribute, but I am wondering if there is a better way to do this. I have been looking at providers, but I am unsure how or if those would even fit into this.
Instead of letting your directive assume that testCtrl.save() exist on the scope, you would pass in that function as an attribute. Something like this: http://jsbin.com/jidizoxi/1/edit
Your directive binds the value of the my-on-click attribute as a callable function. Your template passes in the controllers ctrlOnClick() function, and when the buttons ng-click calls myOnClick() Angular will call ctrlOnClick() since they are bound to each other.
EDIT:
Another common approach is to pass in a config object to the directive. So your controller would look something like:
$scope.directiveConfig = {
method1: function() { ... },
method2: function() { ... },
method3: function() { ... },
...
}
And your template:
<my-directive config="directiveConfig"></my-directive>
The directive then gets a reference to that object by:
scope: {
config: '='
}
The directive can then call methods on the object like this: $scope.config.method1().