I have a little SPA using angular. The concept is simple, after login, $routeProvider redirects to a home page where I have a homeController specified.
this is from my home view that is rendered by ng-view while navigating to "/home" :
<my-directive datas=getData()></my-directive>
<ul>
<li ng-repeat="data in datas"> {{data.title}} {{data.content}} </li>
</ul>
my directive is written as:
angular.module('app').directive('myDirective', ['myService', function (myService) {
return {
restrict: "E",
scope: {
data: '='
},
templateUrl: "partials/my-directive.html",
controller: function ($scope) {
$scope.getDatas = function()
{
myService.retData();
}
}
};
}]);
the home controller is:
angular.module('app').controller('homeController', homeController);
homeController.$inject = ['myService', '$scope'];
function homeController(myService, $scope) {
var vm = this;
vm.data = [];
initController();
function initController() {
vm.data = myService.retData();
}
}
and finally my service is
angular.module('app').service('myService', myService);
function myService () {
var data = [
{ id: 1, title: 'Albert Einstein', content: 'Random Content' }
];
return {
retData: function () {
return data;
},
addData: function (title, content) {
var currentIndex = data.length + 1;
data.push({
id: currentIndex,
title: title,
content: content
});
}
};
}
now that i mentioned everything, here comes the problem. the directive is not able to retrieve data from the service. Actually when i run the project in VS2013, myDirective.js is not even loaded. I included all services, directives, controllers etc in the main HTML page.
What is causing this problem?
Does it have something to do with the scope being isolated in the directive?
What is a better approach to sharing data between a controller, directive and service?
I may have made some silly mistakes while rewriting all the code. Please do point them out, however keep in mind my actual issue and what error may be causing that.
Better to use isolated scope to pass data controller to directive.
Html:
<my-directive datas="getData()" data="data"></my-directive>
Directive:
angular.module('app').directive('myDirective', [function () {
return {
restrict: "E",
scope: {
data: '='
},
templateUrl: "partials/my-directive.html",
link: function (scope) {
//Here you got the isolated scope data
var details = scope.data;
}
};
}]);
OR
app.directive('myDirective', function() {
return {
restrict: 'E',
templateUrl: 'partials/my-directive.html',
scope: {
date: '=',
},
controller : ['$scope', 'myService', function($scope, myService) {
myService.retData();
}],
link: function(scope, elem, attr) {
//
}
};
});
Related
I have a directive (DirectiveA), which makes an $http call and creates a new html code.
directiveA
(function(){
angular.module('app').directive('directiveA', directiveA);
})();
(function(){
angular.module('app').controller('DirectiveAController', DirectiveAController);
})();
function directiveA($timeout){
return {
restrict: 'E',
scope: {
url:'#'
},
template: '<div ng-if="template" ng-bind-html="template"></div>',
link: function ( scope, element, attrs ) {
scope.element = element;
},
controller: DirectiveAController
};
}
directiveA.$inject = ['$timeout']
function DirectiveAController($scope, $http, $sce){
$http.get(`${$scope.url}`).then(function(res){
if(res.success){
$scope.template = $sce.trustAsHtml(res.template);
}
});
}
DirectiveAController.$inject = ['$scope', '$http','$sce'];
this works fine.
On the new created element, i want to capture the click function using another directive.
Directive 2
(function(){
angular.module('mcq').directive('captureClick', captureClick);
})();
function captureClick($timeout, $compile){
return {
link: function ( scope, element, attrs ) {
console.log("i am called") // Working on page load but not on dynamic element
scope.element = element;
},
};
}
captureClick.$inject = ['$timeout', '$compile'];
response.template
<button capture-click></button>
Rendered a dummy element of response.template (as static content) and the directive works. How can i get it work on dynamically rendered element.
Using compile my directive is picked up.
(function(){
angular.module('app').directive('directiveA', directiveA);
})();
(function(){
angular.module('app').controller('DirectiveAController', DirectiveAController);
})();
function directiveA($timeout){
return {
restrict: 'E',
scope: {
url:'#'
},
template: '',
link: function ( scope, element, attrs ) {
scope.element = element;
},
controller: DirectiveAController
};
}
directiveA.$inject = ['$timeout']
function DirectiveAController($scope, $http, $sce, $compile){
$http.get(`${$scope.url}`).then(function(res){
if(res.success){
var com = $compile(res.template)($scope);
$scope.element.append(com[0].outerHTML);
}
});
}
DirectiveAController.$inject = ['$scope', '$http','$sce', '$compile'];
Note sure, this is the correct way and has any cons
I am having a hard time trying to figure out how I mock out a required controller for a directive I have written that's the child of another.
First let me share the directives I have:
PARENT
angular
.module('app.components')
.directive('myTable', myTable);
function myTable() {
var myTable = {
restrict: 'E',
transclude: {
actions: 'actionsContainer',
table: 'tableContainer'
},
scope: {
selected: '='
},
templateUrl: 'app/components/table/myTable.html',
controller: controller,
controllerAs: 'vm',
bindToController: true
};
return myTable;
function controller($attrs, $scope, $element) {
var vm = this;
vm.enableMultiSelect = $attrs.multiple === '';
}
}
CHILD
angular
.module('app.components')
.directive('myTableRow', myTableRow);
myTableRow.$inject = ['$compile'];
function myTableRow($compile) {
var myTableRow = {
restrict: 'A',
require: ['myTableRow', '^^myTable'],
scope: {
model: '=myTableRow'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
link: link
};
return myTableRow;
function link(scope, element, attrs, ctrls) {
var self = ctrls.shift(),
tableCtrl = ctrls.shift();
if(tableCtrl.enableMultiSelect){
element.prepend(createCheckbox());
}
self.isSelected = function () {
if(!tableCtrl.enableMultiSelect) {
return false;
}
return tableCtrl.selected.indexOf(self.model) !== -1;
};
self.select = function () {
tableCtrl.selected.push(self.model);
};
self.deselect = function () {
tableCtrl.selected.splice(tableCtrl.selected.indexOf(self.model), 1);
};
self.toggle = function (event) {
if(event && event.stopPropagation) {
event.stopPropagation();
}
return self.isSelected() ? self.deselect() : self.select();
};
function createCheckbox() {
var checkbox = angular.element('<md-checkbox>').attr({
'aria-label': 'Select Row',
'ng-click': 'vm.toggle($event)',
'ng-checked': 'vm.isSelected()'
});
return angular.element('<td class="md-cell md-checkbox-cell">').append($compile(checkbox)(scope));
}
}
function controller() {
}
}
So as you can probably see, its a table row directive that prepends checkbox cells and when toggled are used for populating an array of selected items bound to the scope of the parent table directive.
When it comes to unit testing the table row directive I have come across solutions where can mock required controllers using the data property on the element.
I have attempted this and am now trying to test the toggle function in my table row directive to check it adds an item to the parent table directive's scope selected property:
describe('myTableRow Directive', function() {
var $compile,
scope,
compiledElement,
tableCtrl = {
enableMultiSelect: true,
selected: []
},
controller;
beforeEach(function() {
module('app.components');
inject(function(_$rootScope_, _$compile_) {
scope = _$rootScope_.$new();
$compile = _$compile_;
});
var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');
element.data('$myTableController', tableCtrl);
scope.data = {foo: 'bar'};
compiledElement = $compile(element)(scope);
scope.$digest();
controller = compiledElement.controller('myTableRow');
});
describe('select', function(){
it('should work', function(){
controller.toggle();
expect(tableCtrl.selected.length).toEqual(1);
});
});
});
But I'm getting an error:
undefined is not an object (evaluating 'controller.toggle')
If I console log out the value of controller in my test it shows as undefined.
I am no doubt doing something wrong here in my approach, can someone please enlighten me?
Thanks
UPDATE
I have come across these posts already:
Unit testing a directive that defines a controller in AngularJS
How to access controllerAs namespace in unit test with compiled element?
I have tried the following, given I'm using controllerAs syntax:
var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
element.data('$actTableController', tableCtrl);
$scope.data = {foo: 'bar'};
$compile(element)($scope);
$scope.$digest();
console.log(element.controller('vm'));
But the controller is still coming up as undefined in the console log.
UPDATE 2
I have come across this post - isolateScope() returning undefined when testing angular directive
Thought it could help me, so I tried the following instead
console.log(compiledElement.children().scope().vm);
But still it returns as undefined. compiledElement.children().scope() does return a large object with lots of angular $$ prefixed scope related properties and I can see my vm controller I'm trying to get at is buried deep within, but not sure this is the right approach
UPDATE 3
I have come across this article which covers exactly the kind of thing I'm trying to achieve.
When I try to implement this approach in my test, I can get to the element of the child directive, but still I am unable to retrieve it's scope:
beforeEach(function(){
var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
element.data('$actTableController', tableCtrl);
$scope.data = {foo: 'bar'};
compiledElement = $compile(element)($scope);
$scope.$digest();
element = element.find('act-table-row');
console.log(element);
console.log(element.scope()); //returns undefined
});
I just wonder if this is down to me using both a link function and controllerAs syntax?
You were very close with the original code you'd posted. I think you were just using .controller('myTableRow') on the wrong element, as your compiledElement at this point was the whole table element. You needed to get a hold of the actual tr child element in order to get the myTableRow controller out of it.
See below, specifically:
controller = compiledElement.find('tr').controller('myTableRow');
/* Angular App */
(function() {
"use strict";
angular
.module('app.components', [])
.directive('myTableRow', myTableRow);
function myTableRow() {
return {
restrict: 'A',
require: ['myTableRow', '^^myTable'],
scope: {
model: '=myTableRow'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
link: link
};
function link($scope, $element, $attrs, $ctrls) {
var self = $ctrls.shift(),
tableCtrl = $ctrls.shift();
self.toggle = function() {
// keeping it simple for the unit test...
tableCtrl.selected[0] = self.model;
};
}
function controller() {}
}
})();
/* Unit Test */
(function() {
"use strict";
describe('myTableRow Directive', function() {
var $compile,
$scope,
compiledElement,
tableCtrl = {},
controller;
beforeEach(function() {
module('app.components');
inject(function(_$rootScope_, _$compile_) {
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
tableCtrl.enableMultiSelect = true;
tableCtrl.selected = [];
var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');
element.data('$myTableController', tableCtrl);
$scope.data = {
foo: 'bar'
};
compiledElement = $compile(element)($scope);
$scope.$digest();
controller = compiledElement.find('tr').controller('myTableRow');
//console.log(controller); // without the above .find('tr'), this is undefined
});
describe('select', function() {
it('should work', function() {
controller.toggle();
expect(tableCtrl.selected.length).toEqual(1);
});
});
});
})();
<link rel="stylesheet" href="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.css" />
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine-html.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/boot.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular-mocks.js"></script>
Here is an example to quote the use of angular directives using the parent child relationship.
The definition of annotated-image looks like this:(which is the parent)
angular.module('annotatedimage').directive('annotatedImage', function() {
function AnnotatedImageController(scope) {}
return {
{
restrict: 'E',
template: [
'<annotated-image-controls annotations="configuration.annotations"></annotated-image-controls>',
'<annotated-image-viewer src="configuration.image" annotations="configuration.annotations"></annotated-image-viewer>',
'<annotated-image-current></annotated-image-current>'
].join('\n'),
controller: ['$scope', AnnotatedImageController],
scope: {
configuration: '='
}
}
};
});
Now for the annotatedImageController , annotatedImageViewer and the annotatedImageCurrent which are the children.
angular.module('annotated-image').directive('annotatedImageControls', function() {
function link(scope, el, attrs, controller) {
scope.showAnnotations = function() {
controller.showAnnotations();
};
controller.onShowAnnotations(function() {
scope.viewing = true;
});
}
return {
restrict: 'E',
require: '^annotatedImage',
template: [
'<div>',
'<span span[data-role="show annotations"] ng-click="showAnnotations()" ng-hide="viewing">Show</span>',
'<span span[data-role="hide annotations"] ng-click="hideAnnotations()" ng-show="viewing">Hide</span>',
'<span ng-click="showAnnotations()">{{ annotations.length }} Annotations</span>',
'</div>'
].join('\n'),
link: link,
scope: {
annotations: '='
}
};
});
angular.module('annotated-image').directive('annotatedImageViewer', function() {
function link(scope, el, attrs, controller) {
var canvas = el.find('canvas');
var viewManager = new AnnotatedImage.ViewManager(canvas[0], scope.src);
controller.onShowAnnotations(function() {
viewManager.showAnnotations(scope.annotations);
});
}
return {
restrict: 'E',
require: '^annotatedImage',
template: '<canvas></canvas>',
link: link,
scope: {
src: '=',
annotations: '='
}
};
});
The same can be done for the annotatedImageCurrent
Summary
<parent-component>
<child-component></child-component>
<another-child-component></another-child-component>
</parent-component>
Parent Component
module.directive('parentComponent', function() {
function ParentComponentController(scope) {
// initialize scope
}
ParentComponentController.prototype.doSomething = function() {
// does nothing here
}
return {
restrict: 'E',
controller: ['$scope', ParentComponentController],
scope: {}
};
});
Child Component
module.directive('childComponent', function() {
function link(scope, element, attrs, controller) {
controller.doSomething();
}
return {
restrict: 'E',
require: '^parentComponent',
link: link,
scope: {}
}
});
I wrote a plunker to see how to use bindToDirective to isolate scopes and using directive controller to call main controller function, but, I am doing something wrong. Could you suggest?
This is the plunker: http://plnkr.co/edit/UJLjTmIiHydHr8qRzAsX?p=preview
Code sample:
.controller('Ctrl', function() {
var self = this;
self.func = function() {
console.log('In the controller function');
};
})
.directive('myDirective', [ function() {
var self = {};
self.link = function (scope, elem, attrs, ctrl) {
elem.bind('click', function () {
ctrl.ctrlFunc();
});
elem.addClass('fa fa-file-excel-o fa-lg');
};
return {
restrict: 'E',
scope: {},
controller: function () {
},
controllerAs: 'DirCtrl',
bindToController: {
ctrlFunc: '&'
},
link: self.link
};
}])
html sample to associate main controller function to directive:
<div ng-controller="Ctrl">
<my-directive ctrlfunc="Ctrl.func()"></my-directive>
</div>
You have a number of issues:
You need a hyphen in your directive argument name and you should be passing the function reference, not calling the function directly (with params):
<my-directive ctrl-func="ctrl.func"></my-directive>
Second, you are using alias syntax in your controller (var self = this;), but not in your template. You need to update it to the following:
<div ng-controller="Ctrl as ctrl">
<my-directive ctrl-func="ctrl.func"></my-directive>
</div>
Finally, pass down the function reference with two-way binding instead of with & since that passes down values for implicit evaluation.
bindToController: {
ctrlFunc: '='
},
See working plunkr
I'm not sure you need bindToController...
This version calls your Ctrl's function: http://plnkr.co/edit/Rxu5ZmmUAU8p63hR2Qge?p=preview
JS
angular.module('plunker', [])
.controller('Ctrl', function($scope) {
$scope.func = function() {
console.log('In the controller function');
};
}) angular.module('plunker', [])
.controller('Ctrl', function($scope) {
$scope.func = function() {
console.log('In the controller function');
};
})
.directive('myDirective', [ function() {
return {
template: '<pre>[clickme]</pre>',
replace: true,
restrict: 'E',
scope: {
target: '&'
},
link: function (scope, elem, attrs) {
elem.bind('click', function () {
var fn = scope.target && scope.target(scope);
fn && fn();
});
elem.addClass('fa fa-file-excel-o fa-lg');
}
};
}])
HTML
<div ng-controller="Ctrl">
<my-directive target="func"></my-directive>
</div>
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.
I have just came up with a directive that loads a dropdown box according to a list coming from an API call ($resource).
Controller:
App.controller(
'TestCtrl', [
'$scope', 'countriesFactory',
function($scope, countriesFactory){
/* Call API */
countriesFactory().then(function(data){
$scope.countryList = data;
});
}])
The API call returns:
{"country":[{"code":"ABW","label":"Aruba"},{"code":"AFG","label":"Afghanistan"},{"code":"AGO","label":"Angola"}]}
Template:
<input-select model-ref-list="countryList"></input-select>
Directive:
App
.directive("inputSelect"
, function() {
var Template =
'<select ng-options="item.label for item in modelRefList" required></select>';
return {
restrict: 'EA',
template: Template,
scope: {
modelRefList: '='
},
link: function(scope){
console.log(scope.modelRefList);
}
};
}
);
First of all: I simplified a lot the overall issue, so that it looks that the directive is completely overkill in that situation, but in the end, it is not :D.
Problem: My console.log is always undefined.
I made a bit of research and realized that I needed to play with promises to wait for my country list to appear to be actually given to the directive.
So I tried modifying my controller and not use the result of the API call promise, but directly the resource itself:
New Controller:
App.controller(
'TestCtrl', [
'$scope', 'countriesFactory',
function($scope, countriesFactory){
/* Call API */
$scope.countryList = resourceAPICall();
}])
But still undefined :/.
How can I pass direclty the resource (containing the promise I can then use to defer the load of the select) to the directive?
SOLUTION FOR ANGULARJS 1.2:
Directive:
App
.directive("inputSelect"
, function() {
var Template =
'<select ng-options="item.label for item in modelRefList" required></select>';
return {
restrict: 'EA',
template: Template,
scope: {
modelRefList: '='
},
link: function(scope){
scope.modelRefList.$promise.then(function(data){
console.log(data);
}
};
}
);
To pass a API call result to a directive, you need to pass its resource and play with its promise inside the directive itself.
Thanks everybody for the help.
Here we simulated async call factory by using wrapper with $q.
We changed modelReflist to modelRefList
added ng-model="item" to template
HTML
<div ng-controller="TestCtrl">
<input-select model-ref-list="countryList"></input-select>
</div>
JS
var App = angular.module('myModule', ['ngResource']);
App.controller(
'TestCtrl', [
'$scope', 'countriesFactory',
function ($scope, countriesFactory) {
/* Call API */
countriesFactory.resourceAPICall().then(function (data) {
$scope.countryList = data.country;
console.log($scope.countryList);
});
}])
App.$inject = ['$scope', 'countriesFactory'];
App.directive("inputSelect", function () {
var Template = '<select ng-model="item" ng-options="item.label as item.label for item in modelRefList" required></select>';
return {
restrict: 'EA',
template: Template,
scope: {
modelRefList: '='
},
link: function (scope) {
console.log(scope.countryList);
}
};
});
App.factory('countriesFactory', ['$resource', '$q', function ($resource, $q) {
var data = {
"country": [{
"code": "ABW",
"label": "Aruba"
}, {
"code": "AFG",
"label": "Afghanistan"
}, {
"code": "AGO",
"label": "Angola"
}]
};
var factory = {
resourceAPICall: function () {
var deferred = $q.defer();
deferred.resolve(data);
return deferred.promise;
}
}
return factory;
}]);
Demo Fiddle
modelReflist needs to be fully camel-cased in your directive scope. modelRefList.