javascriptruby-on-railsturbolinks

Javascript doesn't run on back button or menu item click in my rails application


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 onload 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 class="col-md-6 mx-auto text-center">
      <h3 class="heading-black">Link Accounts</h3>
      <p class="text-muted lead">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)%>" class="border px-3 p-1 add-experience"><i class="fa fa-link"></i>&nbsp;Link Account</a>
    </div>
    <br>
    <br>
    <div class="container">
        <div class="row">
            <%= 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!


Solution

  • 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:

    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 class="foo"
    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)
      }
    }