Home > Software design >  How to show percent done of a file upload with turbo_stream and no custom js?
How to show percent done of a file upload with turbo_stream and no custom js?

Time:12-06

I have a form with:

<%= form_with(model: doc, url: my_docs_path(@application, doc), id: "#{dom_id(doc)}_form", html: {multipart: true}) do |form| %>
<%= form.file_field :doc %>
<%= form.submit 'Upload Doc' %>
<% end %>

which hits my DocsController#create just fine and inside the respond_to I have a format.turbo_stream that is making create.turbo_stream.erb work great. A new document records appears on the screen after upload without any custom javascript and without a full page refresh.

But for a large file upload the user gets the impression the page is just "stuck" during upload. It's missing logic like this:

<form autocomplete="off" enctype="multipart/form-data">
<br/>
<input type="file" name="file" id="f1" accept="video/*" onchange="uploadFile('/file/{{.name}}')"/>
<span id="p">---</span>
<br/>
</form>

<script type="text/javascript">
window.uploadFile = function(url){
  var formData = new FormData();

  var fileInputElement = document.getElementById("f1");
  formData.append("file", fileInputElement.files[0]);

  var xhr = new XMLHttpRequest();
  xhr.open("POST", url);
  xhr.upload.onprogress = function (e) {
    if (e.lengthComputable) {
      document.getElementById("p").innerHTML = "" ((e.loaded/e.total)*100);
    }
  }
  xhr.upload.onloadstart = function (e) {
    document.getElementById("p").innerHTML = "0";
  }
  xhr.upload.onloadend = function (e) {
    document.getElementById("p").innerHTML = "" e.loaded;
    document.location.href = '/';
  }
  xhr.send(formData);
}
</script>

Without using this javascript, is there a way in rails7 to make this xhr.upload.onprogress event set a percent done to the user?

CodePudding user response:

Honestly, I didn't expect this to work. The idea is to use Turbo::Broadcastable and broadcast_update_to method:

def broadcast_update_to(*streamables, **rendering)
  Turbo::StreamsChannel.broadcast_update_to(*streamables, target: self, **broadcast_rendering_with_defaults(rendering))
end

https://github.com/hotwired/turbo-rails/blob/v1.3.2/app/models/concerns/turbo/broadcastable.rb#L137

Except that we'll use Turbo::StreamsChannel class directly.

Just one caveat, I don't know a good way or place to hook it all up. This was me just going up the rails stack until it looked like the place, which is Rack::Multipart::Parser::BoundedIO. There is a read method that receives the incoming bytes:

# $(bundle show rack)/lib/rack/multipart/parser.rb

def read(size, outbuf = nil)
  return if @cursor >= @content_length

  left = @content_length - @cursor

  str = if left < size
          @io.read left, outbuf
        else
          @io.read size, outbuf
        end

  if str
    @cursor  = str.bytesize

    #
    # NOTE: looks like @cursor tracks the total bytes received, so if we
    #       send it back in a turbo_stream as the file is uploading it
    #       should update on the page.
    Turbo::StreamsChannel.broadcast_update_to(
      "upload_channel", target: "progress", html: @cursor
    )
    # it uploads crazy fast locally so it fly by. i had sleep(0.5) here.
    # and you have to restart the server when you make changes here.
    #

  else
    # Raise an error for mismatching content-length and actual contents
    raise EOFError, "bad content body"
  end

  str
end

https://github.com/rack/rack/blob/v3.0.2/lib/rack/multipart/parser.rb#L58

On the form page we need to subscribe to upload_channel and have a #progress target for turbo stream to update:

# _form.html.erb

<%= turbo_stream_from "upload_channel" %>
# NOTE: you should see in the logs:
#       Turbo::StreamsChannel is transmitting the subscription confirmation
#       Turbo::StreamsChannel is streaming from upload_channel

<%= tag.div id: "progress" %>

# TODO: make a form here, hit upload, see updates inside the #progress.
  • Related