I have the following Javascript code contained in my profile.js
file in Rails 7:
window.addEventListener('load', function () {
updateProfile();
})
window.onpageshow = function(event) {
if (event.persisted) {
window.location.reload();
}
};
window.addEventListener('popstate', function() {
updateProfile();
});
window.addEventListener( "pageshow", function ( event ) {
var historyTraversal = event.persisted ||
( typeof window.performance != "undefined" &&
window.performance.navigation.type === 2 );
if ( historyTraversal ) {
// Handle page restore.
window.location.reload();
}
});
function fileSelected() {
// Get the selected file
const file = document.querySelector('#fileInput').files[0];
if (file == null){
return
}
// Create a new FileReader object
const reader = new FileReader();
// Set the onl oad event handler for the FileReader object
reader.onload = function(event) {
// Update the src attribute of the profile image
document.querySelector('#profile-image').src = event.target.result;
};
// Read the selected file as a DataURL
reader.readAsDataURL(file);
}
function updateProfile(){
var toggle_switch = document.getElementById('toggle');
var save_button = document.getElementById('save-button');
let nameInput = document.getElementById('name');
nameInput.addEventListener('input', function(event) {
let error_element = document.getElementById('error-message-name');
let regex = /^.{3,}$/; // Regex that requires at least 3 characters
if (regex.test(value)) {
// Value is valid
error_element.classList.remove("visible-error")
error_element.classList.add("invisible-error")
} else {
// Value is invalid
error_element.classList.remove("invisible-error")
error_element.classList.add('visible-error');
}
});
toggle_switch.addEventListener('click', function() {
if (this.getAttribute('data-type') == "influencer"){
this.setAttribute('data-type', "vender");
document.getElementById('profile_type').value = "vender";
}else{
this.setAttribute('data-type', "influencer");
document.getElementById('profile_type').value = "influencer";
}
});
save_button.addEventListener('click', function() {
let new_name = document.getElementById('name').value;
let new_headline = document.getElementById('headline').value;
let new_country = document.getElementById('country').value;
let new_city = document.getElementById('city').value;
let new_about = document.getElementById("about").value;
let new_profile_type = document.getElementById('toggle').getAttribute('data-type');
let div = document.getElementById('user_id');
let user_id = div.getAttribute('data');
var fileInput = document.getElementById("fileInput");
var file = fileInput.files[0];
if (file == null) {
$.ajax({ //A new image was not uploaded for change
type: "PATCH",
url: encodeURI('/users/' user_id),
beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
data: { user: { profile_type: new_profile_type, name: new_name, headline: new_headline, country: new_country, city: new_city, about: new_about} },
success: function(response) {
console.log("Update success.")
}
});
} else {
const formData = new FormData();
formData.append("avatar", file);
$.ajax({
url: encodeURI('/users/' user_id),
type: "PUT",
beforeSend(xhr, options) {
xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
xhr.setRequestHeader('image-change', true);
options.data = formData;
},
success: function(response) {
console.log("Image Update success.")
},
error: () => {
alert("An issue occured. Please try again.");
}
});
}
});
}
This code should be run when a user navigates to my profile page with the following html.erb
:
<head>
<%= stylesheet_link_tag 'linked_card', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_link_tag 'profile_card', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_link_tag 'alert', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag "profile", "data": { "turbolinks-track": "reload" } %>
</head>
<header>
<%= render partial: "layouts/header" %>
</header>
<body class = "profile_main_body">
<%= render partial: "layouts/profile_card", locals: { user: @user } %>
<br>
<br>
<div >
<h3 >Link Accounts</h3>
<p >Click the button below to link an account to your profile. You will be redirected to the chosen service.</p>
<a href="<%= user_profile_path(@user.username)%>" ><i ></i> Link Account</a>
</div>
<br>
<br>
<div >
<div >
<%= render partial: "layouts/linked_card" %>
<%= render partial: "layouts/linked_card" %>
</div>
</div>
<br>
</body>
<footer>
<%= render partial: "layouts/footer" %>
</footer>
I added the following line:
<%= javascript_include_tag "profile", "data": { "turbolinks-track": "reload" } %>
Because I was thinking this is a turbolinks issue. Nothing has changed though with the line above. Is there anyway I can re-run the my javascript when the user clicks a turbo link? The javascript runs fine on a full page reload. Any help would be great thanks!
CodePudding user response:
Turbolinks is replaced by Turbo in Rails 7. However there is a lot of overlap in how you should think about JS.
Turbolinks/Turbo (or almost any SPA framework for that matter) creates a persistent browser session across pages. Thinking in terms of "when the page is loaded I want to attach an event handler to X that does Y" might have been OK in a basic JS tutorial ten years ago but is actually counter-productive:
- It doesn't work when elements are inserted dynamically into the DOM. Like when Turbo Drive or Turbolinks replaces the page contents. Or whenever people try to insert content loaded with AJAX for the first time.
- Adding event handlers directly to a bunch of elements adds a lot of overhead.
- If you hook into an event like
turbolinks:change
you might be adding the event handler multiple times to the same element. Just switching out the load/ready event for the Turbo/Turbolinks equivilent won't necissarily fix stinky code and may just introduce new issues. - It's a broken mental model as you build your JS to just "frobnob X on page Y" instead of thinking in terms of reusable UI components or augmenting the behavior of elements in a reusable way.
So what then?
Stop assigning IDs and event handlers directly to everything. You're just going to end up with duplicate IDs and garbage JS.
Instead use delegation to catch the event as it bubbles to the top of the DOM:
// Do something awesome when the user clicks buttons with
document.addEventHandler('click', function(event){
let el = event.target;
if (!el.matches('.foo')) return;
// ...
});
Use classes and attributes to target elements. Not ID's. Its not 2010 and querying the DOM is much faster.
Use DOM traversal and the form API to get elements relative to the element that was clicked/changed/etc. Remember that in JS functions can actually take arguments. Use data attributes if you need to pass additional information from the backend to your JS.
This is basically what Stimulus does:
<div data-controller="hello">
<input type="text">
<button>Greet</button>
</div>
// src/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
console.log("Hello, Stimulus!", this.element)
}
}