javascriptruby-on-railsturbo

Rails 7 Javascript not working on first page load


I have a Rails 7 app, where users can upload photos to a post. I've used Javascript (client side) to create the drag and drop upload file upload field, but it doesn't work when the page is loaded the first time. If you refresh the page, it works as expected.

I think this is something to do with Turbo, but I don't know how to fix it. I have tried adding <meta name="turbo-visit-control" content="reload"> but with no success.

Any help would be appreciated.

my file upload form and javascript:

<p class="text-2xl font-bold mb-4 text-blue-600 xl:pl-9">Add Some Photos</p>
            <p class="text-l font-semibold mb-7 xl:pl-9">You can upload up to ten photos of your item.</p>
            <article aria-label="File Upload Modal" class="xl:h-2/5 relative flex flex-col bg-white shadow-xl rounded-md xl:m-7" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);" ondragleave="dragLeaveHandler(event);" ondragenter="dragEnterHandler(event);">
              <div id="overlay" class="w-full h-full absolute top-0 left-0 pointer-events-none z-50 flex flex-col items-center justify-center rounded-md">
                <i>
                  <svg class="fill-current w-12 h-12 mb-3 text-blue-700" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
                    <path d="M19.479 10.092c-.212-3.951-3.473-7.092-7.479-7.092-4.005 0-7.267 3.141-7.479 7.092-2.57.463-4.521 2.706-4.521 5.408 0 3.037 2.463 5.5 5.5 5.5h13c3.037 0 5.5-2.463 5.5-5.5 0-2.702-1.951-4.945-4.521-5.408zm-7.479-1.092l4 4h-3v4h-2v-4h-3l4-4z" />
                  </svg>
                </i>
                <p class="text-lg text-blue-700">Drop files to upload</p>
              </div>

              <section class="h-full overflow-auto p-8 w-full h-full flex flex-col">
                <header class="border-dashed border-2 border-gray-400 py-12 flex flex-col justify-center items-center">
                  <p class="mb-3 font-semibold flex flex-wrap justify-center">
                    <span>Drag and drop your</span>&nbsp;<span>files anywhere or</span>
                  </p>
                  <%= f.file_field :photos, multiple: true, class: "hidden", id: "hidden-input", name: "part[photos][]" %>                         
                  <button id="button" type="button" class="mt-2 rounded-lg px-3 py-1 bg-blue-600 hover:bg-blue-700 font-bold text-white focus:shadow-outline focus:outline-none">
                    Upload a file
                  </button>                    
                </header>

                <h1 class="pt-8 pb-3 font-semibold sm:text-lg text-blue-600">
                  To Upload
                </h1>

                <ul id="gallery" class="flex flex-1 flex-wrap -m-1">
                  <li id="empty" class="h-full w-full text-center flex flex-col items-center justify-center items-center">
                    <img class="mx-auto w-32" src="https://user-images.githubusercontent.com/507615/54591670-ac0a0180-4a65-11e9-846c-e55ffce0fe7b.png" alt="no data" />
                    <span class="text-small">No files selected.</span>
                  </li>
                </ul>
              </section>
            </article>

            <template id="file-template">
              <li class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-24">
                <article tabindex="0" class="group w-full h-full rounded-md focus:outline-none focus:shadow-outline elative bg-gray-100 cursor-pointer relative shadow-sm">
                  <img alt="upload preview" class="img-preview hidden w-full h-full sticky object-cover rounded-md bg-fixed" />

                  <section class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
                    <h1 class="flex-1 group-hover:text-blue-800"></h1>
                    <div class="flex">
                      <span class="p-1 text-blue-800">
                        <i>
                          <svg class="fill-current w-4 h-4 ml-auto pt-1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
                            <path d="M15 2v5h5v15h-16v-20h11zm1-2h-14v24h20v-18l-6-6z" />
                          </svg>
                        </i>
                      </span>
                      <p class="p-1 size text-xs text-gray-700"></p>
                      <button class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800">
                        <svg class="pointer-events-none fill-current w-4 h-4 ml-auto" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
                          <path class="pointer-events-none" d="M3 6l3 18h12l3-18h-18zm19-4v2h-20v-2h5.711c.9 0 1.631-1.099 1.631-2h5.316c0 .901.73 2 1.631 2h5.711z" />
                        </svg>
                      </button>
                    </div>
                  </section>
                </article>
              </li>
            </template>

            <template id="image-template">
              <li class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-24">
                <article tabindex="0" class="group hasImage w-full h-full rounded-md focus:outline-none focus:shadow-outline bg-gray-100 cursor-pointer relative text-transparent hover:text-white shadow-sm">
                  <img alt="upload preview" class="img-preview w-full h-full sticky object-cover rounded-md bg-fixed" />

                  <section class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
                    <h1 class="flex-1"></h1>
                    <div class="flex">
                      <span class="p-1">
                        <i>
                          <svg class="fill-current w-4 h-4 ml-auto pt-" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
                            <path d="M5 8.5c0-.828.672-1.5 1.5-1.5s1.5.672 1.5 1.5c0 .829-.672 1.5-1.5 1.5s-1.5-.671-1.5-1.5zm9 .5l-2.519 4-2.481-1.96-4 5.96h14l-5-8zm8-4v14h-20v-14h20zm2-2h-24v18h24v-18z" />
                          </svg>
                        </i>
                      </span>

                      <p class="p-1 size text-xs"></p>
                      <button class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md">
                        <svg class="pointer-events-none fill-current w-4 h-4 ml-auto" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
                          <path class="pointer-events-none" d="M3 6l3 18h12l3-18h-18zm19-4v2h-20v-2h5.711c.9 0 1.631-1.099 1.631-2h5.316c0 .901.73 2 1.631 2h5.711z" />
                        </svg>
                      </button>
                    </div>
                  </section>
                </article>
              </li>
            </template>

            <script>
              const photos = [];

              // Get a reference to the file input element
              const fileInput = document.getElementById('hidden-input');

              // Listen for changes to the file input element
              fileInput.addEventListener('change', function() {
                // Get the list of selected files
                const files = fileInput.files;

                // Loop through the selected files
                for (let i = 0; i < files.length; i++) {
                  // Add the file to the photos array
                  photos.push(files[i]);
                }
              });

              const fileTempl = document.getElementById("file-template"),
                imageTempl = document.getElementById("image-template"),
                empty = document.getElementById("empty");

              // use to store pre selected files
              let FILES = {};

              // check if file is of type image and prepend the initialied
              // template to the target element
              function addFile(target, file) {
                const isImage = file.type.match("image.*"),
                  objectURL = URL.createObjectURL(file);

                const clone = isImage
                  ? imageTempl.content.cloneNode(true)
                  : fileTempl.content.cloneNode(true);

                clone.querySelector("h1").textContent = file.name;
                clone.querySelector("li").id = objectURL;
                clone.querySelector(".delete").dataset.target = objectURL;
                clone.querySelector(".size").textContent =
                  file.size > 1024
                    ? file.size > 1048576
                      ? Math.round(file.size / 1048576) + "mb"
                      : Math.round(file.size / 1024) + "kb"
                    : file.size + "b";

                isImage &&
                  Object.assign(clone.querySelector("img"), {
                    src: objectURL,
                    alt: file.name
                  });

                empty.classList.add("hidden");
                target.prepend(clone);

                FILES[objectURL] = file;
              }

              const gallery = document.getElementById("gallery"),
                overlay = document.getElementById("overlay");

              // click the hidden input of type file if the visible button is clicked
              // and capture the selected files
              const hidden = document.getElementById("hidden-input");
              document.getElementById("button").onclick = () => hidden.click();
              hidden.onchange = (e) => {
                for (const file of e.target.files) {
                  addFile(gallery, file);
                }
              };

              // use to check if a file is being dragged
              const hasFiles = ({ dataTransfer: { types = [] } }) =>
                types.indexOf("Files") > -1;

              // use to drag dragenter and dragleave events.
              // this is to know if the outermost parent is dragged over
              // without issues due to drag events on its children
              let counter = 0;

              // reset counter and append file to gallery when file is dropped
              function dropHandler(ev) {
                ev.preventDefault();
                for (const file of ev.dataTransfer.files) {
                  addFile(gallery, file);
                  overlay.classList.remove("draggedover");
                  counter = 0;
                }
              }

              // only react to actual files being dragged
              function dragEnterHandler(e) {
                e.preventDefault();
                if (!hasFiles(e)) {
                  return;
                }
                ++counter && overlay.classList.add("draggedover");
              }

              function dragLeaveHandler(e) {
                1 > --counter && overlay.classList.remove("draggedover");
              }

              function dragOverHandler(e) {
                if (hasFiles(e)) {
                  e.preventDefault();
                }
              }

              // event delegation to caputre delete events
              // fron the waste buckets in the file preview cards
              gallery.onclick = ({ target }) => {
                if (target.classList.contains("delete")) {
                  const ou = target.dataset.target;
                  document.getElementById(ou).remove(ou);
                  gallery.children.length === 1 && empty.classList.remove("hidden");
                  delete FILES[ou];
                }
              };

              function addFilesToPhotosArray() {
                // Get the gallery element
                const gallery = document.getElementById("gallery");

                // Get the selected files from the gallery
                const selectedFiles = gallery.querySelectorAll(".selected");

                // Get the photos array from the input field
                const photosArray = document.getElementById("hidden-input").value;

                // Add the selected files to the photos array
                for (const selectedFile of selectedFiles) {
                  photosArray.push(selectedFile);
                }

                // Update the input field with the new photos array
                document.getElementById("hidden-input").value = photosArray;
              }

              // clear entire selection
              document.getElementById("cancel").onclick = () => {
                while (gallery.children.length > 0) {
                  gallery.lastChild.remove();
                }
                FILES = {};
                empty.classList.remove("hidden");
                gallery.append(empty);
              };
            </script>
          </div>

Solution

  • For me the suggestion of Rockwell Rice worked. Adding data: { turbo: false } disables Turbo Drive and causes the page to fully reload.

    So if you have a link to the page where the photos can be added, e.g.

    <%= link_to 'Go to Add Some Photos', new_photo_path %>
    

    you can change it to:

    <%= link_to 'Go to Add Some Photos', new_photo_path, data: { turbo: false }  %>
    

    Reference: https://www.hotrails.dev/turbo-rails/turbo-drive