Skip to content

Laravel WYSIWYG editor

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:

[code lang=”php”]
composer create-project laravel/laravel laravel-medium –prefer-dist
[/code]

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:

[code lang=”php”]
php artisan migrate:make create_posts_table
[/code]

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

[code lang=”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’);
}
[/code]

And migrate the database:

[code lang=”php”]
php artisan migrate
[/code]

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

[code lang=”php”]
class Post extends \Eloquent {
protected $guarded = array();

public static $rules = array(
‘title’ => ‘required’,
‘body’ => ‘required’
);
}
[/code]

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

[code lang=”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);
}
}
[/code]

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

[code lang=”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>
[/code]

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

[code lang=”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>
[/code]

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

[code lang=”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>
[/code]

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

[code lang=”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
[/code]

app/views/posts/create.blade.php

[code lang=”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
[/code]

app/views/posts/edit.blade.php

[code lang=”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
[/code]

app/views/posts/show.blade.php

[code lang=”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
[/code]

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

[code lang=”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;
});
[/code]

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

[code lang=”php”]
// upload image route for MediumInsert plugin
Route::any(‘upload’, ‘PostsController@upload’);
// resource routes for posts
Route::resource(‘posts’, ‘PostsController’);
[/code]

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

[code lang=”php”]
php artisan serve
[/code]

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.

5 thoughts on “Laravel WYSIWYG editor”

  1. 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. 🙂

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

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

Comments are closed.