Add Tags to Articles: Laravel Many-to-Many Relationships with Select2
Founder of QuickAdminPanel
This article is a simple example of belongsToMany() relationships in Laravel, with managing the data in CRUD form and table, using Bootstrap framework and Select2 library.
Here’s what we’re building – a list of articles with their tags.
Notice: This example was mostly generated with our QuickAdminPanel generator, but will be explained step-by-step in plain Laravel, so you can use it without our generator.
Step 1. Database/model structure
Here are the migrations.
Articles:
Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title')->nullable(); $table->text('article_text')->nullable(); $table->timestamps(); });
Tags:
Schema::create('tags', function (Blueprint $table) { $table->increments('id'); $table->string('name')->nullable(); $table->timestamps(); });
Pivot table:
Schema::create('article_tag', function (Blueprint $table) { $table->integer('article_id')->unsigned()->nullable(); $table->foreign('article_id')->references('id')->on('articles')->onDelete('cascade'); $table->integer('tag_id')->unsigned()->nullable(); $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); });
The models are really simple, too.
app/Tag.php:
class Tag extends Model { protected $fillable = ['name']; }
app/Article.php:
class Article extends Model { protected $fillable = ['title', 'article_text']; public function tag() { return $this->belongsToMany(Tag::class, 'article_tag'); } }
Routes, Controller and Create Form
Our routes/web.php, in addition to Route Group and Middleware, will have this main line:
Route::group(['middleware' => ['auth'], 'prefix' => 'admin', 'as' => 'admin.'], function () { // ... other routes Route::resource('articles', 'Admin\ArticlesController'); });
Now, here’s our (simplified) app/Http/Controllers/Admin/ArticlesController.php:
namespace App\Http\Controllers\Admin; use App\Article; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\StoreArticlesRequest; use App\Http\Requests\Admin\UpdateArticlesRequest; class ArticlesController extends Controller { public function index() { $articles = Article::all(); return view('admin.articles.index', compact('articles')); } public function create() { $tags = \App\Tag::get()->pluck('name', 'id'); return view('admin.articles.create', compact('tags')); } public function store(StoreArticlesRequest $request) { // ... to be discussed later } public function edit($id) { // ... to be discussed later } public function update(UpdateArticlesRequest $request, $id) { // ... to be discussed later } public function destroy($id) { // ... to be discussed later } }
To add the articles with their tags, we need to have a create form.
Here’s our resources/views/admin/articles/create.blade.php:
@extends('layouts.app') @section('content') <h3 class="page-title">Articles</h3> {!! Form::open(['method' => 'POST', 'route' => ['admin.articles.store']]) !!} <div class="panel panel-default"> <div class="panel-heading"> Create </div> <div class="panel-body"> <div class="row"> <div class="col-xs-12 form-group"> {!! Form::label('title', 'Title', ['class' => 'control-label']) !!} {!! Form::text('title', old('title'), ['class' => 'form-control', 'placeholder' => '']) !!} <p class="help-block"></p> @if($errors->has('title')) <p class="help-block"> {{ $errors->first('title') }} </p> @endif </div> </div> <div class="row"> <div class="col-xs-12 form-group"> {!! Form::label('article_text', 'Article Text', ['class' => 'control-label']) !!} {!! Form::textarea('article_text', old('article_text'), ['class' => 'form-control ', 'placeholder' => '']) !!} <p class="help-block"></p> @if($errors->has('article_text')) <p class="help-block"> {{ $errors->first('article_text') }} </p> @endif </div> </div> <div class="row"> <div class="col-xs-12 form-group"> {!! Form::label('tag', 'Tags', ['class' => 'control-label']) !!} <button type="button" class="btn btn-primary btn-xs" id="selectbtn-tag"> Select all </button> <button type="button" class="btn btn-primary btn-xs" id="deselectbtn-tag"> Deselect all </button> {!! Form::select('tag[]', $tags, old('tag'), ['class' => 'form-control select2', 'multiple' => 'multiple', 'id' => 'selectall-tag' ]) !!} <p class="help-block"></p> @if($errors->has('tag')) <p class="help-block"> {{ $errors->first('tag') }} </p> @endif </div> </div> </div> </div> {!! Form::submit('Save article', ['class' => 'btn btn-danger']) !!} {!! Form::close() !!} @stop @section('styles') @parent <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css" rel="stylesheet" /> @stop @section('javascript') @parent <script src="//code.jquery.com/jquery-1.11.3.min.js"></script> <script src="https://code.jquery.com/ui/1.11.3/jquery-ui.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js"></script> <script> $("#selectbtn-tag").click(function(){ $("#selectall-tag > option").prop("selected","selected"); $("#selectall-tag").trigger("change"); }); $("#deselectbtn-tag").click(function(){ $("#selectall-tag > option").prop("selected",""); $("#selectall-tag").trigger("change"); }); $(document).ready(function () { $('.select2').select2(); }); </script> @stop
This is our visual result:
Now, there are quite a few places to explain here:
- In this article, I won’t discuss the main layout and code like @extends(‘layouts.app’) and @section(‘content’) because you may have different design and structure, it’s not the subject of this article;
- We use LaravelCollective/html package to build the forms with {!! Form::open() !!} and other methods;
- See how $tags are passed into the form, we’re getting those values from the Controller, mentioned above;
- Above the tags field, we have two buttons: Select all and Deselect all – their behavior in jQuery is implemented in @section(‘javascript’);
- We use Select2 library to make the tags searchable and visually compelling.
So here we have our create form.
Saving the data
This part is pretty simple, here’s a method from app/Http/Controllers/Admin/ArticlesController.php:
public function store(StoreArticlesRequest $request) { $article = Article::create($request->all()); $article->tag()->sync((array)$request->input('tag')); return redirect()->route('admin.articles.index'); }
To validate the data, we use app/Http/Requests/StoreArticlesRequest.php class:
namespace App\Http\Requests\Admin; use Illuminate\Foundation\Http\FormRequest; class StoreArticlesRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'tag.*' => 'exists:tags,id', ]; } }
And in the Controller, we just store the article and then use sync() method of many-to-many relationships to store the tags.
Viewing tags in the table
In the screenshot on the top you can see how tags are structured in the table. Here’s our Blade file for this.
resources/views/admin/articles/index.blade.php:
<div class="panel-body table-responsive"> <table class="table table-bordered table-striped datatable"> <thead> <tr> <th>Title</th> <th>Tags</th> <th> </th> </tr> </thead> <tbody> @if (count($articles) > 0) @foreach ($articles as $article) <tr data-entry-id="{{ $article->id }}"> <td field-key='title'>{{ $article->title }}</td> <td field-key='tag'> @foreach ($article->tag as $singleTag) <span class="label label-info label-many">{{ $singleTag->name }}</span> @endforeach </td> <td> @can('article_view') <a href="{{ route('admin.articles.show',[$article->id]) }}" class="btn btn-xs btn-primary">View</a> @endcan @can('article_edit') <a href="{{ route('admin.articles.edit',[$article->id]) }}" class="btn btn-xs btn-info">Edit</a> @endcan @can('article_delete') {!! Form::open(array( 'style' => 'display: inline-block;', 'method' => 'DELETE', 'onsubmit' => "return confirm('Are you sure?');", 'route' => ['admin.articles.destroy', $article->id])) !!} {!! Form::submit('Delete', array('class' => 'btn btn-xs btn-danger')) !!} {!! Form::close() !!} @endcan </td> @endif </tr> @endforeach @else <tr> <td colspan="8">@lang('global.app_no_entries_in_table')</td> </tr> @endif </tbody> </table> </div>
This is the most important line – we’re using Bootstrap label classes for styling:
<span class="label label-info label-many">{{ $singleTag->name }}</span>
Editing the Article
Here’s Controller code:
public function edit($id) { $tags = \App\Tag::get()->pluck('name', 'id'); $article = Article::findOrFail($id); return view('admin.articles.edit', compact('article', 'tags')); }
And in resources/views/admin/articles/edit.blade.php we show tags this way:
{!! Form::label('tag', trans('global.articles.fields.tag').'', ['class' => 'control-label']) !!} <button type="button" class="btn btn-primary btn-xs" id="selectbtn-tag"> {{ trans('global.app_select_all') }} </button> <button type="button" class="btn btn-primary btn-xs" id="deselectbtn-tag"> {{ trans('global.app_deselect_all') }} </button> {!! Form::select('tag[]', $tags, old('tag') ? old('tag') : $article->tag->pluck('id')->toArray(), ['class' => 'form-control select2', 'multiple' => 'multiple', 'id' => 'selectall-tag' ]) !!} <p class="help-block"></p> @if($errors->has('tag')) <p class="help-block"> {{ $errors->first('tag') }} </p> @endif
As you can see, we pull in the current value as $article->tag->pluck(‘id’)->toArray().
Updating the data is straightforward, here’s the controller method, really similar to store():
public function update(CreateArticlesRequest $request, $id) { $article = Article::findOrFail($id); $article->update($request->all()); $article->tag()->sync((array)$request->input('tag')); return redirect()->route('admin.articles.index'); }
Deleting the data
This is probably the most simple code – here’s Controller method:
public function destroy($id) { $article = Article::findOrFail($id); $article->delete(); return redirect()->route('admin.articles.index'); }
But keep in mind that in our migrations files we specified that we need to delete tags on deleting the article, with this rule onDelete(‘cascade’):
Schema::create('article_tag', function (Blueprint $table) { $table->integer('article_id')->unsigned()->nullable(); $table->foreign('article_id')->references('id')->on('articles')->onDelete('cascade'); $table->integer('tag_id')->unsigned()->nullable(); $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); });
Basically, that’s it – a simple demo of many-to-many with Select2 and Bootstrap. If you want it to be generated automatically, try our QuickAdminPanel!