Angular.js directive not responding on changes on attributes - angularjs

I have searched and searched, tried and tried but nothing seems to work: I have an element directive with some attributes e.g width="width" and I'm changing this attributes in the controller on resize event on window, I have set the binding as "=" but if I watch for 'width' it works once but then it doesn't, I have tried the watch with true, tried observe on attrs, tried changing bindings, nothing works, any ideas?
Maybe I should call digest or apply?
function Ctrl1($scope, $window) {
$scope.width = $window.innerWidth;
$scope.height = $window.innerHeight;
angular.element($window).bind('resize', function () {
$scope.width = $window.innerWidth;
$scope.height = $window.innerHeight;
console.log($scope.width, $scope.height);
});
$scope.name = 'angular';
$scope.counter = 0;
$scope.myClick = function() {
$scope.height++;
$scope.width++;
}
}
angular.module('myApp', []).directive('myElement', function() {
return {
restrict: 'E',
scope: {
'width': '=',
'width': '='
},
template: '<button ng-click="myOtherClick()">From Directive</button>',
link: function(scope, iElement, iAttrs) {
scope.myOtherClick = function() {
scope.width++;
scope.height++;
}
}
}
});
http://jsfiddle.net/2y1brpzf/

You can use these two solutions:
1. In your directive, set:
scope: {
width: '#',
},
and then do
attrs.$observe('width', function(passedId) {
2.
Use your attribute as a function, in directive:
$scope.$watch(function() {
return element.attr('width');//value to be watched;
}, function watchCallback(newVal, oldVal) {
(..)
}
These two work fine in angular 1.2.6.

I've solved it adding $scope.$digest(); after updating width and height in controller:
$scope.$digest();
http://jsfiddle.net/2y1brpzf/1/

Related

How to make my own ng-change and ng-model in a directive?

I am trying to implement a directive with its own model and change attribute (as an overlay for ng-model and ng-change). It works apparently fine but when the function of the father scope is executed and some variable of the scope is modified in it, it is delayed, the current change is not seen if not the one executed in the previous step.
I have tried adding timeouts, $apply, $digest ... but I can not get it synchronized
angular.module('plunker', []);
//Parent controller
function MainCtrl($scope) {
$scope.directiveValue = true;
$scope.textValue = "init";
$scope.myFunction =
function(){
if($scope.directiveValue === true){
$scope.textValue = "AAAA";
}else{
$scope.textValue = "BBBB";
}
}
}
//Directive
angular.module('plunker').directive('myDirective', function(){
return {
restrict: 'E',
replace: true,
scope: {
myModel: '=model',
myChange: '&change'
},
template: '<span>Check<input ng-model="myModel" ng-change="myChange()"
type="checkbox"/></span>',
controller: function($scope) {
},
link: function(scope, elem, attr) {
var myChangeAux = scope.myChange;
scope.myChange = function () {
setTimeout(function() {
myChangeAux();
}, 0);
};
}
});
// Html
<body ng-controller="MainCtrl">
<my-directive model="directiveValue" change="myFunction()"></my-directive>
<div>Valor model: {{directiveValue}}</div>
<div>Valor texto: {{textValue}}</div>
</body>
The correct result would be that the "myFunction" function runs correctly
Example: https://plnkr.co/edit/q3IqRCIhwLChlGrkDxyO?p=preview
You should use AngularJS' $timeout which is a wrapper for the browser default setTimeout and internally calls setTimeout as well as $digest, all at the right time in the execution.
Your directive code should change as such:
angular.module('plunker').directive('myDirective', function($timeout){
return {
restrict: 'E',
replace: true,
scope: {
myModel: '=model',
myChange: '&change'
},
template: '<span>Check<input ng-model="myModel" ng-change="myChange()" type="checkbox"/></span>',
controller: function($scope) {
},
link: function(scope, elem, attr) {
var myChangeAux = scope.myChange;
scope.myChange = function () {
$timeout(myChangeAux, 0);
};
}
};
});
Docs for AngularJS $timeout

Mocking out required controllers in directive tests

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: {}
}
});

Child directive template not updating after scope change

I have a custom directive with children directives:
<rp-nav>
<rp-nav-item cat="1"></rp-nav-item>
<rp-nav-item cat="2"></rp-nav-item>
<rp-nav-item cat="3"></rp-nav-item>
<rp-nav-item cat="4"></rp-nav-item>
<rp-flyout></rp-flyout>
</rp-nav>
Here are the modules I have defined:
var app = angular.module('app', []);
app.directive('rpNav', function() {
return {
restrict: 'E',
controller: function($scope) {
$scope.currentItem = 'none'; //initialize currentItem
this.setCurrentItem = function(itemId) {
$scope.currentItem = itemId;
}
},
};
});
app.directive('rpNavItem', function() {
return {
restrict: 'E',
template: function(el, attrs) {
return '<p>item {{currentItem}} ' + attrs.cat;
},
require: '^rpNav',
link: function(scope, el, attrs, nav) {
el.on('click', function() {
nav.setCurrentItem(attrs.cat);
});
}
};
});
app.directive('rpFlyout', function() {
return {
restrict: 'E',
template: '<p style="background-color: lightblue">{{currentItem}}</p>'
};
});
The idea is to click in any of the items and make the rp-flyout element display information about the clicked rp-nav-item. The scope variable currentItem does change on click, but the template in rp-flyout does not update. What am I missing to achieve this goal? And, is this a "best practice" way of tackling this problem.
Here's a plunker
To expand on the comment, directives are not inherently part of the digest cycle, so you need to add scope.$apply() inside your el.click handler to trigger a digest cycle and update template bindings.
el.on('click', function() {
nav.setCurrentItem(attrs.cat);
scope.$apply();
});

AngularJS - cannot access directive controller in another directive

I have two angularjs directives (extWindow and taskBar) and want to inject taskBar's controller into extWindow in order to access it's scope. Because they don't share the same scope I used
require : '^$directive'
syntax to include it.
Doing so I could get rid of the error 'Controller 'taskBar', required by directive 'extWindow', can't be found!' but TaskBarCtrl is still undefined in link(..) method of the extWindow directive.
Any suggestions how to fix it?
var mod = angular.module('ui', [])
.directive('taskBar', function() {
var link = function(scope, el, attrs) {
$(el).css('display', 'block');
$(scope.titles).each(function(i,t) {
el.append('<span>' + t + '</span>')
});
};
return {
scope: {},
restrict : 'E',
controller: function($scope, $element, $attrs) {
$scope.titles = [];
this.addTitle = function(title) {
$scope.titles.push(w);
};
this.removeTitle = function(title) {
$scope.titles = jQuery.grep(function(n,i) {
return title != n;
});
}
},
link: link
};
}).directive('extWindow', function() {
return {
scope: {},
require: '^?taskBar',
restrict: 'E',
transclude: true,
template: '<div class="ui-window">\
<div class="ui-window-header"><span>{{windowTitle}}</span><div class="ui-window-close" ng-click="close()">X</div></div>\
<div class="ui-window-content" ng-transclude></div>\
</div>',
link: function(scope, element, attrs, taskBarCtrl) {
scope.windowTitle = attrs['windowTitle'];
scope.close = function() {
$(element).css('display', 'none');
}
//taskBarCtrl is not recognized!!!
taskBarCtrl.addTitle(scope.windowTitle);
}
}
});
http://jsfiddle.net/wa9fs2nm/
Thank you.
golbie.
If you have a controller for your parent directive and you need something like.
this.scope = $scope;
this.attrs = $attrs;
And in your in you link function for the child you need something like
var Ctrl = ctrl || scope.$parent.tBarCtrl;
Here's a Plunker

Directive template value not updating

I'm setting up a directive like so (timeout function just as a demo):
app.directive('timeRange', function () {
return {
restrict: 'A',
scope: {
sliderId: "#sliderId",
},
template: '<div id="{{sliderId}}"></div>'+
'<p>From: <span>{{fromVal}}</span>'+
'<span style="float:right;">{{toVal}}</span><span style="float:right;">To: </span>'+
'</p>',
link: function (scope, elem, attrs) {
scope.sliderId = 'NewId';
scope.fromVal = '06:00';
scope.toVal = '17:00';
setTimeout(function(){
scope.fromVal = 'Hello';
log("Changed");
}, 2000);
},
};
});
When the timeout function runs, the HTML doesn't update, the value stays at 06:00. How do I get the template to update when the variable does? Do I need to somehow link it in the scope section where I link the attribute?
The only issue I can see with your example is that you are using setTimeout instead of the $timeout service. Any time you change angular or scope variables this way you are going to have to manually call $scope.$apply() which is what the $timeout service does for you. The following code works better then:
app.directive('timeRange', function ($timeout) {
return {
restrict: 'A',
scope: {
sliderId: "#sliderId",
},
template: '<div id="{{sliderId}}"></div>'+
'<p>From: <span>{{fromVal}}</span>'+
'<span style="float:right;">{{toVal}}</span><span style="float:right;">To: </span>'+
'</p>',
link: function (scope, elem, attrs) {
scope.sliderId = 'NewId';
scope.fromVal = '08:00';
scope.toVal = '17:00';
$timeout(function(){
scope.fromVal = 'Hello';
console.log("Changed");
}, 2000);
},
};
});
Note the injection of $timeout and the way it is used.
See This In Plunker: http://plnkr.co/edit/KlRAeg6cVhehw2EhWzCK?p=preview
Fixing The Original...
The fixed original code looks like (just a snippet):
setTimeout(function(){
scope.fromVal = 'Hello';
scope.$apply();
console.log("Changed");
}, 2000);
Best of luck!

Resources