Add update and delete verbs

1. Add a route for edit and delete

Edit `/routes/web.php’ and add the new routes:

Route::get('todo/{id}/edit', 'TodoController@edit')->where('id', '[0-9]+')->middleware('auth');
Route::put('todo', 'TodoController@put')->middleware('auth');
Route::delete('todo/{id}', 'TodoController@delete')->where('id', '[0-9]+')->middleware('auth');

These routes are using HTTP methods and thus use Route::delete() and Route::put() to be ready for REST approach.

Route::get('todo/{id}/edit', 'TodoController@edit')->where('id', '[0-9]+')->middleware('auth'); is for displaying the form with, already filled in, the data coming from the todo that should be edited.

So, two actions but three new routes.

2. Update master view and add script

Edit `/resources/views/master.blade.php’ and add jquery and a yield variable to make possible to inject js statements:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Some stupid Todos application</title>
  <link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
</head>
<body>
  <main role="main">
    <div class="jumbotron">
      <div class="container">
        <h1 class="display-3">Some stupid Todos application</h1>
        <p>A simple Laravel application</p>
      </div>
    </div>
  </main>
  <div class="container">
    @yield('content')
    <hr/>
    @yield('navigation')
  </div>
  @yield('script')
</body>
</html>

3. Add buttons to the detail view

Edit `/resources/views/show.blade.php’ and add jquery and a yield variable.

The HTTP method (PUT or DELETE) will be derived from the text of the button.

The Ajax part will send the request to the server then remove the two buttons for displaying a text.

@extends('master')

@section('content')
  @isset($data)
    <h3>{{ $data->title }}</h3>
    <p>{{ $data->description }}</p>
    <small>
      Created at: {{ $data->created_at }}
      <br/>
      Last updated: {{ $data->updated_at }}
      <br/>
      Author: {{ $data->user->name }}
    </small>
    <hr/>
    <span class="buttons">
      <input type="button" value="Update" class="edit"/> - <input type="button" value="Delete" class="delete"/>
    </span>  
  @endisset
@endsection

@section('navigation')
  <a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
@endsection

@section('script')
<script defer="defer">
  $('.delete, .edit').click(function(){

    if (this.value.toUpperCase() === 'DELETE') {

      // Add the csrf-token protection but only when the request is
      // made on the same site (no cross-domain).
      // Don't share the token outside
      $.ajaxSetup({
        beforeSend: function(xhr, type) {
          if (!type.crossDomain) {
              xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
          }
      }
      });

      $.ajax({
        url: '/todo/{{ $data->id }}',
        type: 'DELETE',
        contentType: 'application/json',
        success: function (data) {
          if (data.hasOwnProperty("message")) {
            // Replace buttons and display the feedback message
            $('.buttons').html(data.message);
          }
        },
        error: function (data, textStatus, errorThrown) {
          console.log(data);
        }
      });
    } else {
      window.location.replace('/todo/{{ $data->id }}/edit');
    }
});
</script>
@endsection

4. Add the delete action

Edit /app/Http/Controllers/TodoController.php and add a delete() function and add the Response facade:

use Response;

public function delete($id)
{
  Todo::destroy($id);

  return Response::json([
    'status' => true,
    'message' => '<div class="alert alert-success" role="alert">Successfully deleted</div>'
  ]);
}

So, the delete() function will return a JSON output and the Ajax success() anonymous function will use the message and output it:

Feedback

5. Add the put action

Edit /app/Http/Controllers/TodoController.php and add a put() function:

public function put(TodoRequest $request)
{
  // Retrieve the record
  $todo = Todo::where('id', $request->input('id'))->firstOrFail();

  // List of fields that we'll update
  $todo->update($request->only(['title', 'completed', 'description']));

  // Redirect to the edit form
  return redirect()->back()
    ->with('message', '<div class="alert alert-success" role="alert">Successfully updated</div>');
}

We’ll reuse our todo view i.e. the one that is displaying the form.

6. Update the Todo model

Since the update() method requires the use of the $fillable property, we need to update our model. $fillable lists the fields that can be updated so not mentioning user_id f.i. will make the update() method fails when trying to modifying that specific field.

Edit /App/Todo.php and add the property:

public $fillable = ['title', 'completed', 'description'];

7. Edit the form and show the record

Edit /resources/views/form.blade.php and add $data variable like below. Add also extra informations like the completed flag and the ID in a hidden field.

@extends('master')

@section('content')

  <div class="panel-heading">Hi {{ Auth::user()->name }}, please add your new Todo below</div>  
  <div class="panel-body">
    @if(Session::has('message'))
      {!! Session::get('message') !!}
    @endif
    {!! Form::open(['route' => 'storeTodo', 'method' => (isset($data) ? 'PUT' : 'POST')]) !!}
      {!! Form::hidden('id', $data->id ?? 0) !!}
      <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
        {!! Form::text('title', $data->title ?? '', array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
        {!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
      </div>
      <div class="form-group ">
        {!! Form::checkbox('completed', 1, (isset($data) ? ($data->completed==1) : 0))  !!}
        {!! Form::label('completed', 'Completed'); !!}
      </div>
      <div class="form-group">
        {!! Form::label('description', 'Description (optional)'); !!}
        {!! Form::textarea('description', $data->description ?? '', array('class' => 'form-control')) !!}
      </div>
      {!! Form::submit('Submit !') !!}
    {!! Form::close() !!}
  </div>
@endsection

@section('navigation')
  <a href="/todos">Show all</a>
@endsection

7.1. Tips:

We can use the @auth directive; more powerful.

Note: HTML::Forms doesn’t support the PUT method so the trick is to add a _method field. This can be done with the line below.

If we’ve a record (so, we’ve an ID), we’ll use the PUT method, if not, this is a new record so use POST.

Form::open(['route' => 'storeTodo', 'method' => (isset($data) ? 'PUT' : 'POST')])

Since Laravel 5.6, we can also just use @method('put').

@section('content')
  <form>
    @method('put')
    ...

The three lines below will allow to output message sent by the controller.

@if(Session::has('message'))
  {!! Session::get('message') !!}
@endif

The put() in the controller indeed redirect with a message. Laravel use the Session object for storing the message.

return redirect()->back()
  ->with('message', '<div class="alert alert-success" role="alert">Successfully updated</div>');

8. Test

Everything is now in place.

Go to http://127.0.0.1:8000/todos, display a Todo by clicking on his title, the detail of the todo will be displayed with two buttons : Update and Delete.

By clicking on Update, the form will be displayed with the todo’s info loaded.

Put

Clicking on the Submit ! button will send a PUT request to the server and the put() function of the controller will be called; make the changes and show the form back.

By clicking on Delete, an Ajax call will be made with a DELETE request, the delete() function of the controller will be called and a alert message will then be displayed.

Pay attention, in the database, to the updated_at column: Laravel will automatically update that column. Just because our model (/app/Todo.php) has mentionned $timestamp = true;.