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?
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.