Help to implement a form with uploading photos in live view

I am trying to create a form that saves some data, but I intend to include the possibility of adding a photo media. I did some research on the subject, and saw that they are implementing this new feature in liveview. I was wondering if there is any other way that I can create this type of event for the socket to understand the file upload? :smile:

It isn’t really possible at the moment as far as I am aware.

There are some workarounds discussed here: https://github.com/phoenixframework/phoenix_live_view/issues/104

I have it working quite nicely so if you can’t figure out a way forward based on the information there, let me know.

2 Likes

Thanks for the information :smile: .Can you explain to me how you managed to implement this event?

So between my comments (mindok) and those from montebrown in that github issue there should be quite a few pointers to implementing, but here’s the (messy) code…

Image upload controller - handles incoming images

defmodule AppWeb.UploadController do
  use AppWeb, :controller
  alias AppWeb.Context.Media

  def create(conn, %{"file" => %Plug.Upload{}=upload, "id" => raw_thing_id}) do
    {thing_id, ""} = Integer.parse(raw_thing_id)

    # You need code to store the file from the Plug.Upload struct here
    case Media.create_upload_from_plug_upload(upload, thing_id) do

      {:ok, upload}->
        put_flash(conn, :info, "file uploaded correctly")
        conn |> put_status(:created) |> json(%{id: upload.id})

      {:error, reason}->
        put_flash(conn, :error, "error upload file: #{inspect(reason)}")
        render(conn, "new.html")
    end
  end
end

Router entries

    resources "/uploads", UploadController, only: [:create] 

leex template with script embedded (haven’t got around to extracting js as yet, plus you will need some css from dropzone.js). And just to make it super nasty I inject an id into the script. In other places where I do file uploads I attach the JS to an element using liveview hooks and look up the id from a data attribute.

You may have to fight with CSRF tokens a bit too - they have changed in recent liveview versions

<div phx-update="ignore" class="dropzone dz-clickable" id="divtarget">
  <div class="dz-small dz-message d-flex flex-column">
    <span class="icon"><i class="fas fa-cloud-upload-alt fa-3x"></i></span>
    Drag &amp; Drop here or click
  </div>
</div>


<script src="/js/dropzone.js"></script>
<script src="/js/canvasToBlob.js"></script>
<script src="/js/cropper.js"></script>
<script>
Dropzone.options.divtarget = {
  paramName: "file", // The name that will be used to transfer the file
  url: '/uploads',
  maxFilesize: 10, // MB
  headers: {
    'x-csrf-token': '<%= @token %>'
  },
  init: function () {
    this.on("sending", function (fil, xhr, formData) {
      formData.append("id", '<%= @thing.id %>') 
    });
    this.on("complete", function (file) {
      this.removeFile(file);
    });
  },
  acceptedFiles: "image/jpeg, image/jpg, image/png, image/gif",
  transformFile: function (file, done) {
    var myDropZone = this;

    // Create the image editor overlay
    var editor = document.createElement('div');
    editor.style.position = 'fixed';
    editor.style.left = 0;
    editor.style.right = 0;
    editor.style.top = 0;
    editor.style.bottom = 0;
    editor.style.zIndex = 9999;
    editor.style.backgroundColor = '#000';

    // Create the confirm button
    var confirm = document.createElement('button');
    confirm.style.position = 'absolute';
    confirm.style.left = '10px';
    confirm.style.top = '10px';
    confirm.style.zIndex = 9999;
    confirm.textContent = 'Confirm';
    confirm.addEventListener('click', function () {

      // Get the canvas with image data from Cropper.js
      var canvas = cropper.getCroppedCanvas({});

      // Turn the canvas into a Blob (file object without a name)
      canvas.toBlob(function (blob) {

        // Update the image thumbnail with the new image data
        myDropZone.createThumbnail(
          blob,
          myDropZone.options.thumbnailWidth,
          myDropZone.options.thumbnailHeight,
          myDropZone.options.thumbnailMethod,
          false,
          function (dataURL) {

            // Update the Dropzone file thumbnail
            myDropZone.emit('thumbnail', file, dataURL);

            // Return modified file to dropzone
            done(blob);
          }
        );

      }, 'image/jpeg', 0.9);

      // Remove the editor from view
      editor.parentNode.removeChild(editor);

    });
    editor.appendChild(confirm);

    // Load the image
    var image = new Image();
    image.src = URL.createObjectURL(file);
    editor.appendChild(image);

    // Append the editor to the page
    document.body.appendChild(editor);

    // Create Cropper.js and pass image
    var cropper = new Cropper(image, {});
  }
};
</script>

2 Likes