Trying to unit-test the invokement on ngModelCtrl.$render. Now by $setViewValue but actually I want to set a value on ng-model on the directive and test if $render is invoked.
The Directive:
angular.module('inputValidationBelow', []).directive('inputValidationBelow', function ($compile) {
return {
scope: {
dataModel: '=ngModel',
placeHolder: '#',
identifier: '#',
feedbackMessage: '#',
type: '#'
},
templateUrl: 'scripts/directives/inputValidationBelow/inputValidationBelow.html',
restrict: 'E',
transclude: true,
replace: false,
require: ['ngModel', '^form'],
link: function (scope, element, attrs, controllers) {
scope.type = attrs.type;
var ngModelController = controllers[0];
ngModelController.$render = function () {
if (typeof ngModelController.$viewValue !== 'undefined') {
scope.dataModel = ngModelController.$viewValue;
}
};
}
};
});
The unit test
it('should invoke ngModelCtrl.$render after changing value on ngModel from the outside (the view)', function () {
var scope,
element,
ngModelCtrl,
modelValue;
scope = $rootScope.$new();
element = angular.element('<form name="loginForm"><input-validation-below place-holder="foo" feedback-message="bar" identifier="username" ng-model="login"></input-validation-below></form>');
element = $compile(element)(scope);
scope.$digest();
var ngModelCtrl = element.find('input-validation-below').controller('ngModel');
modelValue = {username: 'SJV'};
ngModelCtrl.$setViewValue(modelValue);
scope.$digest();
expect(scope.dataModel).toEqual(modelValue);
});
Test fails: Expected undefined to equal { username : 'SJV' }
I read this in the Angular docs:
Since ng-model does not do a deep watch, $render() is only invoked if the values of $modelValue and $viewValue are actually different from their previous value. If $modelValue or $viewValue are objects (rather than a string or number) then $render() will not be invoked if you only change a property on the objects.
But this confuses me a bit.
However, I need $render to be invoked ng-model being changed from the outside to make it as real life as possible AND for the code coverage its code-covered.
Question: could use some pointers on this.
Related
In my page controller I get data from an ajax call using ngResource:
clientResource.query(
{
searchText: vm.search.text,
pageSize: vm.pageSize
},
(data, headers) => {
vm.clients = data;
vm.headers = JSON.parse(headers("X-Pagination"))
// ...
I have a directive for the pagination which is simply:
<ix-pager headers="vm.headers"></ix-pager>
In the directive controller, I have:
function ixPagerController($scope) {
var vm = this;
vm.headers = $scope.headers;
}
Now when the directive renders and the directive controller fires, $scope.headers is undefined, which is because the AJAX call hasn't returned yet. But when it does and vm.headers is set, this doesn't update the model on the directive. So I can change my directive to use a link function with a watch statement, like so:
return {
templateUrl: "/app/partials/pager.html",
restrict: "E",
controller: ixPagerController,
controllerAs: "vm",
replace: true,
link: function (scope, element, attrs) {
scope.$watch("headers", function (newValue, oldValue) {
if (newValue) {
//scope.headers = newValue;
}
});
},
scope: {
headers:"="
}
}
The problem is, at the commented out line, if I set a breakpoint, the scope.headers value is ALREADY the correct value, i.e. it has already been set. However, on the directive, template:
<pre>{{vm.headers|json}}</pre>
Still shows nothing. It's almost as if there's a missing digest or something. How do I get the model on the directive to update the view correctly?
This is what seems to work:
link: function (scope, element, attrs, ctrl) {
var c = ctrl;
scope.$watch("headers", function (newValue, oldValue) {
if (newValue) {
c.headers = newValue;
}
});
},
I am trying to do some custom validation based on a json object a user gives me.
However the input field visually does not show the value of the ngModel property. I added a plunkr to illustrate the problem.
'use strict';
angular.module('zendantennesApp')
.directive('validation', function ($compile, $parse) {
return {
scope: {
validation: '#',
ngModel: '#'
},
require: "?ngModel",
restrict: 'A',
compile: function(el, attrs) {
el.removeAttr('validation');
el.attr('ng-blur', 'evaluateExpression()');
el.attr('ng-focus', 'assignOriginalValue()');
var fn = $compile(el);
return function(scope, element, attrs, ngModel){
ngModel.$render = function(){
$(element).val(ngModel.$viewValue);
};
fn(scope);
}
},
controller: function($scope){
$scope.originalValue = $scope.ngModel;
$scope.validationObject = JSON.parse($scope.validation.replace(/'/g, '"'));
$scope.evaluateExpression = function(){
console.log($scope.validationObject);
};
$scope.assignOriginalValue = function(){
$scope.originalValue = $scope.ngModel;
console.log($scope.originalValue);
}
}
}
});
https://plnkr.co/edit/1qYxCiSZWHgVeN9CEpxw?p=preview
validation directive will have isolated scope and hence parent scope value will not be accessible unless you explicitly mention during compile. Replace fn(scope); by fn(scope.$parent);
Updated Plunker
I have made a directive which uses ngModel:
.directive('datetimepicker', function () {
return {
restrict: 'E',
require: ['datetimepicker', '?^ngModel'],
controller: 'DateTimePickerController',
replace: true,
templateUrl: ...,
scope: {
model: '=ngModel'
},
link: function (scope, element, attributes, controllers) {
var pickerController = controllers[0];
var modelController = controllers[1];
if (modelController) {
pickerController.init(modelController);
}
}
}
});
But when testing...
var scope, element;
beforeEach(module('appDateTimePicker'));
beforeEach(module('templates'));
beforeEach(inject(function ($compile, $rootScope) {
compile = $compile;
scope = $rootScope;
scope.model = new Date();
element = compile(angular.element('<datetimepicker ng-model="model"></datetimepicker>'))(scope);
scope.$digest();
}));
I can't anyhow set value to ng-model.
Fo example, here scope.model is a date, so scope.year and scope.month should be date and year of that model, but it is undefined.
As seen in the directive's code, I'm using this.init on the controller to initialise all the process.
What am I missing?
EDIT
Example of test:
it('should test', function () {
expect(scope.model).toBe(undefined);
expect(scope.year).toBe(undefined);
});
EDIT
This helped to solve the problem: http://jsfiddle.net/pTv49/3/
The '?^ngModel' mean you are asking for the ng-model on parent elements, but the html in your test has the ng-model on the same element as datetimepicker directive.
If the ng-model really have to be on parent elements, you have to change the html in the test, for example:
element = compile(angular.element('<div ng-model="model"><datetimepicker></datetimepicker></div>'))(scope);
But if it should be on the same element, just remove the ^ symbol in the require:
require: ['datetimepicker', '?ngModel'],
The directive has a scope: {} block so it creates an isolate scope. I assume, in the tests scope refers to the outer scope, while element.isolateScope() should be used to reference the inner scope instead.
I have the following markup:
<div class="controller" ng-controller="mainController">
<input type="text" ng-model="value">
<div class="matches"
positions="{{client.positions | filter:value}}"
select="selectPosition(pos)">
<div class="match"
ng-repeat="match in matches"
ng-click="select({pos: match})"
ng-bind="match.name">
Then, inside my matches directive I have
app.directive('matches', function()
{
return {
scope: {
select: '&'
},
link: function(scope, element, attrs)
{
scope.matches = [];
attrs.$observe('positions', function(value)
{
scope.matches = angular.fromJson(value);
scope.$apply();
})
}
}
}
When I do this, I can console log scope.matches, and it does change with the value from my input. However, the last div .match doesn't render anything! If I remove scope: {...} and replace it with scope: true, then it does render the result, but I want to use the & evaluation to execute a function within my main controller.
What do i do?
Use scope.$watch instead, you can watch the attribute select whenever changes are made from that attribute.
app.directive('matches', function()
{
return {
scope: true,
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch(attrs.select, function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
UPDATE: Likewise, if you define select itself as a scope attribute, you must use the = notation(use the & notation only, if you intend to use it as a callback in a template defined in the directive), and use scope.$watch(), not attr.$observe(). Since attr.$observe() is only used for interpolation changes {{}}, while $watch is used for the changes of the scope property itself.
app.directive('matches', function()
{
return {
scope: {select: '='},
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch('select', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
The AngularJS Documentation states:
$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest
following compilation. The observer is then invoked whenever the
interpolated value changes.
Not scope properties defined as such in your problem which is defining scope in the directive definition.
If you don't need the isolated scope, you could use $parse instead of the & evaluatation like this:
var selectFn = $parse(attrs.select);
scope.select = function (obj) {
selectFn(scope, obj);
};
Example Plunker: http://plnkr.co/edit/QZy6TQChAw5fEXYtw8wt?p=preview
But if you prefer the isolated scope, you have to transclude children elements and correctly assign the scope of your directive like this:
app.directive('matches', function($parse) {
return {
restrict: 'C',
scope: {
select: '&',
},
transclude: true,
link: function(scope, element, attrs, ctrl, transcludeFn) {
transcludeFn(scope, function (clone) {
element.append(clone);
});
scope.matches = [];
attrs.$observe('positions', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
});
Example Plunker: http://plnkr.co/edit/9SPhTG08uUd440nBxGju?p=preview
I have a directive like below:
angular.module('buttonModule', []).directive('saveButton', [
function () {
function resetButton(element) {
element.removeClass('btn-primary');
}
return {
restrict: 'E',
replace: 'false',
scope: {
isSave: '='
},
template:
'<button class="btn" href="#" style="margin-right:10px;" ng-disabled="!isSave">' +
'</button>',
link: function (scope, element) {
console.log(scope.isSave);
scope.$watch('isSave', function () {
if (scope.isSave) {
resetButton(scope, element);
}
});
}
};
}
]);
and the jasmine test as below:
describe('Directives: saveButton', function() {
var scope, compile;
beforeEach(module('buttonModule'));
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
compile = $compile;
}));
function createDirective() {
var elem, compiledElem;
elem = angular.element('<save-button></save-button>');
compiledElem = compile(elem)(scope);
scope.$digest();
return compiledElem;
}
it('should set button clean', function() {
var el = createDirective();
el.scope().isSaving = true;
expect(el.hasClass('btn-primary')).toBeFalsy();
});
});
The issue is the value of isSaving is not getting reflected in the directive and hence resetButton function is never called. How do i access the directive scope in my spec and change the variable values. i tried with isolateScope but the same issue persists.
First note that you are calling the resetButton function with two arguments when it only accepts one. I fixed this in my example code. I also added the class btn-primary to the button element to make the passing of the test clearer.
Your directive is setting up two-way databinding between the outer scope and the isolated scope:
scope: {
isDirty: '=',
isSaving: '='
}
You should leverage this to modify the isSaving variable.
Add the is-saving attribute to your element:
elem = '<save-button is-saving="isSaving"></save-button>';
Then modify the isSaving property of the scope that was used when compiling (you also need to trigger the digest loop to make the watcher detect the change):
var el = createDirective();
scope.isSaving = true;
scope.$apply();
expect(el.hasClass('btn-primary')).toBeFalsy();
Demo: http://plnkr.co/edit/Fr08guUMIxTLYTY0wTW3?p=preview
If you don't want to add the is-saving attribute to your element and still want to modify the variable you need to retrieve the isolated scope:
var el = createDirective();
var isolatedScope = el.isolateScope();
isolatedScope.isSaving = true;
isolatedScope.$apply();
expect(el.hasClass('btn-primary')).toBeFalsy();
For this to work however you need to remove the two-way binding to isSaving:
scope: {
isDirty: '='
}
Otherwise it would try to bind to something non-existing as there is no is-saving attribute on the element and you would get the following error:
Expression 'undefined' used with directive 'saveButton' is
non-assignable!
(https://docs.angularjs.org/error/$compile/nonassign?p0=undefined&p1=saveButton)
Demo: http://plnkr.co/edit/Ud6nK2qYxzQMi6fXNw1t?p=preview