What we're going to do
What we're going to do, in terms of how the user of the application sees it, is the following.
The user has a default profile picture.
The user clicks on their profile picture, and suddenly they can select a new picture (although they can also press cancel and nothing happens).
If the user selects an image and accepts, then their profile picture is updated immediately (no need to reload the page).).
How we're going to do it
We have many ways to implement this feature.
In this case we will do it as follows:
- We're going to add a form (but with styling we'll make it stay hidden). This form will contain a field of type
file. - We're going to use Javascript to associate a
clickabout the profile picture. When this event is detected, we will cause a click on the input of typefile. - Let's listen to the event
changeof the input, so that when a file has been chosen, we make an AJAX request to modify the profile picture. - The AJAX request will return a success or failure message. If the answer was successful, then we will update the profile picture via Javascript (so as not to refresh the page).
Let's do it
The Hidden Form will be hidden thanks to a CSS property called display (with the courage none).
In this case, I'm placing it just before the image.
<form action="{{ url('perfil/foto') }}" method="post" style="display: none" id="avatarForm">
{{ csrf_field() }}
<input type="file" id="avatarInput" name="photo">
</form>
<img src="{{ auth()->user()->getAvatarUrl() }}" id="avatarImage">
Note that:
- The form, as well as the image, and the input, have an id assigned to them (this is important to access these elements via Javascript).
- The input type
filehas a name attribute with the valuephoto(This value must match what we have in our driver, to recover the file uploaded correctly from the backend). - I've simplified the code by removing the
class,altytitlein my image (you can use these attributes as it suits you in your projects).
¿What we do with Javascript?
As I mentioned before, we need to log 2 events. And also get a reference of the elements.
$(function () {
var $avatarImage, $avatarInput, $avatarForm;
$avatarImage = $('#avatarImage');
$avatarInput = $('#avatarInput');
$avatarForm = $('#avatarForm');
$avatarImage.on('click', function () {
$avatarInput.click();
});
$avatarInput.on('change', function () {
alert('change');
});
});
As you may have noticed, at the change of the input I have only put an alert.
Up to this point you should make sure you get that alert, then click on the image and select a file.
If everything is in place, then you can replace the alert to make the Ajax request.
So we would have the following:
$avatarInput.on('change', function () {
var formData = new FormData();
formData.append('photo', $avatarInput[0].files[0]);
$.ajax({
url: $avatarForm.attr('action') + '?' + $avatarForm.serialize(),
method: $avatarForm.attr('method'),
data: formData,
processData: false,
contentType: false
}).done(function (data) {
if (data.success)
$avatarImage.attr('src', data.path);
}).fail(function () {
alert('La imagen subida no tiene un formato correcto');
});
});
Here we must take into account that:
- We are getting the url to which the request will be made, and the
methodfrom the values defined in the form. - We use an object
FormDatato upload the image via Ajax (since theserializein this case only thecsrf token). - The response we get from the Ajax request is made up of an object that we call
dataand that has 2 attributes (successto indicate whether the operation was successful, andpathwith the path to the profile picture).
Finally, we only need to have the route registered perfil/foto (which was the one we used in the action of the form). If you prefer, you can use a different route.
This path must be declared in the Laravel route file.
Route::post('/perfil/foto', 'ProfileController@updatePhoto');
In this case, the route is resolved through a controller called ProfileController. Specifically through its updatePhoto.
So we would have the following in said controller:
public function updatePhoto(Request $request)
{
$this->validate($request, [
'photo' => 'required|image'
]);
$file = $request->file('photo');
$extension = $file->getClientOriginalExtension();
$fileName = auth()->id() . '.' . $extension;
$path = public_path('images/users/'.$fileName);
Image::make($file)->fit(144, 144)->save($path);
$user = auth()->user();
$user->photo_extension = $extension;
$saved = $user->save();
$data['success'] = $saved;
$data['path'] = $user->getAvatarUrl() . '?' . uniqid();
return $data;
}
This method:
- Validates that the
photosent in the request is an image (and is also a required field). - It obtains the extension of the image, so that the user's id, followed by the extension, is the name of the file to be saved.
- Makes use of the façade
Imageso that if the image is larger than 144x144 pixels, then its size is adjusted according to this limit. - Save to the users table, in the
photo_extension, the extension of the uploaded image. - Finally, it returns a JSON response indicating whether the operation was successful, and also sends back the URL of the image.
For the above code to work correctly, you need to have a column in the user table photo_extension.
$table->string('photo_extension')->nullable();
You also need to define the method getAvatarUrl in the model User.
public function getAvatarUrl()
{
if ($this->photo_extension)
return asset('images/users/'.$this->id.'.'.$this->photo_extension);
return asset('images/users/default.jpg');
}
This method returns an absolute URL to the user's profile picture. And if it does not exist, the absolute URL of an image by default.
If you haven't already installed the Intervention/Image package (which is required for image resizing to work), you can install it by simply running:
composer require intervention/image
By the way. If you are observant you will have noticed the use of uniqid() in the updatePhoto. This method is used to add a unique number to the end of the file name, and thus ensure that the user always sees their new profile picture (as they may have previously uploaded an image with the same length, and this image may have been saved in the browser's cache).