How to optimize native JavaScript code when refactoring it from Jquery - javascript

I need to rewrite some Jquery into native JavaScript code but I am facing a problem that I am not so sure how to solve.
This is Jquery code I need to rewrite in native JS:
$('.class1').click(function () {
setTimeout(() => {
$('.class2').css('top', '252px');
$('.class3').css('bottom', '0px');
}, 200);
$('.class2').css('z-index', '-1');
$('.class1').css('z-index', '-1');
});
And this is what I have written in native JavaScrip:
if (document.querySelector('.class1')){
document.querySelector('.class1').addEventListener('click', function () {
setTimeout(() => {
if (document.querySelector('.class2')) {
document.querySelector('.class2').style.top = '252px';
}
if ( document.querySelector('.class3')) {
document.querySelector('.class3').style.bottom = '0px';
}
}, 200);
if (document.querySelector('.class2')) {
document.querySelector('.class2').style.zIndex = '-1';
}
if ( document.querySelector('.class1')) {
document.querySelector('.class1').style.zIndex = '-1';
}
})
}
I was hoping that people could explain to me how to solve two things:
Is there a more elegant way to check for an element on the current page if the code runs on the whole site?
Is there something else that I can replace those if statements inside the function?
In Jquery those statements are executed one by one but in my case I need to check for an element first and if it is there do something with it.

You can make the code more succinct by storing the result of querySelector within a variable. Also note that a class selector in jQuery can return multiple elements, so the native equivalent of it is querySelectorAll().
As such you will need to loop through all the elements in that collection and add the event handlers, or update their style, as necessary. Due to this loop you don't need to explicitly check for the existence of the elements, as the forEach() will simply not execute if the collection is empty.
With that said, try this:
let class1 = document.querySelectorAll('.class1');
let class2 = document.querySelectorAll('.class2');
let class3 = document.querySelectorAll('.class3');
class1.forEach(el => {
el.addEventListener('click', e => {
setTimeout(() => {
class2.forEach(el => el.style.top = '252px');
class3.forEach(el => el.style.top = '0px');
}, 200);
class1.forEach(el => el.style.zIndex = -1);
class2.forEach(el => el.style.zIndex = -1);
});
});

Related

How do i consol.log countRight with this code?

function rightAnswer() {
console.log('right')
var countRight = 0;
[Button2, Buttonb3].forEach(a =>
a.addEventListener('click', () => {
countRight += 1;
})
);
}
I need help with being able to print consol.log(countRight). So far when i use that line of code the consol always shows countRight = 0 even though it should say either 1 or 2 depending on the users input. I need help with making this code work.
It does not work if i put consol.log(countRight) after countRight += 1;
function rightAnswer() {
var countRight = 0;
[Button2, Buttonb3].forEach(a =>
a.addEventListener('click', () => {
countRight += 1;
// this is where you should be able to log the value
console.log(`right value is: ${countRight}`);
})
);
}
This is spaghetti code though. Your function breaks scope by referencing "Button2", "Buttonb3"
If you want to log on clicks the right place to log is in the click handler.
The implication from naming is that countRight will be called to increment the count–as it is no handlers will be added until countRight has been called (and it should be called only once so multiple handlers aren't added).
It might make more sense to add the handlers on the load event. If you really mean to add them at an arbitrary time in the future it can certainly be put in a method, noting again it should only be called once, or the event listeners removed before calling it again.
window.addEventListener('load', () => {
let countRight = 0
const button1 = document.getElementById('button1')
const button2 = document.getElementById('button2')
const buttons = [button1, button2]
const output = document.getElementById('output')
buttons.forEach(b => {
b.addEventListener('click', b => {
countRight++
output.innerText = countRight
console.log(`countRight = ${countRight}`)
})
})
}, false);
<button id="button1">Button #1</button>
<button id="button2">Button #2</button>
<div>Clicked <span id="output">0</span> times.</div>
Tangential
The reason to post a complete example is so people can copy it into their answers and actually run it. It also makes sense to

How can I convert scrollIntoView with smooth animation to a Promise?

I have to scrollIntoView a particular element smoothly and then do something.
Example
element.scrollIntoView({behavior: 'smooth'}).then(() => {
// Do something here
})
I know that it can't be done this way as native scrollIntoView doesn't return a Promise. But, how do I achieve something like this?
I'm using Angular 7 BTW. So if there are any directives that could help me achieve this, it would be great.
You can work with prototypes, I think this could be a solution to your problem without download any npm packages
/* Extends Element Objects with a function named scrollIntoViewPromise
* options: the normal scrollIntoView options without any changes
*/
Element.prototype.scrollIntoViewPromise = function(options){
// "this" refers to the current element (el.scrollIntoViewPromise(options): this = el)
this.scrollIntoView(options);
// I create a variable that can be read inside the returned object ({ then: f() }) to expose the current element
let parent = this;
// I return an object with just a property inside called then
// then contains a function which accept a function as parameter that will be execute when the scroll ends
return {
then: function(x){
// Check out https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API for more informations
const intersectionObserver = new IntersectionObserver((entries) => {
let [entry] = entries;
// When the scroll ends (when our element is inside the screen)
if (entry.isIntersecting) {
// Execute the function into then parameter and stop observing the html element
setTimeout(() => {x(); intersectionObserver.unobserve(parent)}, 100)
}
});
// I start to observe the element where I scrolled
intersectionObserver.observe(parent);
}
};
}
element.scrollIntoViewPromise({behavior: "smooth"}).then(()=>console.log("EHI!"));
I've created an example. I know it's not an angular application, but it's a good starting point. You just need to implement it (If you're using typescript you have to create an interface which extends Element with the new function).
One way you can solve this is by using smooth-scroll-into-view-if-nedded it actually return a promise so you can bind to it and apply your logic.
There is an idea how you may catch animation ending.
You may do it in vanilla JS with a 'scroll' event listener.
Check this example https://codepen.io/Floky87/pen/BEOYvN
var hiddenElement = document.getElementById("box");
var btn = document.querySelector(".btn");
var isScrolling;
function handleScroll(event) {
// Clear our timeout throughout the scroll
window.clearTimeout(isScrolling);
// Set a timeout to run after scrolling ends
isScrolling = setTimeout(function() {
alert(1);
document.removeEventListener("scroll", handleScroll);
}, 66);
}
function handleButtonClick() {
document.addEventListener("scroll", handleScroll, false);
hiddenElement.scrollIntoView({ block: "center", behavior: "smooth" });
}
btn.addEventListener("click", handleButtonClick);
I made it like this
const scrollIntoViewPromise = async (node: HTMLElement, options?: ScrollIntoViewOptions) => {
node.scrollIntoView(options);
return new Promise((resolve) => {
const intersectionObserver = new IntersectionObserver((entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setTimeout(() => {
resolve(true);
intersectionObserver.unobserve(node);
}, 100);
}
});
intersectionObserver.observe(node);
});
};

Confirming that using a factory is the best (only?) way to create a generic multi-use click count listener

I have created the following click count factory for adding the click count to the event information already present in a regular click event. This function creates a clickCountObj to track the number of clicks, as well as a new function for catching a click event on the given element parameter and reporting it back to the listener parameter along with the click count.
Originally, I wanted to do this as a class, rather than a factory... Way back when I was working in Java, I would have done it with a façade class, so that's what I was thinking. But I've concluded that it is not possible in Javascript, because the same function you'd use to create the object would be the one called in response to the click, and I can't see a way around this.
The purpose of this question is simply to improve my understanding and using of JavaScript. Please let me know if I am wrong in my conclusion stated above, or if there are any other alternatives to doing this a better way?
function clickCount(element, listener) {
let clickCountObj = {};
clickCountObj.clickCount = 0;
clickCountObj.clickDelay = 500;
clickCountObj.element = element;
clickCountObj.lastClickTime = 0;
let clickCountListener = function (e) {
// alert("last click time: " + clickCountObj.clickDelay);
if ((e.timeStamp - clickCountObj.clickDelay) < clickCountObj.lastClickTime) {
clickCountObj.clickCount = clickCountObj.clickCount + 1;
// alert("click count up: " + clickCountObj.clickCount);
}
else {
clickCountObj.clickCount = 1;
}
clickCountObj.lastClickTime = e.timeStamp;
listener.call(element, clickCountObj.clickCount, e);
};
if (!element) throw "No element to listener to";
element.addEventListener("click", clickCountListener);
return clickCountListener;
}
For sure you can also use a class:
class ClickCounter {
constructor(element, onClick, delay = 500) {
this.element = element;
this.onClick = onClick;
this.counter = 0;
this.delay = delay;
this.lastClicked = 0;
element.addEventListener("click", () => this.click(), false);
}
click() {
if(Date.now() < this.lastClicked + this.delay)
return;
this.lastClicked = Date.now();
this.onClick.call(this.element, this.counter++);
}
}
new ClickCounter(document.body, count => {
alert(count);
});
[is] doing this a better way?
No, not really. Using a class is not really useful here as you don't want to expose properties and you also don't need inheritance. A factory seems to be a good approach here.
Small sidenote: Instead of
return clickCountListener;
it would make more sense to
return clickCountObj;
as it would expose the settings and the count which might be useful.
warning: unserious content below
Way back when I was working in Java ...
... you took over that senseless naming scheme (clickCountObj.clickCount). I guess you won't loose any necessary information with just settings.count ...

Cypress IO- Writing a For Loop

I have 15 buttons on a page. I need to test each button.
I tried a simple for loop, like
for (var i = 1; i < 15; i++) {
cy.get("[=buttonid=" + i + "]").click()
}
But Cypress didn't like this. How would I write for loops in Cypress?
To force an arbitrary loop, I create an array with the indices I want, and then call cy.wrap
var genArr = Array.from({length:15},(v,k)=>k+1)
cy.wrap(genArr).each((index) => {
cy.get("#button-" + index).click()
})
Lodash is bundled with Cypress and methods are used with Cypress._ prefix.
For this instance, you'll be using the _.times. So your code will look something like this:
Cypress._.times(15, (k) => {
cy.get("[=buttonid=" + k + "]").click()
})
You can achieve something similar to a "for loop" by using recursion.
I just posted a solution here: How to use a while loop in cypress? The control of is NOT entering the loop when running this spec file? The way I am polling the task is correct?
Add this to your custom commands:
Cypress.Commands.add('recursionLoop', {times: 'optional'}, function (fn, times) {
if (typeof times === 'undefined') {
times = 0;
}
cy.then(() => {
const result = fn(++times);
if (result !== false) {
cy.recursionLoop(fn, times);
}
});
});
Then you can use it by creating a function that returns false when you want to stop iterating.
cy.recursionLoop(times => {
cy.wait(1000);
console.log(`Iteration: ${times}`);
console.log('Here goes your code.');
return times < 5;
});
While cy.wrap().each() will work (one of the answers given for this question), I wanted to give an alternate way that worked for me. cy.wrap().each() will work, but regular while/for loops will not work with cypress because of the async nature of cypress. Cypress doesn't wait for everything to complete in the loop before starting the loop again. You can however do recursive functions instead and that waits for everything to complete before it hits the method/function again.
Here is a simple example to explain this. You could check to see if a button is visible, if it is visible you click it, then check again to see if it is still visible, and if it is visible you click it again, but if it isn't visible it won't click it. This will repeat, the button will continue to be clicked until the button is no longer visible. Basically the method/function is called over and over until the conditional is no longer met, which accomplishes the same thing as a for/while loop, but actually works with cypress.
clickVisibleButton = () => {
cy.get( 'body' ).then( $mainContainer => {
const isVisible = $mainContainer.find( '#idOfElement' ).is( ':visible' );
if ( isVisible ) {
cy.get( '#idOfElement' ).click();
this.clickVisibleButton();
}
} );
}
Then obviously call the this.clickVisibleButton() in your test. I'm using typescript and this method is setup in a class, but you could do this as a regular function as well.
// waits 2 seconds for each attempt
refreshQuote(attempts) {
let arry = []
for (let i = 0; i < attempts; i++) { arry.push(i) }
cy.wrap(arry).each(() => {
cy.get('.quote-wrapper').then(function($quoteBlock) {
if($quoteBlock.text().includes('Here is your quote')) {
}
else {
cy.get('#refreshQuoteButton').click()
cy.wait(2000)
}
})
})
}
Try template literals using backticks:
for(let i = 0; i < 3; i++){
cy.get(`ul li:nth-child(`${i}`)).click();
}

Reuse of functions

I think I know the theory behind the solution but I am having trouble implementing it. Consider following piece of code:
this.selectFirstPassiveService = function () {
getFirstPassiveService().element(by.tagName('input')).click();
}
this.clickAddTask = function () {
getFirstActiveService().element(by.tagName('a')).click();
}
this.selectTask = function () {
getFirstActiveService()
.element(by.tagName('option'))
.$('[value="0"]')
.click();
}
this.saveTask = function () {
getFirstActiveService().element(by.name('taskForm')).submit();
}
getFirstActiveService = function () {
return services.filter(function (elem) {
return elem.getAttribute('class').then(function (attribute) {
return attribute === 'service active ';
});
}).first();
}
getFirstPassiveService = function () {
return services.filter(function (elem) {
return elem.getAttribute('class').then(function (attribute) {
return attribute === 'service passive ';
});
}).first();
}
};
To minimalize code duplication, I created two functions:
* getFirstActiveService()
* getFirstPassiveService()
My spec goes as follows:
it('Select service', function () {
servicePage.selectFirstPassiveService();
servicePage.clickAddTask();
servicenPage.selectTask()();
});
Both clickAddTask() and selectTask() use the function called getFirstActiveService(). Everything runs fine in clickAddTask() but when I use the function in selectTask(), some elements (which are present) cannot be found by protractor.
Here goes my theory, every command in getFirstActiveService() is queued in the control flow when the function is called in clickAddTask() and is then executed. When reusing the function in selectTask() the commands aren't queued, the instance created in clickAddTask() is used and therefore, some elements cannot be found since the DOM has changed since then.
Now first question: Is my theory correct?
Second question: How can I fix this?
Thanks in advance!
Cheers
I refactored it a bit and it works now.
The problem was in the test itself, not in the reuse of functions.

Categories

Resources