Stop link from loading - javascript

I have been trying for a few hours now and decided to finally post here. I have been trying so many methods to get my links to stop with preventDefault, this way I can load in content async.
All my links that i have tried that are apart of the template (like in the header) work perfectly, but any of the links brought in through an async view (and generated via js) completely skip over this snippet of code. I wanted to track anything that has a className "router" and stop the default action and run the function navigate instead.
var routeclicked = document.getElementsByClassName('router');
for(let i = 0; i < routeclicked.length; i++) {
routeclicked[i].addEventListener("click", e => {
console.log("Router is: " + routeclicked[i]);
e.preventDefault();
router = routeclicked[i].pathname;
console.log("Router 2 is: " + router);
navigate();
});
}

Attach a listener to "body" or the first non-dynamic parent.
You could go for Event.target and .closest().
Also, don't put functions inside for loops, it defies the reusability of functions.
const navigate = (EL) => {
location = EL.pathname;
}
const navigateHandler = (ev) => {
const EL = ev.target.closest(".router"); // Self or closest
if (!EL) return;
ev.preventDefault();
navigate(EL);
}
document.querySelector("body").addEventListener("click", navigateHandler);

Related

Why doesn't my class names update when set though JS?

This may be a really simple problem but I can't seem to find why this is happening. I'm trying to develop a SPA in vanilla js using webpack, so far I was able to implement routing
with hashchange event and triggering rerendering. But when I tried to add an active class to the relevant link though when the hash changes, It doesn't work. But when I log to the console, it seems that class was added successfully, but in the HTML it doesn't get updated. Why is this?
this is my hashchange listener,
window.addEventListener("hashchange", (e) => {
const hash = window.location.hash.replace("#", "");
const view = routes.find((route) => {
return route.path == hash;
});
const links = document.querySelectorAll(".nav-list--link");
app.render(view.name);
links.forEach((l) => {
const hashHref = l.getAttribute("href").replace("/#", "");
if (hash === hashHref) {
l.classList.add("active");
console.log(l, l.classList);
} else {
l.classList.remove("active");
console.log(l, l.classList);
}
});
});
And this is the console output,
This is the HTML,
I don't understand why it doesn't update in the HTML if it's shown as updated in Javascript

JS DOM issue: Uncaught TypeError: Cannot read property 'addEventListener' of null becomes Uncaught Reference Error

This is code that i've downloaded from a tutorial here: (https://codepen.io/Web_Cifar/pen/PoNNEYY) in hopes of adapting it to something that i am working on. I've pieced it together and while it works in the demo (and there's a nice YT video where he goes thru it), it doesn't work for me in a live situation. I am attempting to build a Gravity Forms-like multi-page data entry form (GF has some quirks that don't work for me).
The first Javascript error that i got was: "Uncaught TypeError: Cannot read property 'addEventListener' of null". Researching that here on StackO, i got the idea that the general advice is that perhaps the DOM is not loaded for my document yet and we are calling JS before that has taken place, and two suggestions seem popular:
1). Move your JS file to the bottom of your HTML document, just before the close of your body tag (it WAS originally in my tag... moving it changed nothing).
2. Wrap your JS function in question in the following code: "window.addEventListener('DOMContentLoaded', (event) => {"
to force the DOM to load before calling the function.
Doing #2 changed the error to: ReferenceError: "changeStep" is not defined. There IS a function called 'changeStep' in the JS code. If you reference the tutorial above, you'll see that it was originally the last function defined in the JS file, so i thought that moving it to the top would change it. No dice. I've done some checks to see if some simple JQuery tests work in my environment and they do. I am relatively new to JS but i don't see why the DOM would not be loaded nor do i see why the function in question cannot be referenced.
Here is a skeleton of my HTML code:
<html>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script> /* A recommended test to see if jQuery is working, and it is. */
$(document).ready(function(){
$("button").click(function(){
alert("jQuery is working perfectly.");
});
});
</script>
<link rel="stylesheet" href="/Users/Me/Documents/multistep.css">
<body>
etc.....(moving to end of html file....)
<script type = "text/javascript" src = "/Users/Me/Documents/multistep.js" ></script>
</body>
</html>
And here is my entire JS script:
const steps = Array.from(document.querySelectorAll("form .step"));
const nextBtn = document.querySelectorAll("form .next-btn");
const prevBtn = document.querySelectorAll("form .previous-btn");
const form = document.querySelector("form");
window.addEventListener('DOMContentLoaded', (event) => {
function changeStep(btn) {
let index = 0;
const active = document.querySelector(".active");
index = steps.indexOf(active);
steps[index].classList.remove("active");
if (btn === "next") {
index++;
} else if (btn === "prev") {
index--;
}
steps[index].classList.add("active");
}
});
window.addEventListener('DOMContentLoaded', (event) => {
nextBtn.forEach((button) => {
button.addEventListener("click", () => {
changeStep("next");
});
});
});
window.addEventListener('DOMContentLoaded', (event) => {
prevBtn.forEach((button) => {
button.addEventListener("click", () => {
changeStep("prev");
});
});
});
window.addEventListener('DOMContentLoaded', (event) => {
form.addEventListener("submit", (e) => {
e.preventDefault();
const inputs = [];
form.querySelectorAll("input").forEach((input) => {
const { name, value } = input;
inputs.push({ name, value });
});
console.log(inputs);
form.reset();
});
});
Please note that i wrapped basically everything in the "window.addEventListener..." tag (as per advice Googled above) and the only other change i made from the referenced demo (that works in codePen) was to move the changeStep function to the first function in the file.
I'm at a loss as to what is going on. Can an experienced JS guy help me get un-stuck here?
changeStep is defined inside the anonymous callback function of the first event listener, so it is only visible inside that function. If you want to access it outside that function's scope, you have to define it outside of the function, i.e. before calling addEventListener. But a better solution is to put everything in one event listener. I do not see a reason why there have to be three of them.
Besides, since you are still calling querySelector outside the event listener, it does not solve the original problem. That is exactly the thing you want to do after the DOM context has been loaded, so you have to put it inside the event listener, too.
So the solution is to put basically everything in one event listener:
window.addEventListener('DOMContentLoaded', (event) => {
const steps = Array.from(document.querySelectorAll("form .step"));
const nextBtn = document.querySelectorAll("form .next-btn");
const prevBtn = document.querySelectorAll("form .previous-btn");
const form = document.querySelector("form");
function changeStep(btn) {
let index = 0;
const active = document.querySelector(".active");
index = steps.indexOf(active);
steps[index].classList.remove("active");
if (btn === "next") {
index++;
} else if (btn === "prev") {
index--;
}
steps[index].classList.add("active");
}
nextBtn.forEach((button) => {
button.addEventListener("click", () => {
changeStep("next");
});
});
prevBtn.forEach((button) => {
button.addEventListener("click", () => {
changeStep("prev");
});
});
});
This should answer your question about why you get the second error, but actually putting the script at the end of the body tag should have solved the problem in the first place, so the first error probably has a different reason. Are you sure that your script tag was at the very end of the body tag, and that you actually have all the elements you are querying in your DOM tree?

addEventListener only firing once

I'm adding a click event to all links that match a particular selector as part of a JS module I'm creating. It looks something like this.
var Lightbox = (function () {
var showLightbox = function () {
// this does stuff
};
var init = function () {
var links = document.querySelectorAll(options.selector);
for(var i = 0; i < links.length; i++) {
links[i].addEventListener('click', function() {
showLightbox();
}, false);
}
};
return {
init: init
};
})();
Lightbox.init();
On first load the any links on the page that match the selector work. There is also a closeLightbox() method that works fine. However when clicking the links for a second time nothing happens. I get no console errors – nuffin.
Is there something I'm doing wrong when adding the event listener?
EDIT: I've updated the code to remove some redundant methods and have pasted the full code here: http://pastebin.com/mC8pSAV2
You are reassigning innerHTML of the whole document:
document.body.innerHTML += response;
on the link click. That wipes out all existing DOM elements with their events and creates new DOM structure with no clicks assigned.

Click all anchor tags on page with given class, but cancel prior to navigation

Trying to automate some testing for some analytics tracking code, and I'm running into issues when I try passing links into the each() method.
I copied a lot of this from stackoverflow - how to follow all links in casperjs, but I don't need return the href of the link; I need to return the link itself (so I can click it). I keep getting this error: each() only works with arrays. Am I not returning an array?
UPDATE:
For each anchor tag that has .myClass, click it, then return requested parameters from casper.options.onResourceReceived e.g. event category, event action, etc. I may or may not have to cancel the navigation the happens after the click; I simply only need to review the request, and do not need the follow page to load.
Testing steps:
click link that has .myClass
look at request parameters
cancel the click to prevent it from going to the next page.
I'm new to javascript and casper.js, so I apologize if I'm misinterpreting.
ANOTHER UPDATE:
I've updated the code to instead return an array of classes. There are a few sketchy bits of code in this though (see comments inline).
However, I'm now having issues canceling the navigation after the click. .Clear() canceled all js. Anyway to prevent default action happening after click? Like e.preventDefault();?
var casper = require('casper').create({
verbose: true,
logLevel: 'debug'
});
casper.options.onResourceReceived = function(arg1, response) {
if (response.url.indexOf('t=event') > -1) {
var query = decodeURI(response.url);
var data = query.split('&');
var result = {};
for (var i = 0; i < data.length; i++) {
var item = data[i].split('=');
result[item[0]] = item[1];
}
console.log('EVENT CATEGORY = ' + result.ec + '\n' +
'EVENT ACTION = ' + result.ea + '\n' +
'EVENT LABEL = ' + decodeURIComponent(result.el) + '\n' +
'REQUEST STATUS = ' + response.status
);
}
};
var links;
//var myClass = '.myClass';
casper.start('http://www.leupold.com', function getLinks() {
links = this.evaluate(function() {
var links = document.querySelectorAll('.myClass');
// having issues when I attempted to pass in myClass var.
links = Array.prototype.map.call(links, function(link) {
// seems like a sketchy way to get a class. what happens if there are multiple classes?
return link.getAttribute('class');
});
return links;
});
});
casper.waitForSelector('.myClass', function() {
this.echo('selector is here');
//this.echo(this.getCurrentUrl());
//this.echo(JSON.stringify(links));
this.each(links, function(self, link) {
self.echo('this is a class : ' + link);
// again this is horrible
self.click('.' + link);
});
});
casper.run(function() {
this.exit();
});
There are two problems that you're dealing with.
1. Select elements based on class
Usually a class is used multiple times. So when you first select elements based on this class, you will get elements that have that class, but it is not guaranteed that this will be unique. See for example this selection of element that you may select by .myClass:
myClass
myClass myClass2
myClass myClass3
myClass
myClass myClass3
When you later iterate over those class names, you've got a problem, because 4 and 5 can never be clicked using casper.click("." + links[i].replace(" ", ".")) (you need to additionally replace spaces with dots). casper.click only clicks the first occurrence of the specific selector. That is why I used createXPathFromElement taken from stijn de ryck to find the unique XPath expression for every element inside the page context.
You can then click the correct element via the unique XPath like this
casper.click(x(xpathFromPageContext[i]));
2. Cancelling navigation
This may depend on what your page actually is.
Note: I use the casper.test property which is the Tester module. You get access to it by invoking casper like this: casperjs test script.js.
Note: There is also the casper.waitForResource function. Have a look at it.
2.1 Web 1.0
When a click means a new page will be loaded, you may add an event handler to the page.resource.requested event. You can then abort() the request without resetting the page back to the startURL.
var resourceAborted = false;
casper.on('page.resource.requested', function(requestData, request){
if (requestData.url.match(/someURLMatching/)) {
// you can also check requestData.headers which is an array of objects:
// [{name: "header name", value: "some value"}]
casper.test.pass("resource passed");
} else {
casper.test.fail("resource failed");
}
if (requestData.url != startURL) {
request.abort();
}
resourceAborted = true;
});
and in the test flow:
casper.each(links, function(self, link){
self.thenClick(x(link));
self.waitFor(function check(){
return resourceAborted;
});
self.then(function(){
resourceAborted = false; // reset state
});
});
2.2 Single page application
There may be so many event handlers attached, that it is quite hard to prevent them all. An easier way (at least for me) is to
get all the unique element paths,
iterate over the list and do every time the following:
Open the original page again (basically a reset for every link)
do the click on the current XPath
This is basically what I do in this answer.
Since single page apps don't load pages. The navigation.requested and page.resource.requested will not be triggered. You need the resource.requested event if you want to check some API call:
var clickPassed = -1;
casper.on('resource.requested', function(requestData, request){
if (requestData.url.match(/someURLMatching/)) {
// you can also check requestData.headers which is an array of objects:
// [{name: "header name", value: "some value"}]
clickPassed = true;
} else {
clickPassed = false;
}
});
and in the test flow:
casper.each(links, function(self, link){
self.thenOpen(startURL);
self.thenClick(x(link));
self.waitFor(function check(){
return clickPassed !== -1;
}, function then(){
casper.test.assert(clickPassed);
clickPassed = -1;
}, function onTimeout(){
casper.test.fail("Resource timeout");
});
});

Back-Forward buttons of browser are showing weird behaviour. History.js

I have a <select> tag. On changing the selected item, some <div>s are updated using AJAX via the functions: update_div1, update_div2, update_div3. The code managing this operation is below and working well:
$('select.select-x').change(function(e) {
e.preventDefault();
var value = $('select.select-x option:selected').val();
var text = $('option:selected').text();
update_div1(value);
update_div2(value);
update_div3(value);
manage_history(value,text);
return false;
});
The problem is in the last function manage_history which is responsable of managing browser history. In this function I am using history.js to push browser states in history stack. As I know after reading many articles on SO about history.js, the actions of back and forward buttons should be inclueded in History.Adapter.bind(). The problem is that the back-forward buttons of the browser are having weird behaviour: They execute the functions inside History.Adapter.bind() many times, exaclty the number of times I already had an onChange event triggerd via <select>.
The code of manage_history is:
function manage_history(str,str2)
{
var History = window.History;
if ( !History.enabled ) { return false; }
var path = 'http://localhost/enc/?p='+str;
History.pushState({myid:str},str2,path);
History.Adapter.bind(window,'statechange',function() {
var State = History.getState();
update_div1(State.data.myid); fter
update_div2(State.data.myid);
update_div3(State.data.myid);
});
}
I hope I was clear. Your help is appreciated.
The whole Solution after theBrain contribution:
$('select.select-x').change(function(e) {
e.preventDefault();
var value = $('select.select-x option:selected').val();
var text = $('option:selected').text();
manage_history(value,text);
return false;
});
History.Adapter.bind(window,'statechange',function() { // Note: Using statechange instead of popstate
var State = History.getState();
update_div1(value);
update_div2(value);
update_div3(value);
});
function manage_history(str,str2)
{
var History = window.History;
if ( !History.enabled ) { return false; }
var path = 'http://localhost/enc/?p='+str;
History.pushState({myid:str},str2,path);
update_div1(State.data.myid);
update_div2(State.data.myid);
update_div3(State.data.myid);
}
move the History.Adapter.bind away from the manage_history function. You rebind it every time you call the manage_history function.

Categories