I am trying to unit test a directive that makes a dropdown list using some JSON to specify the details of the list. The directive works fine, but I'm having issues while trying to unit test it.
Here's the test:
/* global inject, expect, angular */
define(function(require){
'use strict';
require('angular');
require('angularMock');
require('reporting/js/directives/app.directives');
require('reporting/js/directives/drop.down.field.directive');
describe("drop down field", function() {
// debugger;
var directive, scope;
beforeEach(module('app.directives'));
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope;
scope.dropDownResponses = {};
scope.dropDownField = {
"name": "Test Drop Down",
"type": "dropdown",
"hidden": "false",
"defaultValue": "None",
"values": [
{
"key": "1",
"value": "FL",
"select": "true"
},
{
"key": "2",
"value": "GA",
"select": "false"
},
{
"key": "3",
"value": "TX",
"select": "false"
}
],
"validation": null
};
directive = angular.element('<div drop-down-field="dropDownField" drop-down-responses="dropDownResponses"></div>');
$compile(directive)(scope);
scope.$digest();
}));
it("should build three dropdown choices", function() {
expect(directive.find('option').length).toBe(4);
});
it('should have one dropdown', function() {
expect(directive.find("select").length).toBe(1);
});
it('should update the model when a new choice is selected', function() {
angular.element(directive.find("select")[0]).val('1');
angular.element(directive.find("select")[0]).change();
expect(scope.dropDownResponses[scope.dropDownField.name]).toBe("1");
});
});
});
Here's the directive:
define(function(require) {
'use strict';
var module = require('reporting/js/directives/app.directives');
var template = require('text!reporting/templates/drop.down.field.tpl');
module.directive('dropDownField', function () {
return {
restrict: 'A',
replace: true,
template:template,
scope: {
dropDownField : "=",
dropDownResponses : "="
}
};
});
return module;
});
Here's the markup:
<div>
{{dropDownField.name}}
<select ng-model="dropDownResponses[dropDownField.name]" ng-options="value.key as value.value for value in dropDownField.values"></select>
</div>
The last it block is what is of concern here. When I fire the change event, the value on the model always winds up being one more that expected. For instance, the value stored in scope.dropDownResponses in this case winds up being 2.
Any ideas?
Its coming up to this questions first birthday and I found it intriguing as to why the test is not passing.
I have come to the conclusion that the premise of the test is wrong as the test
expect(scope.dropDownResponses[scope.dropDownField.name]).toBe("1");
should be
expect(scope.dropDownResponses[scope.dropDownField.name]).toBe("2");
The reason for this is that the the value stored in scope.dropDownResponses is in fact 2 as the questioner found.
When you are selecting by val('1') you are selecting the second option in the select element
<select ng-model="dropDownResponses[dropDownField.name]" ng-options="value.key as value.value for value in dropDownField.values" class="ng-valid ng-dirty">
<option value="0" selected="selected">FL</option>
<option value="1">GA</option>
<option value="2">TX</option>
</select>
which reflects the second item in the array in the spec
{
"key": "2",
"value": "GA",
"select": "false"
},
You can see this in action in this jsfiddle where the console.log output
it('should update the model when a new choice is selected', function() {
console.log(angular.element(directive.find("select")));
console.log('selected before = ' + angular.element(directive.find("select option:selected")).text());
angular.element(directive.find("select")[0]).val(1);
console.log('selected after = ' + angular.element(directive.find("select option:selected")).text());
angular.element(directive.find("select")[0]).change();
console.log('selected Text value = ' + angular.element(directive.find("select option:selected")).text());
expect(scope.dropDownResponses[scope.dropDownField.name]).toBe("2");
console.log(scope.dropDownResponses[scope.dropDownField.name]);
//console.log('selectedIndex=' + angular.element(directive.find("select")).selectIndex());
console.log(angular.element(directive.find("select"))[0]);
});
is
selected before =
(index):98 selected after = GA
(index):100 selected Text value = GA
(index):102 2
Related
'use strict';
var app = angular.module('myApp', []);
app.controller('AppCtrl', function($scope){
$scope.mail_notifications = [
{
"key": "all",
"value": "For any event on all my projects"
},
{
"key": "selected",
"value": "For any event on the selected projects only..."
},
{
"key": "only_my_events",
"value": ""
},
{
"key": "only_assigned",
"value": "Only for things I am assigned to"
},
{
"key": "only_owner",
"value": ""
},
{
"key": "none",
"value": "No events"
}
];
$scope.mail_notification = 'all';
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="AppCtrl">
<select ng-model="mail_notification" ng-options="c.key as c.value for c in mail_notifications"></select>
</div>
</div>
I have a below Fiddle link for reference.
https://jsfiddle.net/maayuresh/mjugfLr0/1/
In above Fiddle , I have json object named mail_notifications.
In this some properties are empty , null like this -> ""
This null values are getting displayed in select dropdown list.
My aim is to remove that from the select dropdown.
How can i achive that ?
FYI,
This null values are getting displayed in select dropdown list.
null is not equivalent to "" (empty string)
To filter the item with value is not an empty string:
$scope.mail_notifications = $scope.mail_notifications.filter(x => x["value"] != "");
You may also work with filtering the item with value is a truthy value as below:
$scope.mail_notifications = $scope.mail_notifications.filter(x => x["value"]);
Demo # JS Fiddle
I have an array of towns - each town has a label and a value. Then there is an HTML input which should display the town label selected and on submit send the relevant value. A JQuery script with a filter that chooses the town based on the beginning characters of the label and not the characters in the rest of the label body. I need the script to, filter as designed, but when a town is selected, the HTML id #dtags must display the label and the #dtag must submit the value via a hidden input.
I have two scripts, each one does ONE of the above successfully, but I am battling to get one script to combine both "features".
I am looking for some assistance to create a single JQuery script to achieve the above. My JQuery skills are limited and the code below is what I have managed to find through searching and adapting - credit to the originators.
Please see code below:-
<div class="ui-widget">
<input id="dtags" class="form-control col-md-8" placeholder="Choose Delivery Town" required>
<input id="dtag" type="hidden" name="deltown">
</div>
<script>
(function() {
var towns = [
{ "label": "AANDRUS, Bloemfontein", "value": 1 },
{ "label": "AANHOU WEN, Stellenbosch", "value": 2 },
{ "label": "ABBOTSDALE, Western Cape", "value": 3 },
{ "label": "ABBOTSFORD, East London", "value": 4 },
{ "label": "ABBOTSFORD, Johannesburg", "value": 5 },
{ "label": "ABBOTSPOORT, Limpopo", "value": 6 },
{ "label": "ABERDEEN, Eastern Cape", "value": 7 },
{ "label": "ACKERVILLE, Witbank", "value": 8 },
{ "label": "ACORNHOEK, Mphumalanga", "value": 9 },
{ "label": "ACTIVIA PARK, Germiston", "value": 10 }
];
$("#dtags").autocomplete({
source: towns
});
// Overrides the default autocomplete filter function to search only from the beginning of the string
$.ui.autocomplete.filter = function (array, term) {
var matcher = new RegExp("^" + $.ui.autocomplete.escapeRegex(term), "i");
return $.grep(array, function (value) {
return matcher.test(value.label || value.value || value);
});
};
})();
//Uses event.preventDefault(); to halt the default and works as below
// $("#dtags").autocomplete({
//minLength: 1,
//source: towns,
//select: function(event, ui) {
//event.preventDefault();
//$("#dtags").val(ui.item.label);
//$("#dtag").val(ui.item.value);
//}
//});
//});
//});
//});
</script>
In case this could help anyone in the future, this is the script that I managed to get working successfully as per the question - slight change as to using an external data source:-
<script>
(function() {
var towns = [<?php require_once("towns.txt")?>];
$("#dtags").autocomplete({
source: towns,
select: function( event, ui ) {
$("#dtags").val(ui.item.label);
$("#dtag").val(ui.item.value);
return false;
}
});
// Overrides the default autocomplete filter function to search only from the beginning of the string
$.ui.autocomplete.filter = function (array, term) {
var matcher = new RegExp("^" + $.ui.autocomplete.escapeRegex(term), "i");
return $.grep(array, function (value) {
return matcher.test(value.label || value.value || value);
});
};
})();
</script>
I've been struggling with Angular's isolate scope for over 24hrs now. Here's my scenario: I have an ng-repeat iterating over an array of objects from which I want to use a custom directive to either generate a <select> or <input> based on the field_type property of the current object being iterated. This means I'll have to generate the template and $compile in the post-link function of the directive since I have no access to the iterated object in the template function.
Everything works as expected, apart from the actual binding of the generated template to the controller (vm) in my outer scope. I think my approach (adding this in the template string: ng-model="vm.prodAttribs.' + attr.attribute_code +'") may be wrong, and would appreciate pointers in the right direction. Thanks!
See sample code below:
directives:
directives.directive('productAttributeWrapper', ['$compile', function($compile){
//this directive exists solely to provide 'productAttribute' directive access to the parent scope
return {
restrict: 'A',
scope: false,
controller: function($scope, $element, $attrs){
this.compile = function (element) {
$compile(element)($scope);
console.log('$scope.prodAttribs in directive: ', $scope.prodAttribs);
};
}
}
}]);
directives.directive('productAttribute', ['$compile', function($compile){
return {
restrict: 'A',
require: '^productAttributeWrapper', //use the wrapper's controller
scope: {
attribModel: '=',
prodAttribute: '=productAttribute', //binding to the model being iterated by ng-repeat
},
link: function(scope, element, attrs, ctrl){
var template = '';
var attr = scope.prodAttribute;
if(!attr) return;
switch(attr.attribute_field_type.toLowerCase()){
case 'textfield':
template =
'<input type="text" id="'+attr.attribute_code+'" ng-model="vm.prodAttribs.' + attr.attribute_code +'">';
break;
case 'dropdown':
template = [
'<select class="cvl" id="'+attr.attribute_code+'" ng-model="vm.prodAttribs.' + attr.attribute_code +'">',
'#cvl_option_values',
'\n</select>'
].join('');
var options = '\n<option value="">Select One</option>';
for(var i=0; i<attr.cvl_option_values.length; i++) {
var optionVal = attr.cvl_option_values[i].value;
options += '\n<option value="'+optionVal+'">' + attr.cvl_option_values[i].value + '</option>';
}
template = template.replace('#cvl_option_values', options);
break;
}
element.html(template);
ctrl.compile(element.html()); //try to bind template to outer scope
}
}
}]);
html:
<div ng-controller="ProductController as vm">
<div product-attribute="attrib" ng-repeat="attrib in vm.all_attribs"></div>
</div>
controller:
app.controller('ProductDetailsController', function(){
var vm = this;
//also added the property to $scope to see if i could access it there
$scope.prodAttribs = vm.prodAttribs = {
name: '',
description: '',
price: [0.0],
condition: null
}
vm.all_attributes = [
{
"attribute_id": 1210,
"attribute_display_name": "Product Type",
"attribute_code": "product_type",
"attribute_field_type": "Textfield",
"cvl_option_values": [],
"validation_rules": {}
},
{
"attribute_id": 902,
"attribute_display_name": "VAT",
"attribute_code": "vat",
"attribute_field_type": "dropdown",
"cvl_option_values": [
{
"option_id": "5",
"value": "5%"
},
{
"option_id": "6",
"value": "Exempt"
}
],
"validation_rules": {}
}];
})
issue is probably here :
element.html(template);
ctrl.compile(element.html()); //try to bind template to outer scope
element.html() returns a html as a string, not the ACTUAL dom content, so what you inserted into your directive's element is never actually compiled by angular, explaining your (absence of) behaviour.
element.append(ctrl.compile(template));
should work way better.
For directive requiring parent controller, I would also change your ctrl.compile method (renamed to insertAndCompile here)
ctrl.insertAndCompile = function(content) {
$compile(content)($scope, function(clone) {
$element.append(clone);
}
}
You would just have to call it this way :
ctrl.insertAndCompile(template);
instead of the 2 lines I gave as first answer.
I would suggest to use templates instead of html compilation manually. The solution is much simpler:
Controller would contain data declaration:
app.controller('ProductDetailsController', function($scope) {
$scope.prodAttribs = {
name: '',
description: '',
price: [0.0],
condition: null
}
$scope.all_attribs = [{
"attribute_id": 1210,
"attribute_display_name": "Product Type",
"attribute_code": "product_type",
"attribute_field_type": "Textfield",
"cvl_option_values": [],
"validation_rules": {}
}, {
"attribute_id": 902,
"attribute_display_name": "VAT",
"attribute_code": "vat",
"attribute_field_type": "dropdown",
"cvl_option_values": [{
"option_id": "5",
"value": "5%"
}, {
"option_id": "6",
"value": "Exempt"
}],
"validation_rules": {}
}];
});
Your directive would be as simple as that:
app.directive('productAttribute', function() {
return {
restrict: 'A',
scope: {
attribModel: '=',
prodAttribute: '=productAttribute'
},
templateUrl: 'template.html',
controller: function($scope) {}
}
});
template.html would be:
<div>
<select ng-show="prodAttribute.attribute_field_type.toLowerCase() == 'dropdown'" class="cvl" id="" ng-model="prodAttribs.attribute_code">
<option value="">Select One</option>
<option ng-repeat="item in prodAttribute.cvl_option_values track by $index" value="{{item.value}}">{{item.value}}</option>
</select>
<input ng-show="prodAttribute.attribute_field_type.toLowerCase() == 'textfield'" type="text" id="{{prodAttribute.attribute_code}}" ng-model="prodAttribute.attribute_code">
</div>
And your html:
<div ng-controller="ProductController">
<div ng-repeat="attrib in all_attribs" product-attribute="attrib">{{attrib}}</div>
</div>
$scope.StateList = {"States": [
{
"Id": 1,
"Code": "AL",
"Name": "Alabama"
},
{
"Id": 2,
"Code": "AK",
"Name": "Alaska"
},
{
"Id": 3,
"Code": "AZ",
"Name": "Arizona"
},
{
"Id": 4,
"Code": "AR",
"Name": "Arkansas"
}]}
And I display the data as follows in an html select:
<select ng-model="Address.State"
ng-options="state.Code as state.Name for state in StateList.States"></select>
Right now this will display the full name of the state in the select like "Arizona". What I would like to do is format the display without adding a new property to the object, to use something like (state.Name, state.Code, state.Id). I am trying to use filters of some sort to do this but I have not figured it out yet. Thanks for your suggestions.
plunker
There are three ways that you can achieve this. The first is to just set the value you want inline:
<select ng-model="Address.State" ng-options="state.Code as (state.Name + ', ' + state.Code + ', ' + state.Id) for state in StateList.States"></select>
Or you can do the same thing, but as a function in your controller:
$scope.display = function(state) {
return state.Name + ', ' + state.Code + ', ' + state.Id;
}
<select ng-model="Address.State" ng-options="state.Code as display(state) for state in StateList.States"></select>
Or you can create a filter (as per PSLs answer)
Try creating a small format filter:-
app.filter('stateName', function() {
return function(itm) {
return [itm.Name , itm.Code, itm.Id].join();
}});
and use it as:-
ng-options="state.Code as (state|stateName) for state in StateList.States
Plnkr
I am trying to bind data from a web service and then use that data to pre-populate a form. All form controls are binding correctly except for a single multi-select element. If I manually select an option the model does update. Below is my controller:
myApp.controller('AdminVideosEditCtrl', [
'$scope',
'$http',
'$routeParams',
'$location',
function($scope, $http, $routeParams, $location) {
$http.get('/videos/' + $routeParams.videoId + '?embed=presenters').success(function(data) {
$scope.video = data.data
// Load providers
$http.get('/providers').success(function(data) {
$scope.providers = data.data;
// Load Presenters
$http.get('/presenters').success(function(data) {
$scope.presenters = data.data;
});
});
});
}
]);
Once the final request returns, my model looks like this (output via {{ video | json }}):
{
"id": "ca3ca05a-834e-47b1-aaa1-3dbe38338ca9",
"title": "Doloremque iure consequatur quam ea.",
"is_public": false,
"is_visible": true,
"url": "http://someurl.com/",
"provider_id": "1b4d18eb-d56c-41ae-9431-4c058a32d651",
"level_id": "38ed7286-da05-44b9-bfb9-e1278088d229",
"duration": "17:38",
"transcript_file": "rerum-sint-voluptatum.md",
"presenters": [
{
"id": "5111531d-5f2a-45f5-a0c4-4fa3027ff249",
"first_name": "First",
"last_name": "Last",
"full_name": "First Last"
}
],
"provider": {
"id": "1b4d18eb-d56c-41ae-9431-4c058a32d651",
"title": "You Tube"
}
}
Here is how the multi-select looks in my view:
<div class="form-group">
<label for="presenters" class="col-lg-2 control-label">Presenters</label>
<div class="col-lg-10">
<select multiple="multiple" class="form-control" id="presenters" ng-model="video.presenters" ng-options="presenter.full_name for ( id , presenter ) in presenters">
</select>
</div>
</div>
The select element populates correctly, and I would expect for it to default with the "First Last" element selected, however nothing is selected. I know my model is initialized correctly because if I manually select the element nothing in the model changes (if I select a different element it does, but still retains the same structure as it does on initial page load).
I tried adding a $scope.$apply call, and I also tried $scope.$root.$eval(), neither of which worked.
Update
The presenters model (containing all of the presenters) looks like this after it is fetched from the service (names have been changed to protect the innocent):
[
{
"id": "47b6e945-2d4b-44c2-b44b-adb96460864d",
"first_name": "First",
"last_name": "Last",
"full_name": "First Last"
},
{
"id": "5111531d-5f2a-45f5-a0c4-4fa3027ff249",
"first_name": "One",
"last_name": "Two",
"full_name": "One Two"
},
{
"id": "7cb1e44b-2806-4576-80b2-ae730ad356f7",
"first_name": "Three",
"last_name": "Four",
"full_name": "Three Four"
}
]
Solution
Just put this at the bottom of your controller
$scope.video.presenters.forEach(function(obj, idx){
$scope.presenters.forEach(function(presenter, jdx){
if (obj.id === presenter.id) {
$scope.video.presenters = [$scope.presenters[jdx]]
}
});
});
JSFIDDLE
More Robust Solution
This is more robust as you might want to preselect multiple options. This solution pushes each selected option into an array and then assigns it to $scope.video.presenters model
var selectedOptions = [];
$scope.video.presenters.forEach(function (obj, idx) {
$scope.presenters.forEach(function (presenter, idj) {
if (obj.id === presenter.id) {
selectedOptions.push($scope.presenters[idj]);
$scope.video.presenters = selectedOptions;
}
});
JSFIDDLE
Note: Ideally you should be using the id key as the unique for the objects.
This solution assumes and only caters for preselecting one option.