on Tutorials

Laravel WYSIWYG editor

5 comments
Getting started with Laravel
Tweet about this on TwitterShare on FacebookShare on Google+Share on LinkedInShare on RedditShare on StumbleUpon

I really like how Medium editor works, it brings a new way of adding and editing content and it really looks fresh, fast and intuitive.

Luckily, some great people wrote excellent jQuery plugins which allow us to integrate Medium style WYSIWYG editor to Laravel and that is the subject of today’s tutorial.

DemoSource files

I built a simple Laravel application which only have one entity for posts, just to demonstrate this integration. So, to get started, install new Laravel project to a folder of your choice:

composer create-project laravel/laravel laravel-medium --prefer-dist

If you don’t know how to do this, please read my Getting started with Laravel tutorial.

You also need to alter the database credentials in /app/config/database.php file.

Ok, now we have a fresh new Laravel application, let’s start by creating a table for our Posts. I will do this manually. First, create a new migration:

php artisan migrate:make create_posts_table

Now, open that file and paste this in:
/app/database/migrations/DATE_create_posts_table.php

         /**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
        Schema::create('posts', function(Blueprint $table) {
            $table->increments('id');
			$table->string('title');
			$table->text('body');
            $table->timestamps();
        });
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
	    Schema::drop('posts');
	}

And migrate the database:

php artisan migrate

Ok, now we have a database table for our posts, let’s create a Post model:
/app/models/Post.php

class Post extends \Eloquent {
    protected $guarded = array();

    public static $rules = array(
        'title' => 'required',
        'body' => 'required'
    );
}

Simple model with some simple validation rules in it.

Now, we will create a Posts controller, I will show the whole code at once and explain below:
/app/controllers/PostsController.php

class PostsController extends BaseController {

    /**
     * Post Repository
     *
     * @var Post
     */
    protected $post;

    public function __construct(Post $post)
    {
        $this->post = $post;
    }

    /**
     * Display a listing of the resource.
     *
     * @return Response
     */
    public function index()
    {
        $posts = $this->post->all();

        return View::make('posts.index', compact('posts'));
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return Response
     */
    public function create()
    {
        return View::make('posts.create');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @return Response
     */
    public function store()
    {
        $input = Input::all();
        $validation = Validator::make($input, Post::$rules);

        if ($validation->passes())
        {
            $this->post->create($input);
            return Response::json(array('success' => true, 'errors' => '', 'message' => 'Post created successfully.'));
        }
        return Response::json(array('success' => false, 'errors' => $validation, 'message' => 'All fields are required.'));
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        $post = $this->post->findOrFail($id);

        return View::make('posts.show', compact('post'));
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function edit($id)
    {
        $post = $this->post->find($id);

        if (is_null($post))
        {
            return Redirect::route('posts.index');
        }

        return View::make('posts.edit', compact('post'));
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  int  $id
     * @return Response
     */
    public function update($id)
    {
        $input = array_except(Input::all(), '_method');
        $validation = Validator::make($input, Post::$rules);

        if ($validation->passes())
        {
            $post = Post::find($id);
            $post->update($input);

            return Response::json(array('success' => true, 'errors' => '', 'message' => 'Post updated successfully.'));
        }

        return Response::json(array('success' => false, 'errors' => $validation, 'message' => 'All fields are required.'));
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return Response
     */
    public function destroy($id)
    {
        $this->post->find($id)->delete();

        return Redirect::route('posts.index');
    }

    public function upload()
    {
        $file = Input::file('file');
        $input = array('image' => $file);
        $rules = array(
            'image' => 'image'
        );
        $validator = Validator::make($input, $rules);
        if ( $validator->fails()) {
            return Response::json(array('success' => false, 'errors' => $validator->getMessageBag()->toArray()));
        }

        $fileName = time() . '-' . $file->getClientOriginalName();
        $destination = public_path() . '/uploads/';
        $file->move($destination, $fileName);

        echo url('/uploads/'. $fileName);
    }
}

This is a pretty familiar controller with some gotchas. First, store and update methods are returning JSON, as they will be called using AJAX. There is also an upload method which will be used for image uploads to the editor.

Next, we need layouts, I created two, one will be used when we don’t need to use the editor:
app/views/layouts/layout.blade.php

<!doctype html>
<html>
	<head>
		<meta charset="utf-8">
        <title>@yield('title') - Laravel Medium editor demo on Codeforest</title>
        <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
        <link rel="stylesheet" href="{{ url('css/medium-editor-insert.css') }}">
        <link rel="stylesheet" href="{{ url('css/style.css') }}">
		<style>
			table form { margin-bottom: 0; }
			form ul { margin-left: 0; list-style: none; }
			.error { color: red; font-style: italic; }
			body { padding-top: 20px; }
		</style>
	</head>

	<body>

		<div class="container">
                @if (Session::has('message'))
                    <div class="flash alert">
                        <p>{{ Session::get('message') }}</p>
                    </div>
                @endif

                @yield('main')
		</div>
	</body>

</html>

And another one which is similar, but is adding a partial view in footer, some needed CSS in header and injecting editors:
app/views/layouts/layout.editor.blade.php

<!doctype html>
<html>
	<head>
		<meta charset="utf-8">
        <title>@yield('title') - Laravel Medium editor demo on Codeforest</title>
        <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.1/css/bootstrap.min.css">
        <link rel="stylesheet" href="{{ url('css/medium-editor.css') }}">
        <link rel="stylesheet" href="{{ url('css/style.css') }}">
        <link rel="stylesheet" href="{{ url('css/themes/default.css') }}">
        <link rel="stylesheet" href="{{ url('css/medium-editor-insert.css') }}">
        <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css">
		<style>
			table form { margin-bottom: 0; }
			form ul { margin-left: 0; list-style: none; }
			.error { color: red; font-style: italic; }
			body { padding-top: 20px; }
		</style>
	</head>

	<body>

		<div class="container">
			@if (Session::has('message'))
				<div class="flash alert">
					<p>{{ Session::get('message') }}</p>
				</div>
			@endif

			@yield('main')
		</div>
    @include('partials.editor')
	</body>

</html>

And we need the partial which will inject the proper scripts:
app/views/partials/editor.blade.php

<script src="//code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.1/js/bootstrap.min.js"></script>
<script src="{{ url('js/medium-editor.js') }}"></script>
<script src="{{ url('js/medium-editor-insert.js') }}"></script>
<script src="{{ url('js/main.js') }}"></script>

The code for medium wysiwyg editor and medium insert plugin can be obtained on Github.

Now, we need the views:
app/views/posts/index.blade.php

@extends('layouts.layout')

@section('title', 'Posts')

@section('main')

<h1>All Posts</h1>

<p>{{ link_to_route('posts.create', 'Add new post') }}</p>

@if ($posts->count())
	<table class="table table-striped">
		<thead>
			<tr>
				<th>Title</th>
				<th>Body</th>
                <th></th>
                <th></th>
                <th></th>
			</tr>
		</thead>

		<tbody>
			@foreach ($posts as $post)
				<tr>
				        <td style="width: 200px;">{{{ strip_tags($post->title) }}}</td>
					<td>{{{ strip_tags(Str::words($post->body, 20)) }}}</td>
                                        <td>{{ link_to_route('posts.show', 'View', array($post->id), array('class' => 'btn btn-info')) }}</td>
                                        <td>{{ link_to_route('posts.edit', 'Edit', array($post->id), array('class' => 'btn btn-info')) }}</td>
                                        <td>
                                             {{ Form::open(array('method' => 'DELETE', 'route' => array('posts.destroy', $post->id))) }}
                                             {{ Form::submit('Delete', array('class' => 'btn btn-danger')) }}
                                             {{ Form::close() }}
                                       </td>
				</tr>
			@endforeach
		</tbody>
	</table>
@else
	There are no posts
@endif

@stop

app/views/posts/create.blade.php

@extends('layouts.layout_editor')

@section('title', 'Create Post')

@section('main')

    <div class="error alert alert-danger"></div>
    <div class="success alert alert-success"></div>

    {{ Form::open(array('route' => 'posts.store')) }}
        <div class="title-editable" id="post-title"><h1>Enter post title</h1></div>
        <div class="body-editable" id="post-body"><p>Enter post body</p></div>
    {{ Form::submit('Save Post', array('class' => 'btn btn-primary', 'id' => 'form-submit')) }}

    {{ Form::close() }}

@stop

app/views/posts/edit.blade.php

@extends('layouts.layout_editor')

@section('title', 'Edit post')

@section('main')

    <div class="error alert alert-danger"></div>
    <div class="success alert alert-success"></div>

    {{ Form::open(array('method' => 'PATCH', 'route' => array('posts.update', $post->id))) }}
        <div class="title-editable" id="post-title">{{ $post->title }}</div>
        <div class="body-editable" id="post-body">{{ $post->body }}</div>
        <input type="hidden" id="post-id" value="{{ $post->id }}">
        {{ Form::submit('Update Post', array('class' => 'btn btn-primary', 'id' => 'form-update')) }}
    {{ Form::close() }}

@stop

app/views/posts/show.blade.php

@extends('layouts.layout_editor')

@section('title', 'View Post')

@section('main')

    <p>{{ link_to_route('posts.index', 'Return to all posts') }}</p>
    <div id="hideEditor" style="display:none;"></div>
    <div id="post-title" class="title-editable">{{ $post->title }}</div>

    <div id="post-body" class="body-editable">{{ $post->body }}</div>

@stop

As you can see, there are no actual forms on create and edit view, just some markup. At the end, we need to add Javascript to make all this work:
public/js/main.js

// initializing editors
var titleEditor = new MediumEditor('.title-editable', {
    buttonLabels: 'fontawesome'
});
var bodyEditor = new MediumEditor('.body-editable', {
    buttonLabels: 'fontawesome'
});
$(function () {
    // initializing insert image on body editor
    $('.body-editable').mediumInsert({
        editor: bodyEditor,
        images: true,
        imagesUploadScript: "{{ URL::to('upload') }}"
    });
    // deactivate editors on show view
    if ($('#hideEditor').length) {
        $('.body-editable').mediumInsert('disable');
        bodyEditor.deactivate();
        titleEditor.deactivate();
    }
});
// hiding messages
$('.error').hide().empty();
$('.success').hide().empty();

// create post
$('body').on('click', '#form-submit', function(e){
    e.preventDefault();
    var postTitle = titleEditor.serialize();
    var postContent = bodyEditor.serialize();

    $.ajax({
        type: 'POST',
        dataType: 'json',
        url : "{{ URL::action('PostsController@store') }}",
        data: { title: postTitle['post-title']['value'], body: postContent['post-body']['value'] },
        success: function(data) {
            if(data.success === false)
            {
                $('.error').append(data.message);
                $('.error').show();
            } else {
                $('.success').append(data.message);
                $('.success').show();
                setTimeout(function() {
                    window.location.href = "{{ URL::action('PostsController@index') }}";
                }, 2000);
            }
        },
        error: function(xhr, textStatus, thrownError) {
            alert('Something went wrong. Please Try again later...');
        }
    });
    return false;
});

// update post
$('body').on('click', '#form-update', function(e){
    e.preventDefault();
    var postTitle = titleEditor.serialize();
    var postContent = bodyEditor.serialize();

    $.ajax({
        type: 'PUT',
        dataType: 'json',
        url : "{{ URL::action('PostsController@update', array(Request::segment(2))) }}",
        data: { title: postTitle['post-title']['value'], body: postContent['post-body']['value'] },
        success: function(data) {
            if(data.success === false)
            {
                $('.error').append(data.message);
                $('.error').show();
            } else {
                $('.success').append(data.message);
                $('.success').show();
                setTimeout(function() {
                    window.location.href = "{{ URL::action('PostsController@index') }}";
                }, 2000);
            }
        },
        error: function(xhr, textStatus, thrownError) {
            alert('Something went wrong. Please Try again later...');
        }
    });
    return false;
});

Actually, that’s it. I think that the above JavaScript code which make AJAX calls for create and update post is self explanatory.

The only thing left is adjusting the routes file:
app/routes.php

// upload image route for MediumInsert plugin
Route::any('upload', 'PostsController@upload');
// resource routes for posts
Route::resource('posts', 'PostsController');

If you made it to here, you can now test all this in your browser…first run a server:

php artisan serve

and point your browser to http://localhost:8000.

You can see all of this in action or download code from GitHub below:

DemoSource files

I hope you will find this useful in your next awesome Laravel 4 project. If you have any questions, do not hesitate to ask below in comments.

Tweet about this on TwitterShare on FacebookShare on Google+Share on LinkedInShare on RedditShare on StumbleUpon



  • Softbox Technologies

    Really nice

  • http://eezy.com/ Shawn Rubel

    This is wonderful work, can’t believe how extensive the tutorial is; will be adding it to my collection of Laravel resources – hoping to find good use for it.

    Thank you, Zvonko. :)

  • Mladjo

    is it possible to update javascript part from main.js that works with medium-editor-insert-plugin 0.2.1
    Thanks

    • landry

      Hi did you manage this! i was trying something similar with no much luch due to an 405 ajax error.

  • http://www.tekkianswer.com/ Ramel de la Cruz

    Thanks for sharing! This is great. :)