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 - Stop the NPM
- 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 namebtn-create
- Replace the
<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
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 emptytbody
tag) - Remove the
shared.alert
include from the page
- Remove the entire
- 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Update the controller
- Open the file app/Http/Controllers/Admin/GenreController.php
- Go to the
index()
method- Replace
return view('admin.genres.index', $result);
withreturn view('admin.genres.index');
- Delete the rest of the code
- Replace
public function index()
{
return view('admin.genres.index');
}
1
2
3
4
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)
- Get all the genres (ordered by
public function qryGenres()
{
$genres = Genre::orderBy('name')
->withCount('records')
->get();
return $genres;
}
1
2
3
4
5
6
7
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
2
3
4
5
6
- Test the route in a new browser window http://localhost:3000/admin/genres/qryGenres
Build the table with AJAX
- Add a JavaScript function
loadTable()
that loads the genres via a call of thegetJSON()
method with the routeadmin/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 thetbody
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
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
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 thedeleteGenre()
function and close the modal - Nothing will be deleted at this moment, you only see the
id
inside the browser console
- Get the all the information about the genre (via the data attributes of the corresponding
<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
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
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
- Instead of a redirect, we use the helper function
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
2
3
4
5
6
7
8
Delete a genre and rebuild the table
- Use the
post()
method to post theid
of the selected genre toadmin/genres/{id}
via AJAX- Don't forget to add the parameters csrf token (
_token
) and the method (_method
) delete
- Don't forget to add the parameters csrf token (
- 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
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')
- Use
data.type
anddata.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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>×</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
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
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)
- Get the necessary information about the genre (via the data attributes of the corresponding
$(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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 thepost()
method call - If the
post()
method fails,e.responseJSON.errors
contains an array with all the validation errors
- The jQuery method
$(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
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
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
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
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()
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()
andedit()
) to the index page
public function create()
{
return redirect('admin/genres');
}
1
2
3
4
2
3
4
public function edit(Genre $genre)
{
return redirect('admin/genres');
}
1
2
3
4
2
3
4
EXERCISE: Add Bootstrap tooltips
- Add Bootstrap-styled tooltips to the edit and delete buttons of the dynamically added genre list
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
2
3