How to test dynamically named inputs inside ng-repeat using $setViewValue() - angularjs

I am trying to test a directive that dynamically adds form inputs to a page using ng-repeat. The code runs fine in the browser but trying to test it with Jasmine I discovered what seems (to me) to be a bug or at least weird behaviour in Angular.
I'd expect to be able to set the view value on an input using
form.questions.answer1.$setViewValue();
but in my tests when I console log the form.questions object I get this:
form.questions.answer{{ question.questionId }}
i.e. The index of the object hasn't been parsed (although the html is output correctly).
Is there any other way of triggering the ng-change event? I have tried setting the value of the input using jQuery (inside my test) but although it successfully changes the value it doesn't fire off the ng-change event.
plunker (check the contents of your console to see what I mean.).
My code:
<!-- language: lang-js -->
app.directive('repeatedInputs', function(){
var template ='<div ng-form name="questions">'+
'<div ng-repeat="(key, question) in questions" >' +
'<span id="question{{ question.questionId }}">{{ question.questionText }}</span>'+
'<span><input type="text" name="answer{{ question.questionId }}"' +
' id="answer{{question.questionId}}"' +
' ng-model="question.answer" ng-change="change()"/></span>' +
'</div>' +
'</div>';
return {
template: template,
scope: {
answers: '=',
singleAnswer: '='
},
/**
* Links the directive to the view.
*
* #param {object} scope
* Reference to the directive scope.
*
* #param {object} elm
* Reference to the directive element.
*/
link: function (scope, element) {
scope.questions = [
{
questionId: '1',
questionText: 'What is your name?',
answer: null
},
{
questionId: '2',
questionText: 'What is your quest?',
answer: null
},
{
questionId: '3',
questionText: 'What is your favourite colour?',
answer: null
}
];
scope.change = function () {
for (var i in scope.questions) {
scope.answers[i] = scope.questions[i].answer;
}
};
}
};
});
Here is my spec file:
<!-- language: lang-js -->
describe('repeating inputs directive', function () {
var element, scope, $compile;
beforeEach(function(){
module('plunker');
inject(function ($rootScope, _$compile_) {
scope = $rootScope.$new();
scope.theAnswers = [];
scope.singleAnswer = null;
element = angular.element(
'<form name="form">'
+'<div repeated-inputs answers="theAnswers" single-answer="singleAnswer">'
+'</div></form>'
);
$compile = _$compile_;
$compile(element)(scope);
scope.$apply();
})
});
it('should store the input from the answers in the parent scope',
function () {
// I want to do this
//scope.form.questions.answer1.$setViewValue('Ben');
// but inside the object, the answers name field is not being parsed
// I am expecting the path to the answer to look like this:
// scope.form.questions.answer1
// instead it looks like this:
// scope.form.questions.answer{{ question.questionId }}
console.log(scope.form.questions);
expect(scope.theAnswers[0]).toEqual('Ben');
});
});

So I have found out that what I was trying to do is currently impossible in Angular. The only way to test this is using a web browser - I have a protractor test for this piece of functionality now.
Interpolated names (for ngModel) are not currently supported.

You have to do:
elementScope = $compile(element)(scope).isolateScope();
...
expect(elementScope.theAnswers[0]).toEqual('Ben');
Since an isolated scope will be created for the directive due to:
scope: {
answers: '=',
singleAnswer: '='
}

Related

Angular 1.2: custom directive's dynamic content: ng-click doesn't work

I am trying to do a very simple thing in Angular 1.2: I want to create dynamic content for my custom directive, and add a click handler (clickCustomer) to parts of it. However, when I do that in the following pattern, whilst the clickCustomer function is available on the element's scope, it is not invoked when clicking on it. I'm gussing I need to get Angular to compile the dynamic content, but I'm not sure if that's actually the case, and if it is, how to do so.
'use strict';
angular.module('directives.customers')
.directive('customers', function () {
return {
restrict: 'A',
replace: true,
template: '<div class="customers"></div>',
controller: function ($scope, $element) {
var customers = ['Customer1', 'Customer2', 'Customer3'];
var customersMapped = customers.map(function (customer) {
return '<span ng-click="clickCustomer()" data-customer="' + customer + '">' + customer + '</span>';
});
var text = customersMapped.join(', ');
$element.html(text);
$scope.clickCustomer = function (event) {
console.log('Customer clicked', event);
}
}
};
});
You're right, you need to use the $compile service and compile the attached DOM elements so Angular will set up the events and scopes.
Check this fiddle.

Angularjs form $error is not getting updated when the model is updated inside a directive unless the model be cleared

I can't figure out what's happening in the following example. I just trying to create my own required validation in my own directive where I have an array and I want to make it required (it's a simplification of what I want to do but enough to show the point)
Fiddler: http://jsfiddle.net/gsubiran/p3zxkqwe/3/
angular.module('myApp', [])
.directive('myDirective', function($timeout) {
return {
restrict: 'EA',
require: 'ngModel',
controller: 'myDirectiveController',
controllerAs: 'D_MD',
link: function(scope, element, attrs, ngModel) {
ngModel.$validators.required = function(modelValue) {
var result = false;
if (modelValue && modelValue.length > 0)
result = true;
return result;
};
},
bindToController: {
ngModel: '='
},
template: '(<span>ArrayLength:{{D_MD.ngModel.length}}</span>)<br /><input type=button value="add (inside directive)" ng-click=D_MD.AddElem() /><br /><input value="clear (inside directive)" type=button ng-click=D_MD.Clear() />'
}; }) .controller('myDirectiveController', [function() {
var CTX = this;
//debugger;
//CTX.ngModel = "pipo";
CTX.clearModel = function() {
CTX.ngModel = [];
};
CTX.AddElem = function() {
CTX.ngModel.push({
Name: 'obj100',
Value: 100
});
};
CTX.Clear = function() {
CTX.ngModel = [];
}; }]) .controller('MainCtrl', function($scope) {
var CTX = this;
CTX.patito = 'donde esta el patito';
CTX.arrayElements = [];
CTX.setElements = function() {
CTX.arrayElements = [{
Name: 'obj0',
Value: 0
}, {
Name: 'obj1',
Value: 1
}, {
Name: 'obj2',
Value: 2
}];
};
CTX.clearElements = function() {
CTX.arrayElements = [];
}; })
When I hit the add (outside directive) button, the required works fine,
but when I hit the add (inside directive) button I still getting the required error in the form (the form is defined outside directive).
But the more confusing thing for me is the following:
When I hit the clear (inside directive) button after hitting add (outside directive) button to make the required error go out, in this case the form is updating and the validation error is showing.
Why $validations.required is not firing inside the directive when I add new element to array but yes when I clear it?
Any ideas?
******* UPDATE *******
It seems to be related with array.push if I change array.push with the assignation of new array with wanted elements inside it works ok.
Still the question why it is happening.
As workaround I changed in the directive the AddElem function in this way:
CTX.AddElem = function() {
CTX.ngModel = CTX.ngModel.concat({
Name: 'obj100',
Value: 100
});
};
The ngModel you use here is a JS object. Angular has a reference to that object in its $modelValue and $viewValue (because angular basically does $viewValue = $modelValue). The $modelValue is the actual value of the ngModel, which, if you change it, will change the $viewValue after having run $validators.
To know if your ngModel has Changed, angular compares the ngModel.$viewValue with the ngModel.$modelValue. Here, you are doing a push() to the $viewValue which is updating the $modelValue at the same time because they are just references of each other. Therefore, when comparing them, they have the same value ! Which is why angular does not run your $validator.
The docs explain it :
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 values. 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.
If I over-simplify angular code, this snippet explains it:
var myArray = [];
var ngModel = {
$viewValue: myArray,
$modelValue: myArray,
$validate: function () { console.log('validators updated'); }, // log when validators are updated
}
function $apply() { // the function that is called on the scope
if (ngModel.$viewValue !== ngModel.$modelValue) {
ngModel.$viewValue = ngModel.$modelValue;
ngModel.$validate(); // this will trigger your validator
} else {
console.log('value not changed'); // the new value is no different than before, do not call $validate
}
}
// your push is like doing :
ngModel.$viewValue.push(12);
$apply(); // will output 'value not changed', because we changed the view value as well as the model value
// whereas your should do:
var newArray = [];
// create a copy of the array (you can use angular.copy)
for (var i = 0; i < myArray.length; i++) {
newArray.push(myArray[i]);
}
ngModel.$viewValue.push(12);
ngModel.$viewValue = newArray; // here we clearly update the $viewValue without changing the model value
$apply(); // will output 'validators updated'
Of course you are not forced to do an array copy. Instead, you can force the update of your ngModel. This is done by calling ngModel.$validate();
One way of doing it would be to add a forceUpdate() function in your scope, and call it from the controller after you do a push();
Example: http://jsfiddle.net/L7Lxkq1f/

Angular directive injecting DOM element with click event

Content edited
The goal is to create a directive that can be attached to a textbox that, when the textbox has focus, an image/button will appear after the textbox and the image/button click event will fire a function contained within the directive. The goal is for this functionality to be entirely self-contained in the directive so it can be easily deployed in many pages or apps.
The image/button appears after the textbox with no problem but the click event of the button does not fire the function. I have created a plunkr with the example code.
In the plunk, line 15 defines a function called 'search,' which does nothing more than fire an alert. When the textbox has focus, the button appears as expected and line 34 calls the search function successfully, which means the function itself is working. However, the button's click event doesn't fire the search function.
Original post
I'm trying to recreate some functionality in our apps that is currently being accomplished with jQuery. The functionality involves attaching a pseudo-class to a textbox which is then picked up by jQuery and an image of a magnifying glass is injected into the DOM immediately after the textbox. Clicking on the image causes a dialog box to pop open.
What I've accomplished so far is a simple html page, a simple controller, and a simple directive. When the textbox has focus, the image appears as expected. However, the ng-click directive does not fire.
Here's the html:
<input
id="txtAlias"
type="text"
ng-model="pc.results"
user-search />
</div>
Here is the controller:
angular
.module('app')
.controller('PeopleController', PeopleController);
PeopleController.$inject = ['$http'];
function PeopleController() {
var pc = this;
pc.results = '';
pc.search = function () {
alert('test');
};
}
And this is the directive:
angular
.module('app')
.directive('userSearch', userSearch);
function userSearch($compile) {
return {
restrict: 'EAC',
require: 'ngModel',
//transclude: true,
scope: {
//search : function(callerid){
// alert(callerid);
//}
},
template: "The user's alias is: <b><span ng-bind='pc.results'></span>.",
//controller: UserSearchController,
link: function (scope, element, attrs) {
element.bind('focus', function () {
//alert(attrs.id + ' || ' + attrs.userSearch);
var nextElement = element.parent().find('.openuserdialog').length;
if (nextElement == 0) {
var magnifyingglass = $compile('<img src="' + homePath + 'Images/zoomHS.png" ' +
'alt="User Search" ' +
'ng-click="pc.search("' + attrs.id + '")" ' +
'class="openuserdialog">')(scope);
element.after(magnifyingglass);
}
});
}
};
};
For the time being, I'd be happy to get an alert to fire by either hitting pc.search in the controller or by search in the isolated scope. So far, neither has worked. I'm sure it's something simple that's missing but I can't figure out what.
Solution
Thanks to a user over at the Google forum for showing me the controllerAs property for directives. This version now works perfectly:
angular
.module('app')
.directive('userSearch', userSearch);
function userSearch($compile){
return {
controller: function ()
{
this.search = function () {
alert('Test');
};
},
link: function (scope, element, attrs) {
element.bind('focus', function () {
var nextElement = element.parent().find('.openuserdialog').length;
if (nextElement === 0) {
var btn = '<img src="' + homePath + 'Images/zoomHS.png" ' +
'ng-click="userSearch.search()" ' +
'class="openuserdialog" />';
element.after($compile(btn)(scope));
}
});
},
controllerAs: 'userSearch'
};
};
You are using isolated scope in your directive which means it don't have access to its parent scope. So in this case you need to pass your method reference explicitly to directive. Passed method reference to your directive scope by new variable inside a isolated scope of directive.
Markup
<input id="txtAlias"
type="text" ng-model="pc.results"
user-search search="search(id)" />
scope: {
search: '&'
}
As you don't have access to parent scope, you can't use controller alias over there like you are using pc.. Simply do use following without alias. So directive will bind those variables from directive scope directly.
template: "The user's alias is: <b><span ng-bind='results'></span>.",
Also change compile template to
if (nextElement == 0) {
var magnifyingglass = $compile('<img src="' + homePath + 'Images/zoomHS.png" ' +
'alt="User Search" ' +
'ng-click="search({id: ' + attrs.id + '})' +
'class="openuserdialog">')(scope);
element.after(magnifyingglass);
}
Rather I'd love to have the compiled template as part of template of directive function only. And I'll show and hide it based on ng-if="expression" directive.
Relative answer
Rather than trying to inject into the DOM, and then trying to hook up to that thing you just injected, wrap both the input and the search button/icon in a directive. You can use an isolated scope and two-way binding to hook up both the input and the button:
HTML:
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<search-box data="name" search-function="search"></search-box>
</body>
Here's both a controller and a directive that demonstrate this. Note the "=" in the isolated scope, creating a two-way binding to the corresponding attributes when the directive is used in a template.
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.search= function() { alert('search clicked'); }
});
app.directive('searchBox', function() {
return {
restrict: 'E',
scope: {
searchFunction: '=',
data: '=',
},
template: '<input ng-model="data" /><button ng-click="searchFunction()">Search</button>'
}
})
You should be able to easily replace the button element with an img or whatever else your heart desires.
Here's a plunk with an alert() for the search box, and where typing in the text box in the directive affects the corresponding property of the controller scope:
http://plnkr.co/edit/0lj4AmjOgwNZ2DJMSHDj

AngularJS: Can't get access value of isolate scope property in directive

Problem:
In a directive nested within 3 ui-views, I can't access the values of my isolate scope. scope.values returns {} but when I console.log scope I can see all the values on the values property.
In a different app I can make this works and I converted this one to that method as well but it still doesn't work and I'm tracing the routes, ctrl's and I can't find the difference between the two.
Where I'm trying to access it from
Init App > ui-view > ui-view > ui-view > form-elements > form-accordion-on
What I'm working with:
The view
<ul class='form-elements'>
<li
class='row-fluid'
ng-hide='group.hidden'
ng-repeat='group in main.card.groups'
card='main.card'
form-element
values='values'
group='group'>
</li>
</ul>
This directive contains tons of different form types and calls their respective directives.
.directive('formElement', [function () {
return {
scope: {
values: '=',
group: '='
},
link: function(scope, element) {
l(scope.$parent.values);
element.attr('data-type', scope.group.type);
},
restrict: 'AE',
template: "<label ng-hide='group.type == \"section-break\"'>" +
"{{ group.name }}" +
"<strong ng-if='group.required' style='font-size: 20px;' class='text-error'>*</strong> " +
"<i ng-if='group.hidden' class='icon-eye-close'></i>" +
"</label>" +
"<div ng-switch='group.type'>" +
"<div ng-switch-when='accordion-start' form-accordion-on card='card' values='values' group='group'></div>" +
"<div ng-switch-when='accordion-end' form-accordion-off values='values' class='text-center' group='group'><hr class='mbs mtn'></div>" +
"<div ng-switch-when='address' form-address values='values' group='group'>" +
"</div>"
};
}])
This is the directive an example directive.
.directive('formAccordionOn', ['$timeout', function($timeout) {
return {
scope: {
group: '=',
values: '='
},
template: "<div class='btn-group'>" +
"<button type='button' class='btn' ng-class='{ active: values[group.trackers[0].id] == option }' ng-model='values[group.trackers[0].id]' ng-click='values[group.trackers[0].id] = option; toggleInBetweenElements()' ng-repeat='option in group.trackers[0].dropdown track by $index'>{{ option }}</button>" +
"</div>",
link: function(scope, element) {
console.log(scope) // returns the scope with the values property and it's values.
console.log(scope.values); // returns {}
})
// etc code ...
Closely related to but I'm using = on every isolate scope object:
AngularJS: Can't get a value of variable from ctrl scope into directive
Update
Sorry if this is a bit vague I've been at this for hours trying to figure out a better solution. This is just what I have atm.
Update 2
I cannot believe it was that simple.
var init = false;
scope.$watch('values', function(newVal, oldVal) {
if (_.size(newVal) !== _.size(oldVal)) {
// scope.values has the value I sent with it!
init = true;
getInitValues();
}
});
But this feels hacky, is there a more elegant way of handling this?
Update 3
I attach a flag in my ctrl when the values are ready and when that happens bam!
scope.$watch('values', function(newVal) {
if (newVal.init) {
getInitValues();
}
});
The output of console.log() is a live view (that may depend on the browser though).
When you examine the output of console.log(scope); scope has already been updated in the meantime. What you see are the current values. That is when the link function is executed scope.values is indeed an empty object. Which in turn means that values get updated after the execution of link, obviously.
If your actual problem is not accessing values during the execution of link, the you need to provide more details.
Update
According to your comments and edits you seem to need some one time initialization, as soon as the values are there. I suggest the following:
var init = scope.$watch('values', function(newVal, oldVal) {
if (newVal ==== oldVal) { //Use this if values is replaced, otherwise use a comparison of your choice
getInitValues();
init();
}
});
init() removes the watcher.

unable to make custom html with angular tags work with select2

I am using angular-ui's ui-select2. I want to add custom html formatting to the selections. Select2 allows this by specifying the formatSelection in its config.
I have html with angular tags as below that I want to use for formatting the selection-
var format_code = $compile('<div ng-click="showHide=!showHide" class="help-inline"><div style="cursor: pointer;" ng-show="!!showHide" ng-model="workflow.select" class="label">ANY</div><div style="cursor: pointer;" ng-hide="!!showHide" ng-model="workflow.select" class="label">ALL</div></div>')( $scope );
var format_html = "<span>" + data.n + ' : ' + data.v +' ng-bind-html-unsafe=format_code'+ "</span>"
$scope.select_config = {
formatSelection: format_html
}
If I compile the html as in above and assign it, I just see an [object,object] rendered in the browser. If I dont compile it, I see the html rendered properly, but the angular bindings dont happen, ie the clicks dont work.
Any ideas what is wrong?
I had the same problem, select2 loading in a jquery dialog and not using the options object I would give it.
What I ended up doing is isolating the element in a directive as following:
define(['./module'], function (module) {
return module.directive('dialogDirective', [function () {
return {
restrict: 'A',
controller: function ($scope) {
console.log('controller gets executed first');
$scope.select2Options = {
allowClear: true,
formatResult: function () { return 'blah' },
formatSelection: function () { return 'my selection' },
};
},
link: function (scope, element, attrs) {
console.log('link');
scope.someStuff = Session.someStuff();
element.bind('dialogopen', function (event) {
scope.select2content = MyResource.query();
});
},
}
}]);
and the markup
<div dialog-directive>
{{select2Options}}
<select ui-select2="select2Options" style="width: 350px;">
<option></option>
<option ng-repeat="item in select2content">{{item.name}}</option>
</select>
{{select2content | json}}
</div>
What is important here:
'controller' function gets executed before html is rendered. That means when the select2 directive gets executed, it will already have the select2Options object initialized.
'link' function populates the select2content variable asynchronously using the MyResource $resource.
Go on and try it, you should see all elements in the dropdown as "blah" and selected element as "my selection".
hope this helps, that was my first post to SO ever.

Resources