Admin: genres (p2)

  • Version 1 of our genres CRUD implementation is not so user friendly
    • We have different views (index.blade.php, create.blade.php and edit.blade.php) and multiple page reloads
    • The confirmation behind the delete button uses a standard browser dialog box/window
  • In this part, we refactor the whole logic to just one page where all the actions go through JavaScript and AJAX

TIPS

  • First, add some dummy genres (e.g. '_genre1', '_genre2', '_genre3', ...) to the database you can play with
  • If you mess up your database, you can always bring it back to the original state with the command:
    php artisan migrate:fresh

Noty

  • Noty is a JavaScript library to send notifications the the user
  • Install and configure Noty:
    • Stop the NPM watch script
    • Install Noty with the command npm install noty
    • Open resources/sass/app.scss and import the Noty and the Bootstrap theme styles


     
     

    @import url('https://fonts.googleapis.com/css?family=Nunito');
    @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css');
    @import '~noty/src/noty';
    @import '~noty/src/themes/bootstrap-v4';
    
    1
    2
    3
    4
    • Open resources/js/app.js and import the Noty script

     

    require('./bootstrap');
    window.Noty = require('noty');
    
    1
    2
    • Open resources/js/vinylShop.js and add some defaults for Noty
    Noty.overrideDefaults({
        layout: 'topRight',
        theme: 'bootstrap-v4',
        timeout: 3000
    });
    
    1
    2
    3
    4
    5
  • Restart the the NPM watch script: npm run watch

Master page

  • We have to refactor most parts of the view resources/views/admin/genres/index.blade.php, because the table will now be composed with AJAX
  • Update the 'Create new genre' button
    • Replace the href attribute with a null link
    • Give the link an id with name btn-create

 




<p>
    <a href="#!" class="btn btn-outline-success" id="btn-create">
        <i class="fas fa-plus-circle mr-1"></i>Create new genre
    </a>
</p>
1
2
3
4
5
  • Only the static/fixed parts of the page are retained
    • Remove the entire @foreach block from the page (resulting in an empty tbody tag)
    • Remove the shared.alert include from the page
  • This is what's left over in the main section:
@section('main')
    <h1>Genres</h1>

    <p>
        <a href="#!" class="btn btn-outline-success" id="btn-create">
            <i class="fas fa-plus-circle mr-1"></i>Create new genre
        </a>
    </p>
    <div class="table-responsive">
        <table class="table">
            <thead>
            <tr>
                <th>#</th>
                <th>Genre</th>
                <th>Records for this genre</th>
                <th>Actions</th>
            </tr>
            </thead>
            <tbody>
           
            </tbody>
        </table>
    </div>
@endsection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

'Master page with empty table'

Update the controller

  • Open the file app/Http/Controllers/Admin/GenreController.php
  • Go to the index() method
    • Replace return view('admin.genres.index', $result); with return view('admin.genres.index');
    • Delete the rest of the code


 


public function index()
{
    return view('admin.genres.index');
}
1
2
3
4
  • Add, at the bottom of the controller, a new qryGenres() method
    • Get all the genres (ordered by name and with the number of records of this genre)
    • Return the result (as JSON)
public function qryGenres()
{
    $genres = Genre::orderBy('name')
        ->withCount('records')
        ->get();
    return $genres;
}
1
2
3
4
5
6
7

Add a route for the new query

  • Open routes/web.php and add a new route
  • It's important to place this route BEFORE the resource route!


 




Route::middleware(['auth', 'admin'])->prefix('admin')->group(function () {
    route::redirect('/', 'records');
    Route::get('genres/qryGenres', 'Admin\GenreController@qryGenres');
    Route::resource('genres', 'Admin\GenreController');
    Route::get('records', 'Admin\RecordController@index');
});
1
2
3
4
5
6

Build the table with AJAX

  • Add a JavaScript function loadTable() that loads the genres via a call of the getJSON() method with the route admin/genres/qryGenres as parameter
    • Call this function immediately when the page is loaded
    • Construct for every genre a table row and append this row to the tbody tag
      (We use this function every time we edit, update or delete a genre, so clear the tbody tag first)
    • Replace the script inside the script_after section with the following script:
@section('script_after')
    <script>
        $(function () {
            loadTable();
        });

        // Load genres with AJAX
        function loadTable() {
            $.getJSON('/admin/genres/qryGenres')
                .done(function (data) {
                    console.log('data', data);
                    // Clear tbody tag
                    $('tbody').empty();
                    // Loop over each item in the array
                    $.each(data, function (key, value) {
                        let tr = `<tr>
                               <td>${value.id}</td>
                               <td>${value.name}</td>
                               <td>${value.records_count}</td>
                               <td data-id="${value.id}"
                                   data-records="${value.records_count}"
                                   data-name="${value.name}">
                                    <div class="btn-group btn-group-sm">
                                        <a href="#!" class="btn btn-outline-success btn-edit">
                                            <i class="fas fa-edit"></i>
                                        </a>
                                        <a href="#!" class="btn btn-outline-danger btn-delete">
                                            <i class="fas fa-trash"></i>
                                        </a>
                                    </div>
                               </td>
                           </tr>`;
                        // Append row to tbody
                        $('tbody').append(tr);
                    });
                })
                .fail(function (e) {
                    console.log('error', e);
                })
        }
    </script>
@endsection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

'Master page with filled table'

Delete a genre

Noty confirm box

  • Replace the default confirm box with a Noty Confirm
  • When you click on a delete button:
    • Get the all the information about the genre (via the data attributes of the corresponding td tag)
    • Set some default properties (text inside the modal, text inside the button and some colors) for the Noty modal
    • Overwrite these properties if there are records associated to the genre
    • When you click on the delete button inside the modal, send the id of the genre to the deleteGenre() function and close the modal
    • Nothing will be deleted at this moment, you only see the id inside the browser console




 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


 
 
 
 
 





<script>
    $(function () {
        loadTable();

        $('tbody').on('click', '.btn-delete', function () {
            // Get data attributes from td tag
            let id = $(this).closest('td').data('id');
            let name = $(this).closest('td').data('name');
            let records = $(this).closest('td').data('records');
            // Set some values for Noty
            let text = `<p>Delete the genre <b>${name}</b>?</p>`;
            let type = 'warning';
            let btnText = 'Delete genre';
            let btnClass = 'btn-success';
            // If records not 0, overwrite values for Noty
            if (records > 0) {
                text += `<p>ATTENTION: you are going to delete ${records} records at the same time!</p>`;
                btnText = `Delete genre + ${records} records`;
                btnClass = 'btn-danger';
                type = 'error';
            }
            // Show Noty
            let modal = new Noty({
                timeout: false,
                layout: 'center',
                modal: true,
                type: type,
                text: text,
                buttons: [
                    Noty.button(btnText, `btn ${btnClass}`, function () {
                        // Delete genre and close modal
                        deleteGenre(id);
                        modal.close();
                    }),
                    Noty.button('Cancel', 'btn btn-secondary ml-2', function () {
                        modal.close();
                    })
                ]
            }).show();
        });
    });

    // Delete a genre
    function deleteGenre(id) {
        // Delete the genre from the database
        console.log('id', id);
    }

    // Load genres with AJAX
    function loadTable() {...}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

'Delete a genre without records' 'Delete a genre with records'

Update the controller

  • Update the destroy() method inside the controller
    • Instead of a redirect, we use the helper function response() to return some JSON-encoded data (that will be used in a popup notification) back to the view



 
 
 
 


public function destroy(Genre $genre)
{
    $genre->delete();
    return response()->json([
        'type' => 'success',
        'text' => "The genre <b>$genre->name</b> has been deleted"
    ]);
}
1
2
3
4
5
6
7
8

Delete a genre and rebuild the table

  • Use the post() method to post the id of the selected genre to admin/genres/{id} via AJAX
    • Don't forget to add the parameters csrf token (_token) and the method (_method) delete
  • Update the deleteGenre() function:


 
 
 
 
 
 
 
 
 
 
 
 
 
 

function deleteGenre(id) {
    // Delete the genre from the database
    let pars = {
        '_token': '{{ csrf_token() }}',
        '_method': 'delete'
    };
    $.post(`/admin/genres/${id}`, pars, 'json')
        .done(function (data) {
            console.log('data', data);
            // Rebuild the table
            loadTable();
        })
        .fail(function (e) {
            console.log('error', e);
        });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Noty toast response

  • Open the browser console and take a look at the data that comes back as a response from the server (when deleting e.g. the genre 'afrobeat')

'Delete a genre - console window'

  • Use data.type and data.text to create a Noty toast





 
 
 
 
 








$(function () {
    ...
    $.post(`/admin/genres/${id}`, pars, 'json')
        .done(function (data) {
            console.log('data', data);
            // Show toast
            new Noty({
                type: data.type,
                text: data.text
            }).show();
            // Rebuild the table
            loadTable();
        })
        .fail(function (e) {
            console.log('error', e);
        })
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

'Show toast'

Edit a genre

Add a Bootstrap modal to the page

  • Add a new file modal.blade.php to the resources/views/admin/genres folder
  • Create a Bootstrap modal and place a form with one text field inside the body of the modal




 





 
 








 









<div class="modal" id="modal-genre">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">modal-genre-title</h5>
                <button type="button" class="close" data-dismiss="modal">
                    <span>&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <form action="" method="post">
                    @method('')
                    @csrf
                    <div class="form-group">
                        <label for="name">Name</label>
                        <input type="text" name="name" id="name"
                               class="form-control"
                               placeholder="Name"
                               minlength="3"
                               required
                               value="">
                        <div class="invalid-feedback"></div>
                    </div>
                    <button type="submit" class="btn btn-success">Save genre</button>
                </form>
            </div>
        </div>
    </div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

REMARK

  • We will use the same form for editing a genre and for creating a new genre!
  • Depending on the action we have to dynamically change the form:
Edit Create
.modal-title Edit "name of genre" New genre
form action /admin/genres/{id} /admin/genres
@method PUT POST
input value original value empty
  • Include the sub-view at the end of the index page





 


@section('main')
    ...
    <div class="table-responsive">
        ...
    </div>
    @include('admin.genres.modal')
@endsection
1
2
3
4
5
6
7
  • When you click on an edit button:
    • Get the necessary information about the genre (via the data attributes of the corresponding td tag)
    • Update the modal window (see table above)
    • Make the modal visible (it is hidden by default)





 
 
 
 
 
 
 
 
 
 
 
 


$(function () {
     loadTable();

     $('tbody').on('click', '.btn-delete', function () { ... }

     $('tbody').on('click', '.btn-edit', function () {
         // Get data attributes from td tag
         let id = $(this).closest('td').data('id');
         let name = $(this).closest('td').data('name');
         // Update the modal
         $('.modal-title').text(`Edit ${name}`);
         $('form').attr('action', `/admin/genres/${id}`);
         $('#name').val(name);
         $('input[name="_method"]').val('put');
         // Show the modal
         $('#modal-genre').modal('show');
     });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

'Edit modal'

Update the controller

  • Update the update() method inside the controller








 
 
 
 


public function update(Request $request, Genre $genre)
{
    $this->validate($request,[
        'name' => 'required|min:3|unique:genres,name,' . $genre->id
    ]);
            
    $genre->name = $request->name;
    $genre->save();
    return response()->json([
        'type' => 'success',
        'text' => "The genre <b>$genre->name</b> has been updated"
    ]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Update the genre and rebuild the table

  • Post the form with AJAX when the form is submitted
    • The jQuery method serialize() creates a URL encoded text string of the form values, which is used as a parameter of the post() method call
    • If the post() method fails, e.responseJSON.errors contains an array with all the validation errors







 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


$(function () {
    loadTable();

    $('tbody').on('click', '.btn-delete', function () { ... }

    $('tbody').on('click', '.btn-edit', function () { ... }

    $('#modal-genre form').submit(function (e) {
        // Don't submit the form
        e.preventDefault();
        // Get the action property (the URL to submit)
        let action = $(this).attr('action');
        // Serialize the form and send it as a parameter with the post
        let pars = $(this).serialize();
        console.log(pars);
        // Post the data to the URL
        $.post(action, pars, 'json')
            .done(function (data) {
                console.log(data);
                // Noty success message
                new Noty({
                    type: data.type,
                    text: data.text
                }).show();
                // Hide the modal
                $('#modal-genre').modal('hide');
                // Rebuild the table
                loadTable();
            })
            .fail(function (e) {
                console.log('error', e);
                // e.responseJSON.errors contains an array of all the validation errors
                console.log('error message', e.responseJSON.errors);
                // Loop over the e.responseJSON.errors array and create an ul list with all the error messages
                let msg = '<ul>';
                $.each(e.responseJSON.errors, function (key, value) {
                    msg += `<li>${value}</li>`;
                });
                msg += '</ul>';
                // Noty the errors
                new Noty({
                    type: 'error',
                    text: msg
                }).show();
            });
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

'Edit modal: success' 'Edit modal with empty name' 'Edit modal with exisiting name'

Create a new genre

  • When you click on the 'Create new genre' button:
    • Update the modal window (see table above)
    • Make the modal visible









 
 
 
 
 
 
 
 
 


$(function () {
    loadTable();

    $('tbody').on('click', '.btn-delete', function () { ... }

    $('tbody').on('click', '.btn-edit', function () { ... }

    $('#modal-genre form').submit(function (e) { ... }

    $('#btn-create').click(function () {
        // Update the modal
        $('.modal-title').text(`New genre`);
        $('form').attr('action', `/admin/genres`);
        $('#name').val('');
        $('input[name="_method"]').val('post');
        // Show the modal
        $('#modal-genre').modal('show');
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Update the controller

  • Update the store() method inside the controller









 
 
 
 


public function store(Request $request)
{
    $this->validate($request,[
        'name' => 'required|min:3|unique:genres,name'
    ]);

    $genre = new Genre();
    $genre->name = $request->name;
    $genre->save();
    return response()->json([
        'type' => 'success',
        'text' => "The genre <b>$genre->name</b> has been added"
    ]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Insert the genre and rebuild the table

  • This will be done automatically, because we use the same form and the same script:
    $('#modal-genre form').submit()

'Create modal' 'Create modal - notify toast'

Cleanup your code

  • Delete all the Blade files (create.blade.php, edit.blade.php and form.blade.php) you don't use anymore
  • Redirect all unnecessary methods inside the controller (create() and edit()) to the index page


 


public function create()
    {
        return redirect('admin/genres');
    }
1
2
3
4


 


public function edit(Genre $genre)
   {
       return redirect('admin/genres');
   }
1
2
3
4

EXERCISE: Add Bootstrap tooltips

  • Add Bootstrap-styled tooltips to the edit and delete buttons of the dynamically added genre list

'Bootstrap tooltips'

Commit "Opdracht 8: <= Admin: genres (p2)"

  • Execute the following commands in a terminal window:
git add .
git commit -m "Opdracht 8: <= Admin: genres (p2)"
git push
1
2
3
Last Updated: 12/3/2019, 3:58:32 PM