Reputation: 25
I built a train tracker in Ruby on Rails. I’m running into some javascript issues though.
JS updates the train stops dropdown when a train line is selected. But it doesn’t work when you leave the page and come back, for example, go from this page to Projects via the top menu, then back to the Chicago Train Tracker page via the link. You cannot pick a line anymore.
From what I can tell, it’s because it’s all one "session" and despite leaving the page and coming back, the original JS is still loaded and that… causes it to break somehow? I can’t figure out why but I’m guessing it’s being caused by some rails magic that makes page loading quicker.
Does anyone have any tips on how I can resolve this? For further context, I have the JS right in the .html.erb
. file (because I’m still working out how to serve .js files via the Rails assets pipeline 🥴) I’m not sure if that’s related so I'm hoping that that will be a problem for another day.
I'm not sure that the JS is the issue but here's the JS on the page:
<script>
const REDLINE = {
"47th (95th-bound)": "30238",
"47th (Howard-bound)": "30237",
};
const BLUELINE = {
"Addison (Forest Pk-bound)": "30240",
"Addison (O'Hare-bound)": "30239",
};
const PURPLELINE = {
"Central (Howard-Loop-bound)": "30242",
"Central (Linden-bound)": "30241",
};
const GREENLINE = {
"35-Bronzeville-IIT (63rd-bound)": "30214",
"35-Bronzeville-IIT (Harlem-bound)": "30213",
};
const ORANGELINE = {
"35th/Archer (Loop-bound)": "30022",
"35th/Archer (Midway-bound)": "30023",
};
const PINKLINE = {
"18th (54th/Cermak-bound)": "30162",
"18th (Loop-bound)": "30161",
};
const YELLOWLINE = {
"Dempster-Skokie (Arrival)": "30026",
"Dempster-Skokie (Howard-bound)": "30027",
};
const linePicker = document.getElementById("line");
const stopPicker = document.getElementById("stop");
function linePick() {
var pickList = '<option value="null">Pick a Stop!</option>';
if (linePicker.value == "red") {
var lineKeys = Object.keys(REDLINE);
for (let i = 0; i < lineKeys.length; i++) {
var key = lineKeys[i];
pickList += '\n<option value="' + REDLINE[key] + '">' + key + "</option>";
}
} else if (linePicker.value == "brown") {
var lineKeys = Object.keys(BROWNLINE);
for (let i = 0; i < lineKeys.length; i++) {
var key = lineKeys[i];
pickList += '\n<option value="' + BROWNLINE[key] + '">' + key + "</option>";
}
} else if (linePicker.value == "blue") {
var lineKeys = Object.keys(BLUELINE);
for (let i = 0; i < lineKeys.length; i++) {
var key = lineKeys[i];
pickList += '\n<option value="' + BLUELINE[key] + '">' + key + "</option>";
}
} else if (linePicker.value == "green") {
var lineKeys = Object.keys(GREENLINE);
for (let i = 0; i < lineKeys.length; i++) {
var key = lineKeys[i];
pickList += '\n<option value="' + GREENLINE[key] + '">' + key + "</option>";
}
} else if (linePicker.value == "purple") {
var lineKeys = Object.keys(PURPLELINE);
for (let i = 0; i < lineKeys.length; i++) {
var key = lineKeys[i];
pickList += '\n<option value="' + PURPLELINE[key] + '">' + key + "</option>";
}
} else if (linePicker.value == "purple") {
var lineKeys = Object.keys(PURPLELINE);
for (let i = 0; i < lineKeys.length; i++) {
var key = lineKeys[i];
pickList += '\n<option value="' + PURPLELINE[key] + '">' + key + "</option>";
}
} else if (linePicker.value == "yellow") {
var lineKeys = Object.keys(YELLOWLINE);
for (let i = 0; i < lineKeys.length; i++) {
var key = lineKeys[i];
pickList += '\n<option value="' + YELLOWLINE[key] + '">' + key + "</option>";
}
} else if (linePicker.value == "pink") {
var lineKeys = Object.keys(PINKLINE);
for (let i = 0; i < lineKeys.length; i++) {
var key = lineKeys[i];
pickList += '\n<option value="' + PINKLINE[key] + '">' + key + "</option>";
}
} else if (linePicker.value == "orange") {
var lineKeys = Object.keys(ORANGELINE);
for (let i = 0; i < lineKeys.length; i++) {
var key = lineKeys[i];
pickList += '\n<option value="' + ORANGELINE[key] + '">' + key + "</option>";
}
}
stopPicker.innerHTML = pickList;
}
function stopPick() {
var pickedStop = stopPicker.value;
console.log(pickedStop);
window.location.href = window.location.origin + `/projects/train_tracker/${pickedStop}`
};
</script>
I shortened the constants so the example isn't too long. The two functions are called by the form onChange
. Also worth noting that I don't have this issue when I run it locally. It only occurs on the Heroku-hosted live page.
Any help is appreciated.
Upvotes: 2
Views: 632
Reputation: 101911
You have got a lot of things backwards here - and thats pretty natural as Rails is a horrible way to learn JavaScript.
preload
has nothing to do with javascript, its part of the ActiveRecord query interface and is used to preload the assocatiated records to avoid N+1 queries.
js.erb
templates are in no way a valid replacement for serving files via the assets pipeline. Instead they were used by the now replaced Rails UJS to make a poor mans AJAX framework.
It adds an event handler to link and form elements with the data-remote
attribute that makes them send XHR requests to the backend with the application/javascript
mime type. You would respond to that with Javascript that Rails UJS "eval'ed" by creating a script tag inserting it into the page. That lets you use the backend for templating HTML. They should NOT contain <script>
tags.
It has been replaced by the much more compent Turbo in Rails 7.
From what I can tell, it’s because it’s all one "session" and despite leaving the page and coming back, the original JS is still loaded and that… causes it to break somehow?
turbo
and the old turbolinks
both do page replacement via XHR requests.
With turbo/turbolinks inline script tags with non-idempotent code are a disaster as the code they contain will be run multiple times and you get errors like the one you have since the constants will be redeclared. There really is no replacement for figuring out the assets pipeline and writing your JS in separate files.
You also have to think differently about how you're attaching event handlers to elements and interacting with the DOM. The window load event will only fire once at the initial page load so if you're just using that to attach event handlers directly to the elements they will not be present on dynamically inserted elements.
window.addEventListener("load", (event) => {
let btn = document.getElementById('btn');
btn.addEventListener("click", function(){
console.log("This event handler will only work on the initial page load")
});
});
The solution to that problem is to write delegated event handlers or use the events fired by Turbo/Turbolinks when it does page replacement to perform non-idempotent transformations.
document.addEventListener("click", (event) => {
if (event.target.id !== "btn") { return };
console.log("This event handler will work even on dynamically inserted elements");
});
It should be noted that this problem isn't unique to Rails. It occurs whenever you're dealing with elements that are dynamically inserted into the DOM. Rails just tosses you off the deepend into some quite advanced JS topics and makes a mess of it - I would recommend that you try to actually learn the basics of JS outside of Rails and then revisit it.
As for the script here - what you actually want is something like:
// This data structure is very awkward - you would have less issues if
// you used an array and the name of the stop was a property of a nested object instead of the keys
const lines = {
red: {
"47th (95th-bound)": "30238",
"47th (Howard-bound)": "30237",
},
blue: {
"Addison (Forest Pk-bound)": "30240",
"Addison (O'Hare-bound)": "30239",
},
purple: {
"Central (Howard-Loop-bound)": "30242",
"Central (Linden-bound)": "30241",
},
green: {
"35-Bronzeville-IIT (63rd-bound)": "30214",
"35-Bronzeville-IIT (Harlem-bound)": "30213",
},
orange: {
"35th/Archer (Loop-bound)": "30022",
"35th/Archer (Midway-bound)": "30023",
},
pink: {
"18th (54th/Cermak-bound)": "30162",
"18th (Loop-bound)": "30161",
},
yellow: {
"Dempster-Skokie (Arrival)": "30026",
"Dempster-Skokie (Howard-bound)": "30027",
}
};
function reloadStopOptions(stops) {
const fragment = document.createDocumentFragment();
let defaultOption = document.createElement('option');
defaultOption.innerText = "Pick a Stop!";
fragment.append(defaultOption);
// loop over the keys and values of the object
for (const [key, value] of Object.entries(stops)) {
let option = document.createElement('option');
option.innerText = key;
option.value = value;
fragment.append(option);
}
document.getElementById("stop").replaceChildren(fragment);
}
document.addEventListener('change', function(event){
if (!event.target.matches('#line')){
return;
}
let stops = lines[event.target.value];
// @todo handle case where line value is not valid
reloadStopOptions(lines[event.target.value]);
});
document.addEventListener('change', function(event){
if (!event.target.matches('#stop')){
return;
}
console.log('Handle displaying the next arrival time');
});
// sets up the inital state
reloadStopOptions(lines[document.getElementById('line').value]);
<form>
<select id="line">
<option>red</option>
<option>blue</option>
<option>green</option>
</select>
<select id="stop">
</select>
</form>
The key thing here is to understand that you need to separate between data and the logic that operates on the data.
Upvotes: 4