0

I'm working on an avatar uploader for my project. Everything has been so far so good, and this morning I had no issues. A little while later, BOOM. Death and destruction. Very sadness.

When I first choose a file, it pops up the crop tool immediately, and works fine. If I attempt to upload a different file, the crop tool disappears and not even a preview of the image is presented.

How does my system work? I'm using the Laravel framework as my backend, and the Laravel Livewire package for front-end functionality. Livewire allows me to write Vue-like components in PHP.

Here's the form I'm working on. It's also the component that's refreshed every time that Livewire sees a new file in the input.

<label for="image-upload">
    <div class="w-full mt-4 button"><i class="far fa-fw fa-upload"></i> Drag and drop, or <b>browse</b></div>
</label>
<input id="image-upload" type="file" wire:model="image" class="hidden" accept="image/png, image/gif, image/jpeg">

@if($image)
    <div id="avatar-preview" class="w-full" style="height: 300px;"></div>

    <script>
        new Croppie(document.querySelector('#avatar-preview'), {
            viewport: { width: 200, height: 200, type: 'circle' },
            enforceBoundary: true,
        }).bind('{!! $image->temporaryUrl() !!}');
    </script>

    <button type="submit" class="w-full mt-16 button is-green">Submit new avatar</button>
@endif

I'm using the Croppie JS package for the crop tool. It requires that I either pass it an img element it'll attach to, or a container to fit into. I picked a container so I could control the size of the crop tool. When I upload an image to the input, Livewire will take the image, validate it's an image and doesn't pass a certain size, and uploads the image to a temporary directory. The $image->temporaryUrl() echoes the path to that temporary file. When the file upload is done and the temporary image is ready, Livewire refreshes just the component via AJAX. At this point, I attempt to create a new Croppie instance, attach it to my preview container, and bind the temporary image to it.

This works on the first file upload! Then, when I attempt to change the file (as a user might) the entire instance disappears. I attempted to check and see if it was an issue instantiating Croppie again, since in my mind if the component is refreshed, creating a new instance of the tool shouldn't be an issue.

<script>
    if (typeof crop === 'undefined') {
        console.log('crop undefined');
        let crop = new Croppie(document.querySelector('#avatar-preview'), {
            viewport: { width: 200, height: 200, type: 'circle' },
            enforceBoundary: true,
        }).bind('{!! $image->temporaryUrl() !!}');
    } else {
        console.log('crop defined');
        crop.bind('{!! $image->temporaryUrl() !!}');
    }

    console.log('crop finished');
</script>

And the 'undefined' and 'finished' logs come in on the first upload, but nothing happens on the refresh.

So I also tested just doing this...

@if($image)
    <img src="{{ $image->temporaryUrl() }}">
@endif

To ensure at the very least that the preview was working correctly, and lo and behold it does!

The big problem is that even when the Croppie doesn't refresh, no errors or warnings occur. It just doesn't seem to execute what's in the <script> tag at all.

Any tips or advice?

Splashsky
  • 61
  • 8
  • 1
    _“It just doesn't seem to execute what's in the – CBroe Aug 19 '20 at 08:09

1 Answers1

0

After further reading, it appears Livewire can, from the component class, emit "events" that can be picked up by JavaScript. My issue was caused by the natural behavior of changing DOM content by .innerHtml.

In my component class, in the updated() function that handles what to do when the input is updated, I added

$this->emit('avatar_preview_updated', $this->image->temporaryUrl());

I then added this listener to my page's global JS:

livewire.on('avatar_preview_updated', image => {
    new Croppie(document.querySelector('#avatar-preview'), {
        url: image,
        viewport: { width: 200, height: 200, type: 'circle' },
        boundary: { height: 300 },
        enforceBoundary: true,
    });
});

Which produces the desired result.

Splashsky
  • 61
  • 8