We have a menu represented as a ul->li list (simplified):
<ul class="dropdown-menu" role="menu">
<li ng-repeat="filterItem in filterCtrl.filterPanelCfg track by filterItem.name"
ng-class="{'divider': filterItem.isDivider}" class="ng-scope">
Menu Item 1
</li>
...
<li ng-repeat="filterItem in filterCtrl.filterPanelCfg track by filterItem.name"
ng-class="{'divider': filterItem.isDivider}" class="ng-scope">
Menu Item 2
</li>
</ul>
Where somewhere at position N, there is a divider, which can be identified by evaluating filterItem.isDivider or by checking the text of the a link (in case of a divider, it's empty).
Now, the goal is to get all of the menu items that are located before the divider. How would you approach the problem?
My current approach is rather generic - to extend ElementArrayFinder and add takewhile() function (inspired by Python's itertools.takewhile()). Here is how I've implemented it (based on filter()):
protractor.ElementArrayFinder.prototype.takewhile = function(whileFn) {
var self = this;
var getWebElements = function() {
return self.getWebElements().then(function(parentWebElements) {
var list = [];
parentWebElements.forEach(function(parentWebElement, index) {
var elementFinder =
protractor.ElementFinder.fromWebElement_(self.ptor_, parentWebElement, self.locator_);
list.push(whileFn(elementFinder, index));
});
return protractor.promise.all(list).then(function(resolvedList) {
var filteredElementList = [];
for (var index = 0; index < resolvedList.length; index++) {
if (!resolvedList[index]) {
break;
}
filteredElementList.push(parentWebElements[index])
}
return filteredElementList;
});
});
};
return new protractor.ElementArrayFinder(this.ptor_, getWebElements, this.locator_);
};
And, here is how I'm using it:
this.getInclusionFilters = function () {
return element.all(by.css("ul.dropdown-menu li")).takewhile(function (inclusionFilter) {
return inclusionFilter.evaluate("!filterItem.isDivider");
});
};
But, the test is just hanging until jasmine.DEFAULT_TIMEOUT_INTERVAL is reached on the takewhile() call.
If I put console.logs into the loop and after, I can see that it correctly pushes the elements before the divider and stops when it reaches it. I might be missing something here.
Using protractor 2.2.0.
Also, let me know if I'm overcomplicating the problem.
Maybe I'm missing something, but couldn't you just go through ul li a elements while they gave you something from getText(), and store them to some array, or do something with them directly in that loop?
var i = 0;
var el = element.all(by.css('ul li a'));
var tableItems = [];
(function loop() {
el.get(i).getText().then(function(text){
if(text){
tableItems.push(el.get(i));
i+=1;
loop();
}
});
}());
takewhile() actually worked for me once I removed the protractor.promise = require("q"); from onPrepare() - this was there to replace protractor.promise with q on the fly to be able to use the syntactic sugar like spread() function. Apparently, it is not safe to use q in place of protractor.promise.
All I have to do now is to add this to onPrepare():
protractor.ElementArrayFinder.prototype.takewhile = function(whileFn) {
var self = this;
var getWebElements = function() {
return self.getWebElements().then(function(parentWebElements) {
var list = [];
parentWebElements.forEach(function(parentWebElement, index) {
var elementFinder =
protractor.ElementFinder.fromWebElement_(self.ptor_, parentWebElement, self.locator_);
list.push(whileFn(elementFinder, index));
});
return protractor.promise.all(list).then(function(resolvedList) {
var filteredElementList = [];
for (var index = 0; index < resolvedList.length; index++) {
if (!resolvedList[index]) {
break;
}
filteredElementList.push(parentWebElements[index])
}
return filteredElementList;
});
});
};
return new protractor.ElementArrayFinder(this.ptor_, getWebElements, this.locator_);
};
The usage is very similar to filter():
element.all(by.css("ul li a")).takewhile(function (elm) {
return elm.getText().then(function (text) {
return text;
});
});
FYI, proposed to make takewhile() built-in.
Related
I am trying to get some items in an array to update as function of a timer event. But I can't wrap my mind about how to do in in knockout... more I feel a bit knocked-out myself.
The Problem I think is simple, perhaps my approach is wrong, or there simply is something I don't get.
To be a bit more detailed:
The items in the array each represent a time since 'something', to prevent reloading from the server, as you can see, I have bound to the ko.computed(display) function I have defined, which was supposed to take the items.timeElapsed and add the time since the page was loaded, self.moreTimeElapsed()
The html:
<div>
<ul data-bind="foreach: counted">
<li><span data-bind="text: name"></span> - <span data-bind="text: display"></span></li>
</ul>
</div>
and the ViewModel
<script type="text/javascript">
function myViewModel() {
var self = this;
self.email = 'someone#example.com';
var d = new Date();
self.startTime = d.getTime();
self.moreTimeElapsed = ko.observable(0);
var items = [{"name":"counter1","timeElapsed":168},{"name":"counter2","timeElapsed":162}];
for (i = 0; i < items.length; ++i)
{
items[i].display = ko.computed(function ()
{
return items[i].timeElapsed + self.moreTimeElapsed();
})
}
self.counted = ko.observableArray(items);
self.updateCounters = function () {
var d = new Date();
self.moreTimeElapsed(d.getTime() - self.startTime);
}
setInterval(self.updateCounters, 3000);
};
ko.applyBindings(new myViewModel());
function Reload() {
location.reload(true);
}
</script>
Any hint or ideas appreciated.
The problem was with the counter 'i' going out of scope.
i wrapped the code in a closure and that solved the problem
var items = [{"name":"counter1","timeElapsed":1706},{"name":"counter2","timeElapsed":1700}];
var attachDisplayFunc = function(item) {
item.display = ko.computed(function () {
return item.timeElapsed + self.moreTimeElapsed();
})
}
for (i = 0; i < items.length; ++i) {
attachDisplayFunc(items[i]);
}
self.counted = ko.observableArray(items);
So.. is there any way to get notified when making errors as this?
Durring some experiments with protractorJS i noticed that there is no easy way to extend (inherit) ElementFinder object from protractor to add own functions.
For example, i want to create object Checkbox, that would have additional method - check() - should switch checkbox depending on result of isSelected().
I come up with the code -
var ElementFinder = require('protractor/lib/element.js').ElementFinder;
var ElementArrayFinder = require('protractor/lib/element.js').ElementArrayFinder;
class CheckBox extends ElementFinder {
constructor(loc) {
var getWebElements = function () {
var ptor = browser;
var locator = loc;
return ptor.waitForAngular().then(function() {
if (locator.findElementsOverride) {
return locator.findElementsOverride(ptor.driver, null, ptor.rootEl);
} else {
return ptor.driver.findElements(locator);
}
});
}
var ArrayFinderFull = new ElementArrayFinder(browser, getWebElements, loc);
super(browser, ArrayFinderFull);
}
check() {
return this.isSelected().then(selected => selected? this.click() : null)
}
}
But getWebElements is copy-paste from protractor/element.js -
https://github.com/angular/protractor/blob/3.1.0/lib/element.js#L131
This copy-paste flustrating me. I think there should be more proper way to extend ElementFinder.
Does anyone inherited ElementFinder in protractorJS?
I'm not sure this would help, but here is something we did recently to have a takewhile() method available on an ElementArrayFinder. We've put the following into onPrepare():
protractor.ElementArrayFinder.prototype.takewhile = function(whileFn) {
var self = this;
var getWebElements = function() {
return self.getWebElements().then(function(parentWebElements) {
var list = [];
parentWebElements.forEach(function(parentWebElement, index) {
var elementFinder =
protractor.ElementFinder.fromWebElement_(self.ptor_, parentWebElement, self.locator_);
list.push(whileFn(elementFinder, index));
});
return protractor.promise.all(list).then(function(resolvedList) {
var filteredElementList = [];
for (var index = 0; index < resolvedList.length; index++) {
if (!resolvedList[index]) {
break;
}
filteredElementList.push(parentWebElements[index])
}
return filteredElementList;
});
});
};
return new protractor.ElementArrayFinder(this.ptor_, getWebElements, this.locator_);
};
And now we can use takewhile on the result of element.all():
element.all(by.repeater("row in rows")).takewhile(function (elm) {
return elm.getText().then(function (text) {
return some_condition_to_be_true;
});
});
Now it is much simplier to extend ElementFinder, i calling this - page fragments.
I even created lib to solve this issue (PRs welcome!) - https://github.com/Xotabu4/protractor-element-extend
For now it only works with ElementFinder, but i want to be able to extend
ElementArrayFinders as well (planned for 2.0.0 version)
UPDATE
Support for ElementArrayFinder inheritance is added.
I'm new to knockout.js (and this is also my first stackoverflow post) and I'm now facing the following problem.
I'm not able to bind the data from web api to a ko.observablearray. Why is the length of this Announcements ko.observablearray always 0? The code works fine with client side data (by adding new announcements)..
Here's the JS-code:
var AnnouncementModel = function () {
var self = this;
self.AnnouncementText = ko.observable();
self.AllDepartmentsBool = ko.observable();
self.Editable = ko.observable(false);
self.Add = function () {
viewModel.Announcements.push(self);
viewModel.AnnouncementToEdit(new AnnouncementModel());
};
self.Delete = function () {
ajaxHelper(announcementsUri + self.ID, 'DELETE').done(
viewModel.Announcements.remove(self));
};
self.Edit = function () {
self.Editable(!self.Editable());
};
}
//The ViewModel
function AnnouncementsViewModel() {
var self = this;
self.InitialData = ko.observableArray();
self.Announcements = ko.observableArray();
self.AnnouncementToEdit = ko.observable(new AnnouncementModel());
self.error = ko.observable();
function getAllAnnouncements() {
ajaxHelper(announcementsUri, 'GET').done(function(data) {
self.InitialData(data);
});
};
getAllAnnouncements();
};
var viewModel = new AnnouncementsViewModel();
ko.applyBindings(viewModel, document.getElementById("announcements-container"));
function createAnnouncement(announcementDto) {
var announcement = new AnnouncementModel();
announcement.AnnouncementText = ko.observable(announcementDto.AnnouncementText);
announcement.AllDepartmentsBool = ko.observable(announcementDto.AllDepartmentsBool);
announcement.Editable = ko.observable(false);
return announcement;
}
var length = viewModel.InitialData.length;
for (i = 0; i < length; i++) {
var newAnnouncement = createAnnouncement(InitialData[i]);
viewModel.Announcements.push(newAnnouncement);
}
The HTML:
<div id="announcements-container" style="display: inline-block; float: right">
<ul id="announcements-list" class="newsticker" data-bind="foreach: Announcements">
<li>
<span data-bind="html: AnnouncementText"></span>
</li>
</ul>
#Html.Partial("_AnnouncementsModal")
</div>
The InitialData gets populated from the api as it should:
GOT IT WORKING! :
Thanks for the quick answers. I got the code working by iterating the data with .forEach(). Another problem was that the initialData didn't get populated in it's current scope so I edited the getAnnouncements function to work like this :
function getAllAnnouncements() {
ajaxHelper(announcementsUri, 'GET').done(function(data) {
data.forEach(function (entry) {
var newAnnouncement = createAnnouncement(entry);
self.Announcements.push(newAnnouncement);
});
});
};
This line is the likely culprit:
var length = viewModel.InitialData.length;
Remember that InitialData is a function. Functions have a length (it's their "arity", the number of formal arguments they have), but the observable function for an observable array's length isn't the array's length..
You probably wanted the length of the array inside it:
var length = viewModel.InitialData().length;
// -------------------------------^^
Your various calls to push on observable arrays work even though length doesn't because Knockout provides push (and several other things) on the observable array function, as James points out.
Similarly, this line:
var newAnnouncement = createAnnouncement(InitialData[i]);
probably wants to be using the array as well (and is missing viewModel. in front of InitialData).
So that whole section probably wants to be refactored a bit:
viewModel.InitialData().forEach(function(entry) {
var newAnnouncement = createAnnouncement(entry);
viewModel.Announcements.push(newAnnouncement);
});
or without forEach (but really, it's nearly 2016, and it's shimmable on obsolete browsers);
var data = viewModel.InitialData();
for (var i = 0; i < data.length; ++i) {
var newAnnouncement = createAnnouncement(data[i]);
viewModel.Announcements.push(newAnnouncement);
}
Side note: Your code (at least as it is in the question) was also falling prey to The Horror of Implicit Globals by not declaring the i that you use in that for loop. I've added a var above, but this is another reason for using forEach to loop through arrays.
You can also use EcmaScript 6 style enumeration as follows:
viewModel.InitialData().forEach(item => {
let newAnnouncement = createAnnouncement(item);
viewModel.Announcements.push(newAnnouncement);
});
Basically I'm pushing containers into an array, and once one has been pushed, I don't want to allow that same one to be pushed again.
Here is my JSfiddle: http://jsfiddle.net/9Dmcg/3/
Javascript:
$(document).ready(function(){
var favorites = [];
var counter = 0;
$('.containers').on('click', function(){
favorites.push($(this).clone())
$('.favorite').append(favorites);
});
});
I need to find a way to work around that.
Unless there's more to that click event, you can use the .one() method in place of .on to get this functionality.
$(document).ready(function(){
var favorites = [];
var counter = 0;
$('.containers').one('click', function(){
favorites.push($(this).clone())
$('.favorite').append(favorites);
});
});
http://jsfiddle.net/9Dmcg/4/
Even if there were more to it, you could still use .one():
$(document).ready(function(){
var favorites = [];
var counter = 0;
$('.containers').one('click', function(){
favorites.push($(this).clone())
$('.favorite').append(favorites);
$(this).on("click",function(){
alert("Favorite Added!");
}).triggerHandler("click");
});
});
http://jsfiddle.net/9Dmcg/5/
Try to check for element id's I would say. Something like this:
$(document).ready(function(){
var favorites = [];
var counter = 0;
$('.containers').bind('click', function(){
var isAdded = false;
for (var f = 0; f < favorites.length; f++) {
console.log(favorites[f].id + "|" + this.id);
if (favorites[f].id === this.id)
isAdded = true;
}
if (!isAdded)
favorites.push($(this).clone()[0])
console.log(favorites);
$('.favorite').append(favorites);
});
});
And here's working example -> http://jsfiddle.net/9Dmcg/7/
mz
This can be done using the JS Proxy (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
Following is the code to create an array which will not accept duplication entry
var arr = new Proxy([], {
get: function(target, key) {
return target[key];
},
set: function(target, key, value){
if(target.indexOf(value)>=0) return false;
target[key] = value;
return true
}
});
arr.push(100);
arr.push(100); // this will throw error
In the above code, we are modifying the default behaviour of array as the set method will be called on any modification done on the array.
You can check if the element already exists:
$(document).ready(function(){
var favorites = [];
$('.containers').on('click', function(){
var found = false;
for(var i = 0; i < favorites.length; i++)
{
if (favorites[i] == $(this)) found = true;
}
if (!found) favorites.push($(this).clone())
$('.favorite').append(favorites);
});
});
Add a class to items that have been pushed, and then do a basic check:
Instead of:
favorites.push($(this).clone())
you can do:
if( !$(this).hasClass("pushed") ) {
favorites.push( $(this).addClass("pushed").clone() );
}
Just push the element itself instead of cloning it:
favorites.push($(this))
The added benefit is that it gives the user a clue that the item has already be added.
Live demo: http://jsfiddle.net/9Dmcg/6/
Seems like your looking for Array.indexOf. It returns the index of the value on an array. Returns -1 if not found.
It is a new array method, so you will need a polyfill for it to work on old browsers.
$(document).ready(function(){
var favorites = [];
var counter = 0;
$('.containers').on('click', function(){
var clone = $(this).clone(); // caching element
if (favorites.indexOf(clone) !== -1) {
favorites.push(clone);
}
$('.favorite').append(favorites);
});
});
I have an array of DOM elements (lis) that I want to re-order based on an attribute of the lis.
Currently I try to:
Store the lis in an array.
Initiate a jQuery animation queue.
Then add to the queue the following:
Animating ALL the lis away
Detach said lis from dom with jQuery.detach().
Applying a sort() function to the array.
ADD the re-ordered lis back to the DOM and animate them in to position << breaks here
Then I run the queue.
At the moment due to some sort of issue with the elements stored in the array when I try to add the elements from the array back in to the DOM nothing is added.
Here's my code:
jQuery.each(self.filterSet, function (i, e) {
//loop thru array queing up hiding of elements
var self = this;
if ((i + 1) < filterSetLength) {
theQueue.queue("Q1", function (next) {
self = $(self).detach();
next();
});
} else {
//break on last element so that animation doesn't overlap with showing of filtered elements
theQueue.queue("Q1", function (next) {
self = $(self).detach();
next();
});
}
});
self.filterSet.sort(function (a, b) {
var c = parseInt($(a).attr('data-views'));
var d = parseInt($(b).attr('data-views'));
if (c < d) {
return 1;
}
if (c > d) {
return -1;
}
return 0;
});
self.location.find('li:not(.cloned) ul.tiles').each(function (i) {
//per panel....
var limit = 11 * (i + 1);
var self = this;
for (e = 0; e < limit; e++) {
if (filterSet[e] != undefined) {
theQueue.queue("Q2", function (next) {
$(self).append(filterSet[e]).show().fadeIn();
next();
});
} else {
break;
}
}
});
//add second queue in to end of first queue
theQueue.queue("Q1", function (next) {
theQueue.dequeue("Q2");
next();
});
//run everything
theQueue.dequeue("Q1");
Basically I simply have an array of things from jQuery:
var filterSet = new Array();
var filterSet = this.find('li:not(.cloned) ul.tiles li').each(function () {
filterSet.push(this);
});
and I want to sort them and then put them in to the DOM.... for some reason it won't work...
I really can't follow what you're trying to do with your code. Here's a simple jQuery code block that takes a set of li tags, removes them from the DOM, sorts them by their data-views data and reinserts them in sorted order which I think is what you asked for.
You can see it work here: http://jsfiddle.net/jfriend00/xwTsb/
HTML:
<button id="pressMe" type="button">Sort</button><br><br>
<ul id="master">
<li data-views="5">Fifth</li>
<li data-views="2">Second</li>
<li data-views="6">Sixth</li>
<li data-views="3">Third</li>
<li data-views="1">First</li>
<li data-views="4">Fourth</li>
</ul>
JS:
$("#pressMe").click(function() {
var savedObjects = [];
$("#master li").each(function(i, o){
var data = new Object();
data.views = $(this).attr("data-views"); // get data for sorting
data.o = $(this).detach(); // detach and save
savedObjects.push(data); // save for later
});
// sort by view data (treated as a number)
savedObjects.sort(function(a,b) {return(Number(a.views) - Number(b.views))});
var master = $("#master");
$.each(savedObjects, function(i, o) {
master.append(this.o); // add back to DOM
});
});
I leave it to you to add whatever animation you want to this.