I cannot get the parent function call from the isolated scope..The purpose of this code is to create a widget directive which can be used multiple times on the same page... I tried some other option, but doesn't work either. It works using the parent scope.
What am I missing here.
var app = angular.module("winApp", []);
app.controller("winCtrl", function($scope, dataFactory) {
$scope.getData = function() {
dataFactory.get('accounts.json').then(
function(data) {
$scope.items = data;
});
};
});
app.directive("windowSmall", function() {
return {
restrict : 'EA',
replace : 'true',
scope : {
type : '&'
},
transclude: 'true',
templateUrl : 'windowtemplate.html',
link : function(scope, element, attrs) {
element.bind("load", function(){
console.log(attrs.type);
if (angular.equals(attrs.type, 'getData()')) {
scope.active = 'accounts';
console.log(attrs.type);
// scope.getData();
scope.$apply(function() {
scope.$eval(attrs.type);
});
}
});
}
};
});
app.factory('dataFactory', function($http) {
return {
get : function(url) {
return $http.get(url).then(function(resp) {
return resp.data;
});
}
};
});
HTML:
<div ng-app="winApp" ng-controller="winCtrl">
<window-small type = "getData()"> </window-small>
<br> <br>
<!--
<window-small type = "bulletin"> </window-small> -->
You can also use $rootScope for a full proof solution. Due to the fact that an application can have multiple parents but only one $rootScope.
https://docs.angularjs.org/api/ng/service/$rootScope
Replace your link function with :
link : function(scope, element, attrs) {
element.bind("load", function(){
console.log(attrs.type);
if (angular.equals(attrs.type, 'getData()')) {
scope.active = 'accounts';
console.log(attrs.type);
scope.type();
}
});
}
Fiddle : http://jsfiddle.net/X7Fjm/3/
Related
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 am new in angular and stuck in a conceptual problem. I am not able to access "game" service in "helloWorld" directive.
expected = Name : WarCraft
Actual = Name :
Here is my js and html file :
JS code :
var app = angular.module("app",[]);
app.provider("game", function () {
var type;
return {
setType: function (value) {
type = value;
},
$get: function () {
return {
title: type + "Craft"
};
}
};
});
app.config(function (gameProvider) {
gameProvider.setType("War");
});
app.controller("AppCtrl", function ($scope,game) {
$scope.title = game.title;
});
app.directive('helloWorld', ["game",function (game) {
return {
template: 'Name : {{game.title}}'
};
}])
HTML :
<title>Services</title>
<script src="angular.min.js"></script>
<script src="my-file.js"></script>
</head>
<body ng-app="app">
<div ng-controller="AppCtrl">{{title}} </div>
<hello-world></hello-world>
</body>
something like this:
app.directive('helloWorld', ["game",function (game) {
return {
restrict: 'E',
scope: {},
link: function (scope, elem, attrs, ctrl) {
scope.title = game.title;
},
template: 'Name : {{title}}'
};
}])
Access your game service in controller, which will be available in your template. If you need it only in template, inject it only in controller... your link or other function need not know.
app.directive('helloWorld', function () {
return {
controller: function($scope, game) {
$scope.game = game;
},
template: 'Name : {{game.title}}'
};
}])
Here is solution on plunker, to play around.
Just put define variable which will be used by view and set to it needed value.
app.directive('helloWorld', ["game", function(game) {
return {
template: 'Name : {{game.title}}',
link: function(scope) {
scope.game = game;
}
};
}])
The common scenario of requiring a parent controller in a child directive shown here:
the Client directive requires the parent Server controller
<div server>
<div client></div>
</div>
var app = angular.module("app", []);
app.directive("server", function() {
return {
controller: function() {
this.log = function(message) {
console.log(message);
};
}
};
});
app.directive("client", function() {
return {
require: "^server",
link: function($scope, $elem, $attrs, serverCtrl) {
serverCtrl.log("Hello, this is the client!");
}
};
});
What if I have a third directive directive called MegaServer
app.directive("MegaServer", function() {
return {
controller: function() {
this.log = function(message) {
console.log(message);
};
}
};
});
MegaServer can also be a parent to "Client".
<div server>
<div client></div>
</div>
<div mega-server>
<div client></div>
</div>
How do I define my Client directive to require the parent controller if it can be of type either Server or MegaServer?
Have the other server publish its this on its $scope. Then have the client directive use either the optionally required serverCtrl or the $scope.serverCtrl.
angular.module("app").directive("otherServer", function() {
return {
controller: function($scope) {
$scope.serverCtrl = this;
this.log = function(message) {
console.log("otherServer: ", message);
};
}
};
});
angular.module("app").directive("client", function() {
return {
require: "^?server",
link: function(scope, elem, attrs, serverCtrl) {
var ctrl = serverCtrl || scope.serverCtrl;
if (ctrl) {
ctrl.log("Hello, from a client");
ctrl.log("My scope.$id is " + scope.$id);
};
}
};
});
Try it out on JSFiddle.
I have created a global variable in factory. And I have accessed the global variable in my controller but upon changing the value in the directive it is unable to update in the controller.
My directive is
myApp.directive('quiz', function(quizFactory) {
return {
restrict: 'AE',
scope: {},
templateUrl: 'templete.html',
controller: function($scope){
},
link: function(scope, elem, attrs,UserService) {
scope.start = function() {
UserService.var1=true;
}
}
};
My Factory is
myApp.factory('UserService', function() {
return {
var1 : false
};
});
My controller is
myApp.controller('LearnSetQue', ['$scope','UserService',
function($scope,UserService){
$scope.var2=UserService.var1;
$scope.var3=UserService.var1;
}
]);
Here start is button function
<button ng-click="start()">Start</button>
Here upon clicking the start button the var1 should become true var1=true,var2=true and var3=true and how can I update that in the controller.
First in the service, you should return an object with the properties you want to share across your app:
myApp.factory('UserService', function() {
var properties = { var1: false };
return {
getProperties : function() { return properties; }
};
});
Then in the directive
scope.start = function() {
UserService.getProperties().var1=true;
}
And in the controller you should have:
myApp.controller('LearnSetQue', ['$scope','UserService',
function($scope,UserService){
$scope.properties = UserService.getProperties();
]);
And then on the view, just reference the var1 directly
<div>{{ properties.var1 }}</div>
I have two directives, for the sake of simplicity I have reduced my problem to the following:
Directive 01 : handles authentication
it is responsible for opening modals / windows and getting user authenticated.
angular
.module('app.directives')
.directive('requiresLogin', function(){
return {
restrict : 'A',
link : function() { //..}
}
})
Directive 02 : performs specific action
angular
.module('app.directives')
.directive('like', function(){
return {
restrict : 'A',
link : function() { //..}
}
})
Both directive 01 and 02 bind click events.
I am bit confused about the design of two directives.
I could make the second directive a child of the first one and get
communication between the directive, which to some extent makes sense
as the single responsibility of each directive is maintained under
this pattern. However all my future directives that would need
authentication will be children of the first directive.
My Question is :
How can prevent the second directive (actual action) based on the result of first "authenticating" directive ? Is there any other way of doing this without making a "parent-child" relation between them ?
You can use "require" definitly well explained in the following post :
How to require a controller in an angularjs directive
In your context you could do:
.directive('requirelogin', function() {
return {
scope: true,
controller: function() {
var isLogged = false;
this.isLogged = function() {
if(isLogged) return isLogged;
alert("You are not logged!");
};
this.login = function(){
isLogged = true;
};
}
};
})
.directive('like', function() {
return {
scope: true,
require: '^requirelogin',
link: function(scope, element, attrs, loginCtrl) {
scope.loginCtrl = loginCtrl;
scope.sayILike = function(){
if(scope.loginCtrl.isLogged()){
alert('I like');
}
};
scope.login = function(){
scope.loginCtrl.login();
};
}
};
});
Working : http://jsfiddle.net/bennekrouf/nq9g33Lt/25/
Only add the "action" directive and inject an auth service to it:
http://jsfiddle.net/coma/dby686ab/
Logic
app.factory('user', function() {
var roles = ['user'];
return {
hasRole: function(role) {
return !role || roles.indexOf(role) > -1;
}
};
});
app.directive('like', function(user) {
return {
restrict: 'A',
link : function(scope, element, attrs) {
element.on('click', function () {
if (user.hasRole(attrs.authRole)) {
element.addClass('liked');
element.off('click');
} else {
alert('Unauthorized.');
}
});
}
};
});
View
<a like="">Dogs</a>
<a like="" auth-role="user">Birds</a>
<a like="" auth-role="admin">Whales</a>