Two arrays cloned with Lodash, separate but the same. Why? - javascript

I know for a fact there is something pretty obvious here that I am completely missing, so your help is greatly appreciated.
I have a feature that provides two dropdowns. They contain the same data (the feature allows a trade between two people, the people is the data), but I want them each to get their own copy of said data.
Another part of this feature is that by picking Person A in the first dropdown, I want to disable Person A in the second dropdown, and vice versa, so I have the ng-options tag paying attention to a disabled property on the object.
The issue I have is that even with using a method such as Lodash's clone to properly create a "new" array upon first time assignment, every time I access Person A in ONE array (and specifically do NOT access the other array) invariably I am seeing that when I touch Person A, that object is updated in BOTH arrays, which has me flustered.
This feels like a down-to-the-metal, barebones Javascript issue (standard PEBCAK, I feel like I'm clearly misunderstanding or straight up missing something fundamental), maybe with a bit of AngularJS rendering-related fun-ness involved, but... What gives?
angular.module('myApp', [])
.controller('weirdDataController', function($scope) {
$scope.$watch('manager1_id', () => {
if (angular.isDefined($scope.manager1_id) && parseInt($scope.manager1_id, 10) > 0) {
$scope._disableManagerInOtherDropdown(false, $scope.manager1_id);
}
});
$scope.$watch('manager2_id', () => {
if (angular.isDefined($scope.manager2_id) && parseInt($scope.manager2_id, 10) > 0) {
$scope._disableManagerInOtherDropdown(true, $scope.manager2_id);
}
});
$scope._gimmeFakeData = () => {
return [{
manager_id: 1,
manager_name: 'Bill',
disabled: false
},
{
manager_id: 2,
manager_name: 'Bob',
disabled: false
},
{
manager_id: 3,
manager_name: 'Beano',
disabled: false
},
{
manager_id: 4,
manager_name: 'Barf',
disabled: false
},
{
manager_id: 5,
manager_name: 'Biff',
disabled: false
},
];
};
const data = $scope._gimmeFakeData();
$scope.firstManagers = _.clone(data);
$scope.secondManagers = _.clone(data);
$scope._disableManagerInOtherDropdown = (otherIsFirstArray, managerId) => {
const disableManagers = manager => {
manager.disabled = manager.manager_id === managerId;
};
if (otherIsFirstArray) {
$scope.firstManagers.forEach(disableManagers);
} else {
$scope.secondManagers.forEach(disableManagers);
}
console.log('Is the first item the same??', $scope.firstManagers[0].disabled === $scope.secondManagers[0].disabled);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<div ng-app="myApp" ng-controller="weirdDataController">
<div class="col-xs-12 col-sm-6">
<select class="form-control" ng-model="manager1_id" ng-options="manager.manager_id as manager.manager_name disable when manager.disabled for manager in firstManagers track by manager.manager_id">
<option value="" disabled="disabled">Choose one manager</option>
</select>
<select class="form-control" ng-model="manager2_id" ng-options="manager.manager_id as manager.manager_name disable when manager.disabled for manager in secondManagers track by manager.manager_id">
<option value="" disabled="disabled">Choose another manager</option>
</select>
</div>
</div>
<br /><br />
I threw everything relevant on $scope just for the sake of getting it working and illustrating the issue, but here's how it goes:
On init, I grab the array, then clone a copy for each dropdown
When each dropdown changes the model property (the object ID), I have a scope listener then call a method to handle disabling the selected object/person in the OPPOSITE list
Within this method, I determine which of the two lists/arrays to iterate through and mark the disabled object
At the end of this method, I do a simple console.log call to check the value of a given object. For quick-and-dirty simplicity, I just grab item at index 0 .
What I expected: one object have a disabled value of true, and the opposite object to have false. What I see: they both have true (assuming you select the first "real" item in the dropdown)
What's the deal? How big of an idiot am I being?

The answer to my question was: clone() does not perform a "deep" clone by default, therefore I was dealing with the same array despite making the flawed attempt that I was not. Using Lodash's cloneDeep() method solved my issue, but as Patrick suggested I reevaluated how I wrote the method in question and refactored it, which I removed the need to use any cloning at all.

Related

Efficient/Single trigger of ng-change on textbox

Let's say I have the following array in an angular controller:
somelist = [
{ name: 'John', dirty: false },
{ name: 'Max', dirty: false },
{ name: 'Betty', dirty: false }
];
I want to ng-repeat through it in my view, and generate editable fields for each record:
<div ng-repeat="i in somelist">
<input type="text" ng-model="i.name"/>
</div>
How would I go about efficiently marking the field as dirty if someone edits the textbox(model)?
I realize that I could use ng-change on the text field, however, that fires every time a user makes a single change(enters a key) on the textbox, making loads of calls unnecessarily.. Is there a more efficient way of doing this which I am missing?
With JavaScript...
*Edited: if that textareas don't have any other 'change' event to run, you can try inline onchange event, and replace it's value after have run once. Just making onchange="once(this)" become into this — onchange="" *In background. The code will still stay in your HTML. Demo:
(also exist input and keyup events... in Angular as well)
function once(e){
e.style.color="red";
e.onchange = "";
//just demo... remove this.
const d = document.getElementById('demo');
d.innerText = Number(d.innerText) + 1;
}
<textarea class="moo" onchange="once(this)">Change me!</textarea>
<textarea class="moo" onchange="once(this)">Me too!</textarea>
<textarea class="moo" onchange="once(this)">And me!</textarea>
<br><br>
Triggered times: <span id="demo">0</span>
Running eventListener function once (not really):
let once = [];//creating empty array
const moo = document.getElementsByClassName('moo');//getting all textareas
for(let i = 0; i < moo.length; i++ ){//looping, to add 'change' event to each element
once.push(1);//adding '1' to array 'i' times. Here it will look like [1,1,1];
moo[i].addEventListener('change', function(){
if(once[i]==0){return}//if array element equals 0 = return and don't run the function
this.style.color = "red";
once[i] = 0;//after triggered = making array element = 0;
//just demo... remove this.
const d = document.getElementById('demo');
d.innerText = Number(d.innerText) + 1;
});
}
<textarea class="moo">Change me!</textarea>
<textarea class="moo">Me too!</textarea>
<textarea class="moo">And me!</textarea>
<br><br>
Triggered times: <span id="demo">0</span>
*function is still working each time... but returning immediately, which is better, than "full" run.
To improve efficiency, reduce the number of watchers by using :: and eliminating two-way binds, e.g.ng-model:
<div ng-repeat="i in ::somelist">
<input type="text" value="{{i.name}}"
ng-blur="$emit('nameChanged', i)"/>
</div>
Then, in your controller:
$scope.$on('nameChanged', (event, i) => updateName(i));
Then a quick, simple function that updates the name with the corresponding ID using i.id and i.name, assuming you have:
$scope.someList = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Max' },
{ id: 3, name: 'Betty }'
Explanation
If people aren't going to be added/removed from the list, then you can use :: on someList, also known as a one-time binding, to improve efficiency. This circumvents setting up a watcher.
Also, by setting value={{i.name}}, you effectively set up a one-way bind from the controller to the DOM, rather than two-way, meaning that the value of the input isn't being checked every loop of the $digest cycle, but any changes to the model will update the DOM.
Just an idea, feel free to play with variations, such as dropping blur and using a single button that would update all changed fields at once.
You won't get much more efficient than that, unless you also remove the watcher from value="{{i.name}}" like so value="{{::i.name}}", and then you manually update the DOM when the event is received.

Data is not updated when using as object, but changes normally when it's a variable

I'm writing a function to update a custom checkbox when clicked (and I don't want to use native checkbox for some reasons).
The code for checkbox is
<div class="tick-box" :class="{ tick: isTicked }" #click="() => isTicked = !isTicked"></div>
which works find.
However, there are so many checkboxes, so I use object to keep track for each item. It looks like this
<!-- (inside v-for) -->
<div class="tick-box" :class="{ tick: isTicked['lyr'+layer.lyr_id] }" #click="() => {
isTicked['lyr'+layer.lyr_id] = !isTicked['lyr'+layer.lyr_id]
}"></div>
Now nothing happens, no error at all.
When I want to see isTicked value with {{ isTicked }}, it's just shows {}.
This is what I define in the <script></script> part.
export default {
data() {
return {
isTicked: {},
...
};
},
...
}
Could you help me where I get it wrong?
Thanks!
Edit:
I know that declaring as isTicked: {}, the first few clicks won't do anything because its proerty is undefined. However, it should be defined by the first/second click not something like this.
Objects does not reflect the changes when updated like this.
You should use $set to set object properties in order to make them reactive.
Try as below
<div class="tick-box" :class="{ tick: isTicked['lyr'+layer.lyr_id] }" #click="onChecked"></div>
Add below method:
onChecked() {
this.$set(this.isTicked,'lyr'+this.layer.lyr_id, !this.isTicked['lyr'+this.layer.lyr_id])
}
VueJS watches data by reference so to update object in state you need create new one.
onChecked(lyr_id) {
const key = 'lyr'+lyr_id;
this.isTicked = {...this.isTicked, [key]: !this.isTicked[key]};
}

Navigating through Vue.Js survey

I'm using Vue.Js for a survey, which is basically the main part and the purpose of the app. I have problem with the navigation. My prev button doesn't work and next keeps going in circles instead of only going forward to the next question. What I'm trying to accomplish is just to have only one question visible at a time and navigate through them in correct order using next and prev buttons and store the values of each input which I'll later use to calculate the output that will be on the result page, after the survey has been concluded. I've uploaded on fiddle a short sample of my code with only two questions just to showcase the problem. https://jsfiddle.net/cgrwe0u8/
new Vue({
el: '#quizz',
data: {
question1: 'How old are you?',
question2: 'How many times do you workout per week?',
show: true,
answer13: null,
answer10: null
}
})
document.querySelector('#answer13').getAttribute('value');
document.querySelector('#answer10').getAttribute('value');
HTML
<script src="https://unpkg.com/vue"></script>
<div id="quizz" class="question">
<h2 v-if=show>{{ question1 }}</h2>
<input v-if=show type="number" v-model="answer13">
<h2 v-if="!show">{{ question2 }}</h2>
<input v-if="!show" type="number" v-model="answer10">
<br>
<div class='button' id='next'>Next</div>
<div class='button' id='prev'>Prev
</div>
</div>
Thanks in advance!
You should look at making a Vue component that is for a survey question that way you can easily create multiple different questions.
Vue.component('survey-question', {
template: `<div><h2>{{question.text}}</h2><input type="number" v-model="question.answer" /></div>`,
props: ['question']
});
I've updated your code and implemented the next functionality so that you can try and create the prev functionality. Of course you should clean this up a little more. Maybe add a property on the question object so it can set what type the input should be. Stuff like that to make it more re-useable.
https://jsfiddle.net/9rsuwxvL/2/
If you ever have more than 1 of something, try to use an array, and process it with a loop. In this case you don't need a loop, but it's something to remember.
Since you only need to render one question at a time, just use a computed property to find the current question, based on some index. This index will be increased/decreased by the next/previous buttons.
With the code in this format, if you need to add a question, all you have to do is add it to the array.
https://jsfiddle.net/cgrwe0u8/1/
new Vue({
el: '#quizz',
data: {
questions:[
{question:'How old are you?', answer: ''},
{question:'How many times do you workout per week?', answer: ''},
],
index:0
},
computed:{
currentQuestion(){
return this.questions[this.index]
}
},
methods:{
next(){
if(this.index + 1 == this.questions.length)
this.index = 0;
else
this.index++;
},
previous(){
if(this.index - 1 < 0)
this.index = this.questions.length - 1;
else
this.index--;
}
}
})

How to use bootstrap-select or any other js control with Aurelia?

Not sure what I am missing here, probably something silly, but I am unable to find anything regarding, let's say how to use bootstrap-select control in Aurelia views. Can someone point me to the right article please?
PS: I am not looking to create another custom control out of bootstrap-select but use as it as.
Request for your help.
https://silviomoreto.github.io/bootstrap-select/
You can create a custom attribute that adds the bootstrap-select behavior to the <select> element. Here's an example:
http://plnkr.co/edit/So23Hm?p=preview
bootstrap-select.js
import {inject} from 'aurelia-framework';
const defaultOptions = {
style: 'btn-info',
size: 4
};
#inject(Element)
export class BootstrapSelectCustomAttribute {
constructor(element) {
this.element = element;
}
attached() {
let options = Object.assign({}, defaultOptions, this.value || {});
$(this.element).selectpicker(options);
}
detached() {
$(this.element).selectpicker('destroy');
}
}
app.html:
<template>
<require from="./bootstrap-select"></require>
<select value.bind="selectedPlanet" bootstrap-select>
<option model.bind="null">Select a planet</option>
<option repeat.for="planet of planets" model.bind="planet">${planet.name}</option>
</select>
</template>
app.js:
export class App {
selectedPlanet = null;
planets = [
{ name: 'Mercury', diameter: 3032 },
{ name: 'Venus', diameter: 7521 },
{ name: 'Earth', diameter: 7926 },
{ name: 'Mars', diameter: 4222 },
{ name: 'Jupiter', diameter: 88846 },
{ name: 'Saturn', diameter: 74898 },
{ name: 'Uranus', diameter: 31763 },
{ name: 'Neptune', diameter: 30778 }];
}
Pass options to the selectpicker call like this:
<select bootstrap-select.bind="{ size: 4 }">
Or like this:
<select bootstrap-select.bind="myOptions"> <!-- assumes there's a myOptions property on your view-model -->
You need to fit it into the Aurelia lifecycle, but other than that, you're free to do as you like.
If you look here you will find a couple of examples of using jQuery plugins in Aurelia. A relatively complex example being the Autocomplete widget. This is a custom control wrapping the jQuery plugin, which I think you don't want to do, but it should still give you an idea of how to implement it.
Extending Jeremy's answer and partially solving the refresh question, you could add something like this.
I couldn't find any legal way to get an event when the source of the repeater changed in a custom attribute. If anyone knows a better/elegant/decent way, please share.
Based on this answer Two way binding not working on bootstrap-select with aurelia, you could add a "task" that queries the length of the select and if the length changes, call the refresh.
With a little variation from the original, I decided to abort the task when the elements length changes the first time. In my case, they will always change only once, when updated after getting them from the database.
bind() {
var _this = this;
var sel = this.element;
var prevLen = sel.options.length || 0;
var addOpt = setInterval(function () {
var len = sel.options.length || 0;
if (len != prevLen || len > 0) {
clearTimeout(addOpt); //abort the task
$(_this.element).selectpicker("refresh");
}
}, 250);
};
As a way of disclaimer, I'm against writing code like this. It wastes resources when there should be a better way to solve this problem.
Finally, I started saying that it partially solved the refresh problem. Since it aborts the execution after the first change (assuming that no more changes are going to happen), if the items change more than once, this task would need to run forever.

How to select option in drop down protractorjs e2e tests

I am trying to select an option from a drop down for the angular e2e tests using protractor.
Here is the code snippet of the select option:
<select id="locregion" class="create_select ng-pristine ng-invalid ng-invalid-required" required="" ng-disabled="organization.id !== undefined" ng-options="o.id as o.name for o in organizations" ng-model="organization.parent_id">
<option value="?" selected="selected"></option>
<option value="0">Ranjans Mobile Testing</option>
<option value="1">BeaverBox Testing</option>
<option value="2">BadgerBox</option>
<option value="3">CritterCase</option>
<option value="4">BoxLox</option>
<option value="5">BooBoBum</option>
</select>
I have tried:
ptor.findElement(protractor.By.css('select option:1')).click();
This gives me the following error:
An invalid or illegal string was specified
Build info: version: '2.35.0', revision: 'c916b9d', time: '2013-08-12 15:42:01'
System info: os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '10.9', java.version: '1.6.0_65'
Driver info: driver.version: unknown
I have also tried:
ptor.findElement(protractor.By.xpath('/html/body/div[2]/div/div[4]/div/div/div/div[3]/ng-include/div/div[2]/div/div/organization-form/form/div[2]/select/option[3]')).click();
This gives me the following error:
ElementNotVisibleError: Element is not currently visible and so may not be interacted with
Command duration or timeout: 9 milliseconds
Build info: version: '2.35.0', revision: 'c916b9d', time: '2013-08-12 15:42:01'
System info: os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '10.9', java.version: '1.6.0_65'
Session ID: bdeb8088-d8ad-0f49-aad9-82201c45c63f
Driver info: org.openqa.selenium.firefox.FirefoxDriver
Capabilities [{platform=MAC, acceptSslCerts=true, javascriptEnabled=true, browserName=firefox, rotatable=false, locationContextEnabled=true, version=24.0, cssSelectorsEnabled=true, databaseEnabled=true, handlesAlerts=true, browserConnectionEnabled=true, nativeEvents=false, webStorageEnabled=true, applicationCacheEnabled=false, takesScreenshot=true}]
Can anyone please help me with this problem or throw some light on what i might be doing wrong here.
For me worked like a charm
element(by.cssContainingText('option', 'BeaverBox Testing')).click();
I had a similar problem, and eventually wrote a helper function that selects dropdown values.
I eventually decided that I was fine selecting by option number, and therefore wrote a method that takes an element and the optionNumber, and selects that optionNumber. If the optionNumber is null it selects nothing (leaving the dropdown unselected).
var selectDropdownbyNum = function ( element, optionNum ) {
if (optionNum){
var options = element.all(by.tagName('option'))
.then(function(options){
options[optionNum].click();
});
}
};
I wrote a blog post if you want more detail, it also covers verifying the text of the selected option in a dropdown: http://technpol.wordpress.com/2013/12/01/protractor-and-dropdowns-validation/
An elegant approach would involve making an abstraction similar to what other selenium language bindings offer out-of-the-box (e.g. Select class in Python or Java).
Let's make a convenient wrapper and hide implementation details inside:
var SelectWrapper = function(selector) {
this.webElement = element(selector);
};
SelectWrapper.prototype.getOptions = function() {
return this.webElement.all(by.tagName('option'));
};
SelectWrapper.prototype.getSelectedOptions = function() {
return this.webElement.all(by.css('option[selected="selected"]'));
};
SelectWrapper.prototype.selectByValue = function(value) {
return this.webElement.all(by.css('option[value="' + value + '"]')).click();
};
SelectWrapper.prototype.selectByPartialText = function(text) {
return this.webElement.all(by.cssContainingText('option', text)).click();
};
SelectWrapper.prototype.selectByText = function(text) {
return this.webElement.all(by.xpath('option[.="' + text + '"]')).click();
};
module.exports = SelectWrapper;
Usage example (note how readable and easy-to-use it is):
var SelectWrapper = require('select-wrapper');
var mySelect = new SelectWrapper(by.id('locregion'));
# select an option by value
mySelect.selectByValue('4');
# select by visible text
mySelect.selectByText('BoxLox');
Solution taken from the following topic: Select -> option abstraction.
FYI, created a feature request: Select -> option abstraction.
element(by.model('parent_id')).sendKeys('BKN01');
To access a specific option you need to provide the nth-child() selector:
ptor.findElement(protractor.By.css('select option:nth-child(1)')).click();
This is how i did my selection.
function switchType(typeName) {
$('.dropdown').element(By.cssContainingText('option', typeName)).click();
};
Here's how I did it:
$('select').click();
$('select option=["' + optionInputFromFunction + '"]').click();
// This looks useless but it slows down the click event
// long enough to register a change in Angular.
browser.actions().mouseDown().mouseUp().perform();
Try this, it is working for me:
element(by.model('formModel.client'))
.all(by.tagName('option'))
.get(120)
.click();
You can try this hope it will work
element.all(by.id('locregion')).then(function(selectItem) {
expect(selectItem[0].getText()).toEqual('Ranjans Mobile Testing')
selectItem[0].click(); //will click on first item
selectItem[3].click(); //will click on fourth item
});
Another way to set an option element:
var select = element(by.model('organization.parent_id'));
select.$('[value="1"]').click();
To select items (options) with unique ids like in here:
<select
ng-model="foo"
ng-options="bar as bar.title for bar in bars track by bar.id">
</select>
I'm using this:
element(by.css('[value="' + neededBarId+ '"]')).click();
We wrote a library which includes 3 ways to select an option:
selectOption(option: ElementFinder |Locator | string, timeout?: number): Promise<void>
selectOptionByIndex(select: ElementFinder | Locator | string, index: number, timeout?: number): Promise<void>
selectOptionByText(select: ElementFinder | Locator | string, text: string, timeout?: number): Promise<void>
Additional feature of this functions is that they wait for the element to be displayed before any action on the select is performed.
You can find it on npm #hetznercloud/protractor-test-helper.
Typings for TypeScript are provided as well.
Maybe not super elegant, but efficient:
function selectOption(modelSelector, index) {
for (var i=0; i<index; i++){
element(by.model(modelSelector)).sendKeys("\uE015");
}
}
This just sends key down on the select you want, in our case, we are using modelSelector but obviously you can use any other selector.
Then in my page object model:
selectMyOption: function (optionNum) {
selectOption('myOption', optionNum)
}
And from the test:
myPage.selectMyOption(1);
The problem is that solutions that work on regular angular select boxes do not work with Angular Material md-select and md-option using protractor. This one was posted by another, but it worked for me and I am unable to comment on his post yet (only 23 rep points). Also, I cleaned it up a bit, instead of browser.sleep, I used browser.waitForAngular();
element.all(by.css('md-select')).each(function (eachElement, index) {
eachElement.click(); // select the <select>
browser.waitForAngular(); // wait for the renderings to take effect
element(by.css('md-option')).click(); // select the first md-option
browser.waitForAngular(); // wait for the renderings to take effect
});
There's an issue with selecting options in Firefox that Droogans's hack fixes that I want to mention here explicitly, hoping it might save someone some trouble: https://github.com/angular/protractor/issues/480.
Even if your tests are passing locally with Firefox, you might find that they're failing on CircleCI or TravisCI or whatever you're using for CI&deployment. Being aware of this problem from the beginning would have saved me a lot of time:)
Helper to set the an option element:
selectDropDownByText:function(optionValue) {
element(by.cssContainingText('option', optionValue)).click(); //optionValue: dropDownOption
}
If below is the given dropdown-
<select ng-model="operator">
<option value="name">Addition</option>
<option value="age">Division</option>
</select>
Then protractorjs code can be-
var operators=element(by.model('operator'));
operators.$('[value=Addition]').click();
Source-https://github.com/angular/protractor/issues/600
Select option by Index:
var selectDropdownElement= element(by.id('select-dropdown'));
selectDropdownElement.all(by.tagName('option'))
.then(function (options) {
options[0].click();
});
I've improved a bit the solution written by PaulL.
First of all I fixed the code to be compatible with the last Protractor API. And then I declare the function in 'onPrepare' section of a Protractor config file as a member of the browser instance, so it can be referenced form any e2e spec.
onPrepare: function() {
browser._selectDropdownbyNum = function (element, optionNum) {
/* A helper function to select in a dropdown control an option
* with specified number.
*/
return element.all(by.tagName('option')).then(
function(options) {
options[optionNum].click();
});
};
},
The below example is the easiest way . I have tested and passed in Protractor Version 5.4.2
//Drop down selection using option's visibility text
element(by.model('currency')).element(by.css("[value='Dollar']")).click();
Or use this, it $ isshort form for .By.css
element(by.model('currency')).$('[value="Dollar"]').click();
//To select using index
var select = element(by.id('userSelect'));
select.$('[value="1"]').click(); // To select using the index .$ means a shortcut to .By.css
Full code
describe('Protractor Demo App', function() {
it('should have a title', function() {
browser.driver.get('http://www.way2automation.com/angularjs-protractor/banking/#/');
expect(browser.getTitle()).toEqual('Protractor practice website - Banking App');
element(by.buttonText('Bank Manager Login')).click();
element(by.buttonText('Open Account')).click();
//Drop down selection using option's visibility text
element(by.model('currency')).element(by.css("[value='Dollar']")).click();
//This is a short form. $ in short form for .By.css
// element(by.model('currency')).$('[value="Dollar"]').click();
//To select using index
var select = element(by.id('userSelect'));
select.$('[value="1"]').click(); // To select using the index .$ means a shortcut to .By.css
element(by.buttonText("Process")).click();
browser.sleep(7500);// wait in miliseconds
browser.switchTo().alert().accept();
});
});
I've been trawling the net for an answer on how to select an option in a model dropdown and i've used this combination which has helped me out with Angular material.
element(by.model("ModelName")).click().element(By.xpath('xpathlocation')).click();
it appears that when throwing the code all in one line it could find the element in the dropdown.
Took a lot of time for this solution I hope that this helps someone out.
If none of the answer's above worked for you, try this
works with async/await too
For selecting options by text
let textOption = "option2"
await element(by.whichever('YOUR_DROPDOWN_SELECTOR'))
.getWebElement()
.findElement(by.xpath(`.//option[text()="${textOption}"]`))
.click();
or by number
let optionNumber = 2
await element(by.whichever('YOUR_DROPDOWN_SELECTOR'))
.getWebElement()
.findElement(by.xpath(`.//option[${optionNumber}]`))
.click();
Of course you may need to modify the xpath of child options
Don't ask me why, but this is the only way I could automate my dropdowns, when I lost hope already
Update
There was actually one case when even this approach didnt work. THe work around was a bit ugly but worked. I simply had to select the value two times
We wanted to use the elegant solution up there using angularjs material but it didnt work because there are actually no option / md-option tags in the DOM until the md-select has been clicked. So the "elegant" way didn't work for us (note angular material!) Here is what we did for it instead, don't know if its the best way but its definately working now
element.all(by.css('md-select')).each(function (eachElement, index) {
eachElement.click(); // select the <select>
browser.driver.sleep(500); // wait for the renderings to take effect
element(by.css('md-option')).click(); // select the first md-option
browser.driver.sleep(500); // wait for the renderings to take effect
});
We needed to have 4 selects selected and while the select is open, there is an overlay in the way of selecting the next select. thats why we need to wait 500ms to make sure we don't get into trouble with the material effects still being in action.
Another way to set an option element:
var setOption = function(optionToSelect) {
var select = element(by.id('locregion'));
select.click();
select.all(by.tagName('option')).filter(function(elem, index) {
return elem.getText().then(function(text) {
return text === optionToSelect;
});
}).then(function(filteredElements){
filteredElements[0].click();
});
};
// using the function
setOption('BeaverBox Testing');
----------
element.all(by.id('locregion')).then(function(Item)
{
// Item[x] = > // x is [0,1,2,3]element you want to click
Item[0].click(); //first item
Item[3].click(); // fourth item
expect(Item[0].getText()).toEqual('Ranjans Mobile Testing')
});
You can select dropdown options by value:
$('#locregion').$('[value="1"]').click();
Here is how to do it by either option value or index. This example is a bit crude, but it shows how to do what you want:
html:
<mat-form-field id="your-id">
<mat-select>
<mat-option [value]="1">1</mat-option>
<mat-option [value]="2">2</mat-option>
</mat-select>
</mat-form-field>
ts:
function selectOptionByOptionValue(selectFormFieldElementId, valueToFind) {
const formField = element(by.id(selectFormFieldElementId));
formField.click().then(() => {
formField.element(by.tagName('mat-select'))
.getAttribute('aria-owns').then((optionIdsString: string) => {
const optionIds = optionIdsString.split(' ');
for (let optionId of optionIds) {
const option = element(by.id(optionId));
option.getText().then((text) => {
if (text === valueToFind) {
option.click();
}
});
}
});
});
}
function selectOptionByOptionIndex(selectFormFieldElementId, index) {
const formField = element(by.id(selectFormFieldElementId));
formField.click().then(() => {
formField.element(by.tagName('mat-select'))
.getAttribute('aria-owns').then((optionIdsString: string) => {
const optionIds = optionIdsString.split(' ');
const optionId = optionIds[index];
const option = element(by.id(optionId));
option.click();
});
});
}
selectOptionByOptionValue('your-id', '1'); //selects first option
selectOptionByOptionIndex('your-id', 1); //selects second option
static selectDropdownValue(dropDownLocator,dropDownListLocator,dropDownValue){
let ListVal ='';
WebLibraryUtils.getElement('xpath',dropDownLocator).click()
WebLibraryUtils.getElements('xpath',dropDownListLocator).then(function(selectItem){
if(selectItem.length>0)
{
for( let i =0;i<=selectItem.length;i++)
{
if(selectItem[i]==dropDownValue)
{
console.log(selectItem[i])
selectItem[i].click();
}
}
}
})
}
We can create a custom DropDown class for this and add a method as:
async selectSingleValue(value: string) {
await this.element.element(by.xpath('.//option[normalize-space(.)=\'' + value + '\']')).click();
}
Also, to verify what value is currently selected, we can have:
async getSelectedValues() {
return await this.element.$('option:checked').getText();
}
This is a simple one line answer in which angular has special locator which can help to select and index from list.
element.all(by.options('o.id as o.name for o in organizations')).get(Index).click()

Categories