Article image

UPLOAD IMAGES IN LARAVEL AND VUE FROM QUILL RICH TEXT EDITOR

Laravel -

Sep 21 2019

Dino Numic

Very often you will find yourself in a need of a good rich text editor that supports everything out of the box. However, usually, this is not the case, and you either find a new editor to try out or implement the missing features yourself.

Image upload in rich text editors is not always behaving as you want it. Sometimes it stores images in base64 which substantially increases image size and can hinder performance, sometimes it requires additional paid plugin by the creator, and sometimes it is completely left out.

In this article we will look into how we can setup some easy image upload to our server from rich text editor. 

We will utilize a couple of packages.

1.      Spatie Laravel-medialibrary - https://github.com/spatie/laravel-medialibrary

2.      Surmon China Vue Quill Editor - https://github.com/surmon-china/vue-quill-editor

3.      Quill Resize Module - https://github.com/kensnyder/quill-image-resize-module

At this point it would be a good time to install Laravel and Vue dependencies if you haven’t already.

For our example, we will have an article model with a single database field called body. We will create our migration, routes, controller, and views.

First we create a new migration – php artisan make:migration create_articles_table

public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->text(‘body’);
        $table->timestamps();
    });
}

In order to create the table we need to migrate this class. php artisan migrate. We also need an Article model.

class Article extends Model
{
    protected $fillable = [
        'body',
    ];
}

Let’s create ArticleController swiftly with artisan command – php artisan make:controller ArticleController.

Now we will create three methods and three routes that can be called on the controller.

class ArticleController extends Controller

{
    public function create()
    {
    }

    public function store(Request $request)
    {
    }

    public function show(Request $request)
    {
    }
}

And our routes.

Route::get('/article', 'ArticleController@create')->name('article.create');

Route::post('/article/create', 'ArticleController@store')->name('user.store');

Route::get('/article/{article}', 'ArticleController@show')->name('article.show');


Lets quickly create our view files. We will create a pages folder inside our views with an Article folder and two blade files: create and show. In create we will place our rich text editor and in show we will display saved article from the database.

Lets update our create() method to return proper view.

public function create()
{
    return view('pages.article.create');
}

At this point we will install vue-quill-editor and image resize module.

npm install vue-quill-editor –save

npm install quill-image-resize

Now, let’s create a new Vue component called ArticleCreate. Here, we will import quill editor.

In the script part of the component lets import and configure quill editor.

<script>
    // Import Quill required dependencies
    import 'quill/dist/quill.core.css'
    import 'quill/dist/quill.snow.css'
    import 'quill/dist/quill.bubble.css'
    import { quillEditor, Quill } from 'vue-quill-editor'
    import ImageResize from 'quill-image-resize';

    // Register ImageResize module
    Quill.register('modules/imageResize', ImageResize);

    // Set toolbar options
    const toolbarOptions = [
        ['bold', 'italic', 'underline', 'strike'],
        ['blockquote', 'code-block'],

        [{'header': 1}, {'header': 2}],
        [{'list': 'ordered'}, {'list': 'bullet'}],
        [{'script': 'sub'}, {'script': 'super'}],
        [{'indent': '-1'}, {'indent': '+1'}],
        [{'direction': 'rtl'}],

        [{'size': ['small', false, 'large', 'huge']}],
        [{'header': [1, 2, 3, 4, 5, 6, false]}],

        [{'color': []}, {'background': []}],
        [{'font': []}],
        [{'align': []}],
        ['link', 'image', 'video'],
        ['clean'],
    ];

    export default {
        components: {
            quillEditor
        },
        data() {
            return {
                form: {
                    body: ''
                },
                editorOption: {
                    modules: {
                        toolbar: {
                            container: toolbarOptions,
                        },
                        history: {
                            delay: 1000,
                            maxStack: 50,
                            userOnly: false
                        },
                        imageResize: {
                            displayStyles: {
                                backgroundColor: 'black',
                                border: 'none',
                                color: 'white'
                            },
                            modules: [ 'Resize', 'DisplaySize', 'Toolbar' ]
                        }
                    }
                },
            }
        },
        methods: {
            onEditorBlur(editor) {
                // console.log('editor blur!', editor)
            },
            onEditorFocus(editor) {
                // console.log('editor focus!', editor)
            },
            onEditorReady(editor) {
                // console.log('editor ready!', editor)
            },
            submitForm(){
               
            }
        },
        computed: {
            editor(){
                return this.$refs.myQuillEditor.quill
            }
        },
    }
</script>

And let’s add html code to our template.

<template>
    <div>
        <label for="rich-text" class="form-label">Content</label>
        <quill-editor v-model="form.body"
                      class="mb-3"
                      id="rich-text"
                      rows="20"
                      :options="editorOption"
                      ref="myQuillEditor"
                      @blur="onEditorBlur($event)"
                      @focus="onEditorFocus($event)"
                      @ready="onEditorReady($event)">
        </quill-editor>
    </div>
</template>

In this large chunk of code we have done the following. We have imported Vue Quill and Image Resize module that we installed. We set the toolbar options for Quill, registered Image Resize module, and told Quill to use the configurations we just created. So now save the code and visit http://127.0.0.1:8000/article

So far it looks good. Seems like our code is working just fine.

Now let’s make a pause in writing code to think about how we are going to make this image upload work. Here is the idea.

We create a custom image handler for the image upload menu in Quill Toolbar. We also create a hidden file type input somewhere in our template which will not be visible to the user.

Then, whenever user clicks on the upload image button, we prevent the default behavior and instead activate our own file input and let the user select an image.

After the user selects an image, we take the image, convert it to base64, we push the base64 string to images array in our data(), we take the current cursor position and insert the image tag with base64 string as the <img src=”base64” /> to quill editor.

After the user saves the article, in the backend we will go through base64 strings in the images array. For every string in the array, with the help of Spatie Media Library, we will create an image file and then replace in the article content the <img src=”base64” /> with the full url of the newly created image <img src=”url” />.

After we laid out the concept let’s jump to implementing it.

First, let’s add a handler to Quill toolbar configuration


toolbar: {
    container: toolbarOptions,
    handlers: {
        'image': function (value) {
            if (value) {
                document.querySelector('#imageUpload').click();
            } else {
                this.quill.format('image', false);
            }
        }
    },
},

and let’s create an input with file type and place it beneath quill editor.

<div class="custom-file d-none">
    <input ref="image" @change="imageUpload($event)" type="file" class="custom-file-input" id="imageUpload" aria-describedby="imageUploadAddon">
    <label class="custom-file-label" for="imageUpload">Choose file</label>
</div>

We have to add images array to form in data()

images: [],

Add imageUpload method

imageUpload(e) {
    if (e.target.files.length !== 0) {

        let quill = this.editor;
        let reader = new FileReader();
        reader.readAsDataURL(e.target.files[0]);
        let self = this;
        reader.onloadend = function() {
            let base64data = reader.result;
            self.form.images.push(base64data);

            // Get cursor location
            let length = quill.getSelection().index;

            // Insert image at cursor location
            quill.insertEmbed(length, 'image', base64data);

            // Set cursor to the end
            quill.setSelection(length + 1);
        }
    }
},

From the frontend side, the only thing left to do is to create a save button and to make an ajax request to our backend.

Our button

<button @click="submitForm" class="btn btn-sm btn-success">Save</button>

And our method

submitForm(){
    axios.post('/article/create',
        { body: this.form.body, images: this.form.images })
        .then(response => {
            console.log("Response", response)
        })
        .catch(error => {
            console.log("Error", error)
        })
}
}

For our backend, we first need to install Spatie Media Library and associate it with Article model.

composer require "spatie/laravel-medialibrary:^7.0.0"

We will publish the migration files

php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="migrations"

and run the migrations with

php artisan migrate

If you run into issues while migrating change the type of following json fields to text.

$table->text('manipulations');
$table->text('custom_properties');
$table->text('responsive_images');

Now let’s configure our Article model.

use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia\HasMedia;
use Spatie\MediaLibrary\HasMedia\HasMediaTrait;

class Article extends Model implements HasMedia
{
    use HasMediaTrait;
   
    protected $fillable = [
        'body',
    ];
}

Finally, everything is ready to implement the final code to create and replace images.

public function store(Request $request)
{
    $body = $request->body;
    $images = $request->images;

    // Create a new article
    $article = Article::create([
        'body' => $body
    ]);

    // If images not empty
    if ($images) {
        foreach ($images as $image)
        {
            // Create a new image from base64 string and attach it to article in article-images collection
            $article->addMediaFromBase64($image)->toMediaCollection('article-images');

            // Get all images as we will need the last one uploaded
            $mediaItems = $article->load('media')->getMedia('article-images');

            // Replace the base64 string in article body with the url of the last uploaded image
            $article->body = str_replace($image, $mediaItems[count($mediaItems) - 1]->getFullUrl(), $article->body);
        }
    }

    $article->save();

    return response()->json('Success');
}

Let’s add some text and an image in our text editor.

We will now finish our method for showing a single article. 

public function show(Request $request, Article $article)
{
    return view('pages.article.show', compact('article'));
}

Our show.blade.php file.

<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-12">
            <div class="card">
                <div class="card-header">Article</div>
                <div class="card-body">
                    {!! $article->body !!}
                </div>
            </div>
        </div>
    </div>
</div>

Once we click on the save button let’s visit our show route http://127.0.0.1:8000/article/1

Sure enough, we have our text and the image we uploaded. Awesome.

Hope you liked this simple image upload. Subscribe to the newsletter if you would like to get a notification when we publish similar articles. Stay awesome and happy coding.