I made an AngularJS directive which works in AngularJS 1.2 but after updating my application to AngularJS 1.3 the fourth parameter of my link function does not contain an array of controller instances anymore but an array of $get.Constructor {} items.
jtApp.directive("jtWizard", ["$q", "$timeout", function ($q, $timeout) {
return {
require: ["^ngController", "jtWizard"],
restrict: "E",
replace: false,
transclude: true,
templateUrl: "App/Components/wizard.htm",
scope: {
mainHeader: "#"
},
controller: "jtWizardController",
link: function postLink(scope, elm, attrs, controllers, transcludeFn) {
...
var userWizardController = controllers[0];
var jtWizardController = controllers[1];
...
}
);
Any ideas about that?
1.3 changes how directive controllers are created, controllers are now instances of Constructor, I believe it is a side effect of commit 5f3f25a1. This also means you cannot return an object to define a controller, rather must modify the controllers members directly.
controller: function() { this.foo = "bar"; }
rather than
controller: function() { return { foo: "bar" }; }
Functionality should otherwise remain backwards compatible.
Related
I'm trying to extend functionality of any directive by simply attaching an attribute directive, but I'm having trouble getting the scope of the element on which the attribute is defined.
For example, I have this template:
<div class="flex-item-grow flex-item flex-column report-area">
<sv-report sv-reloadable id="reportId"></sv-report>
</div>
Here, sv-reloadable has some implicit understanding of sv-report, but sv-report has no idea about sv-reloadable.
I've defined sv-reloadable as:
angular
.module( 'sv-reloadable', [
'sv.services',
])
.directive('svReloadable', function(reportServices, $timeout) {
return {
restrict: 'A',
controller: function($scope, $timeout) {
$scope.$on('parameter-changed', function(evt, payload) {
evt.stopPropagation();
$scope.viewModel = getNewViewModel(payload);/* hit the server to retrieve new data */
});
}
};
});
Now, $scope in sv-reloadable is the parent scope of sv-report. I'm wanting sv-reloadable to be able to attach a listener to sv-report's scope, and swap out properties of that scope. I understand that it's possible to grab the sibling scopes, but that causes problems when trying to figure out exactly which element it's attached to.
I attempted the following:
link: function(scope, element, attrs) {
ele = element;
var actualScopyThingy = element.scope();
},
Which I had assumed would give me the scope of the element the attribute was defined on, but alas, it still returns the parent scope of the element.
If it's important, sv-report is defined as the following, but I'd like to be able to keep it the same (since sv-reloadable is going to be attached to many different elements, all of which must have viewModel defined on their scope)
return {
restrict: 'E',
replace: true,
templateUrl: 'sv-report/sv-report.tpl.html',
scope: {
id: '=',
reportParameters: '='
},
controller: function ($scope, svAnalytics) {
/* unrelated code here */
},
link: function(scope, element, attrs) {
initialLoadReport(scope);
}
};
After a bit of digging around, isolateScope() is what I was after (rather than scope()). sv-reloadable's directive becomes:
return {
restrict: 'A',
link: function(scope, element, attrs) {
var elementScope = element.isolateScope();
elementScope.$on('parameter-changed', function(evt, payload) {
...
});
}
};
Below is the only way i could figure out how to get a directive to pull out an attribute from its origin element, get a new value by hitting a service, and then adding that new service method return as a class in the directive template. i'm wondering if there is an alternative pattern that might be cleaner then this pattern that might use ng-class or possibly ng-transclude:
html:
<my-directive data-condition="{{hour.condition}}"></my-directive>
js:
angular.module('myApp')
.directive('myDirective', function (myService) {
return {
transclude: true,
replace: true,
scope: true,
template: '<i class="{{wiIconClass}}"></i>',
restrict: 'E',
link: function($scope, $elm, attrs){
$scope.wiIconClass=myService.getValue(attrs.condition);
}
}
});
If your function myService.getValue is synchronous, you could simply do:
<div ng-class="getClass(hour.condition)">
And in your controller:
$scope.getClass = function(condition) {
return myService.getValue(condition);
}
Alternatively, you can directly put your service within your scope:
$scope.myService = myService;
So the HTML becomes
<div ng-class="myService.getValue(hour.condition)">
In both cases, you will need to inject your service into your controller:
myModule.controller('myController', function($scope, myService) {
// this controller has access to myService
})
I would use the Directives scope parameter instead of using the Directives Attribute values. This is because when using the attributes you will need to setup a $watch to see when that value updates, with using $scope you get the benefit of the binding aspect.
As far as to respond to the best way, its hard to say without knowing your actual task. You can have Angular update the elements css class value in several different ways.
Here's a working Plunker with some small updates to your existing code.
http://plnkr.co/edit/W0SOiBEDE03MgostqemT?p=preview
angular.module('myApp', [])
.controller('myController', function($scope) {
$scope.hour = {
condition: 'good'
};
})
.factory('myService', function() {
var condValues = {
good: 'good-class',
bad: 'bad-class'
};
return {
getValue: function(key) {
return condValues[key];
}
};
})
.directive('myDirective', function(myService) {
return {
transclude: true,
replace: true,
scope: {
condition: '='
},
template: '<i class="{{myService.getValue(condition)}}"></i>',
restrict: 'E',
link: function(scope, elm, attrs) {
scope.myService = myService;
}
};
});
Using directives I ended stuck when I needed to have more than one scope.
I'm building a data visualization app with Mongoose Node, Express and D3JS.
Here's the directive
angular.module('prodsChart', [])
.controller('businessCtrl', ['$scope','$http', 'BusinessSrv', 'Products', function($scope, $http, $location, BusinessSrv, Products) {
Products.get()
.success(function(data) {
$scope.products = data;
});
BusinessSrv.getTotal()
.success(function(data) {
$scope.businessSales = data;
});
}])
.directive( 'saleProd', [
function () {
return {
restrict: 'E',
link: function (scope, element) {
// Building the chart here
}
And the HTML :
<sale-prod></sale-prod>
Is it good to inject that way the Services in the Directive ?
Now I have 2 set of data in two $scope.
How do I use them in the directive ?
You can inject the $rootScope into your directive:
angular.module('prodsChart', [])
.directive( 'saleProd', ['$rootScope', function ($rootScope) {
// your code here
}]);
and then use it everywhere within the directive.
In 1st and 2nd example directive is in controller's scope and controller's datasets transferred to the directive as attributes. In 1st example directive can modify controller's data. In 2nd example directive use controllers data as strings and creates 2 objects 'productsObj' and 'salesObj' and can't modify parent's scope. It depends on how you handle attributes in the directive and how to transfer them into it. Just click on items in 'Product list (directive)' section and you'll see result.
1st: http://plnkr.co/edit/v46oEGHvUnxMNYsKAeaW?p=preview
var directive = function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'directive-template.html',
scope: {
products: '=',
sales: '='
}
};
};
html:
<div ng-controller="BusinessController as BusinessCtrl">
<sale-prod products="BusinessCtrl.products" sales="BusinessCtrl.sales"></sale-prod>
</div>
2nd: http://plnkr.co/edit/7CyIsqBNLbeZjyfbkGo9?p=preview
var directive = function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'directive-template.html',
scope: {
products: '#',
sales: '#'
},
link: function(scope, element, attrs) {
attrs.$observe('products', function(newVal) {
scope.productsObj = angular.fromJson(newVal);
});
attrs.$observe('sales', function(newVal) {
scope.salesObj = angular.fromJson(newVal);
});
}
};
};
html:
<div ng-controller="BusinessController as BusinessCtrl">
<sale-prod products="{{BusinessCtrl.products}}" sales="{{BusinessCtrl.sales}}"></sale-prod>
</div>
3rd example is just a piece of code that show's how to inject service in directive and controller. I add it because in you example i didn't see service injection in directive:
(function(undefined) {
var app = angular.module('prodsChart', []);
var controller = function($scope, Business, Products) {
// controller logic
};
var directive = function(Business) {
return {
restrict: 'E',
link: function(scope, element, attrs) {
// here you can use all Business service logic
}
};
};
var serviceBusiness = function() {
// business service logic
};
var serviceProducts = function() {
// products service logic
};
app.controller('BusinessController', ['$scope', 'Business', 'Products', controller])
.directive('saleProd', ['Business', directive])
.service('Business', serviceBusiness)
.service('Products', serviceProducts);
})();
html:
<div ng-controller="BusinessController as BusinessCtrl"></div>
<sale-prod></sale-prod>
You could inject a Service into a directive and use it to bring data across the application or use $emit.
A less elegant solution would be using a .value() and deal with it everywhere in your application.
So on the angular documentation website, you can define Tobias and Jeff
angular.module('docsTransclusionExample', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: 'my-dialog.html',
link: function (scope, element) {
scope.name = 'Jeff';
}
};
});
If you do The name is {{name}} it'll say
The name is Tobias
I'm trying to access Tobias in the link function. From inside the link function, how would I get the $scope.name value equal to Tobias?
Since the scope is isolated scope: {}, directive creates a new child scope. In this case the only way to directly access parent scope property is to use scope $parent, reference to parent scope object. For example:
link: function(scope, element) {
scope.name = 'Jeff';
scope.parentName = scope.$parent.name; // Tobias
}
However this is not ideal. This is why you may want to consider more flexible approach:
<my-dialog name="name"></my-dialog>
and define a scope configuration as:
scope: {
parentName: '=name'
}
You will have to use $parent property of the the scope:
scope.$parent.name = 'Jeff';
you can get it through $parent like this:
link: function (scope, element) {
scope.name = 'Jeff';
console.log(scope.name);
console.log(scope.$parent.name);
}
As you have used transclude:true, you can omit scope:{} if you do not have any local variables. Putting scope:{} does not make sense.
so the declaration would be like following
angular.module('docsTransclusionExample', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
templateUrl: 'my-dialog.html',
link: function (scope, element) {
// scope.name = 'Jeff';
// if name is in your parent scope, you should be able to get it here
console.log(scope.name);
}
};
});
If you look at the template you will see ng-transclude directive has been used, this means where in template the parent scope's variables will be used there. Hope it makes sense.
I'm just wondering why would you want something like this.
This way you're creating a deppendency between the controller and the directive that shouldn't exist.
If you need input data on your directive, declare it explicitly.
If I have two directives, and need to have one of them use the controller of the other one, what is the best way to accomplish something like that in angular? I'm trying to use the require: otherDirective stuff I see in the docs, but I'm getting an
Error: No controller: dirOne
from the following example. I was under the impression that by specifying dirOne in the require statement, dirTwo would have access to the controller of dirOne. What am I doing wrong here?
Here's a plunkr as well
var app = angular.module('myApp', []);
app.directive('dirOne', function() {
return {
controller: function($scope) {
$scope.options = {
"opt1" : true,
"opt2" : false,
"opt3" : false,
}
this.getOptions = function() {
return $scope.options;
};
}
};
});
app.directive('dirTwo',function() {
return {
require: 'dirOne',
link: function(scope, element, attrs, dirOneCtrl) {
$scope.options = dirOneCtrl.getOptions();
alert($scope.options);
}
};
});
http://plnkr.co/edit/vq7vvz
There were two problems with your plunkr:
In order for a directive to require the controller of another directive, the two directives have to be on the same element, or if you use the ^ notation, the required directive can be on a parent element.
<div dir-one dir-two></div>
Also, in the second directive you called your parameter "scope" but then tried to use it as "$scope".
link: function(scope, element, attrs, dirOneCtrl) {
scope.options = dirOneCtrl.getOptions();
alert(scope.options);
}