If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeTime to finally hook up the AJAX and delete one of these rows! Woohoo!
As an early birthday gift, I already took care of the server-side work for us. If you want to check it out, it's inside of the src/AppBundle/Controller
directory: RepLogController
:
... lines 1 - 2 | |
namespace AppBundle\Controller; | |
... lines 4 - 13 | |
class RepLogController extends BaseController | |
{ | |
... lines 16 - 129 | |
} |
I have a bunch of different RESTful API endpoints and one is called, deleteRepLogAction()
:
... lines 1 - 5 | |
use AppBundle\Entity\RepLog; | |
... line 7 | |
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; | |
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; | |
... line 10 | |
use Symfony\Component\HttpFoundation\Response; | |
... lines 12 - 13 | |
class RepLogController extends BaseController | |
{ | |
... lines 16 - 46 | |
/** | |
* @Route("/reps/{id}", name="rep_log_delete") | |
* @Method("DELETE") | |
*/ | |
public function deleteRepLogAction(RepLog $repLog) | |
{ | |
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); | |
$em = $this->getDoctrine()->getManager(); | |
$em->remove($repLog); | |
$em->flush(); | |
return new Response(null, 204); | |
} | |
... lines 60 - 129 | |
} |
As long as we make a DELETE
request to /reps/ID-of-the-rep
, it'll delete it and return a blank response. Happy birthday!
Back in index.html.twig
, inside of our listener function, how can we figure out the DELETE URL for this row? Or, even more basic, what's the ID of this specific RepLog? I have no idea! Yay!
We know that this link is being clicked, but it doesn't give us any information about the RepLog itself, like its ID or delete URL.
This is a really common problem, and the solution is to somehow attach extra metadata to our DOM about the RepLog, so we can read it in JavaScript. And guess what! There's an official, standard, proper way to do this! It's via a data attribute. Yep, according to those silly "rules" of the web, you're not really supposed to invent new attributes for your elements. Well, unless the attribute starts with data-
, followed by lowercase letters. That's totally allowed!
Go Deeper!
You can actually read the "data attributes" spec here: http://bit.ly/dry-spec-about-data-attributes
So, add an attribute called data-url
and set it equal to the DELETE URL for this RepLog. The Symfony way of generating this is with path()
, the name of the route - rep_log_delete
- and the id: repLog.id
:
... lines 1 - 2 | |
{% block body %} | |
<div class="row"> | |
<div class="col-md-7"> | |
... lines 6 - 12 | |
<table class="table table-striped js-rep-log-table"> | |
... lines 14 - 22 | |
{% for repLog in repLogs %} | |
<tr> | |
... lines 25 - 27 | |
<td> | |
<a href="#" | |
class="js-delete-rep-log" | |
data-url="{{ path('rep_log_delete', {id: repLog.id}) }}" | |
> | |
<span class="fa fa-trash"></span> | |
</a> | |
</td> | |
</tr> | |
... lines 37 - 40 | |
{% endfor %} | |
... lines 42 - 50 | |
</table> | |
... lines 52 - 53 | |
</div> | |
... lines 55 - 61 | |
</div> | |
{% endblock %} | |
... lines 64 - 98 |
Sweet! To read that in JavaScript, simply say var deleteUrl = $(this)
, which we know is the link, .data('url')
:
... lines 1 - 64 | |
{% block javascripts %} | |
... lines 66 - 67 | |
<script> | |
$(document).ready(function() { | |
... lines 70 - 71 | |
$table.find('.js-delete-rep-log').on('click', function (e) { | |
e.preventDefault(); | |
$(this).addClass('text-danger'); | |
$(this).find('.fa') | |
.removeClass('fa-trash') | |
.addClass('fa-spinner') | |
.addClass('fa-spin'); | |
var deleteUrl = $(this).data('url'); | |
... lines 82 - 89 | |
}); | |
... lines 91 - 94 | |
}); | |
</script> | |
{% endblock %} |
That's a little bit of jQuery magic: .data()
is a shortcut to read a data attribute.
Tip
.data()
is a wrapper around core JS functionality: the data-*
attributes are also
accessible directly on the DOM Element Object:
var deleteUrl = $(this)[0].dataset.url;
Finally, the AJAX call is really simple! I'll use $.ajax
, set url
to deleteUrl
, method
to DELETE
, and ice_cream
to yes please!
. I mean, success
, set to a function:
... lines 1 - 64 | |
{% block javascripts %} | |
... lines 66 - 67 | |
<script> | |
$(document).ready(function() { | |
... lines 70 - 71 | |
$table.find('.js-delete-rep-log').on('click', function (e) { | |
... lines 73 - 80 | |
var deleteUrl = $(this).data('url'); | |
... line 82 | |
$.ajax({ | |
url: deleteUrl, | |
method: 'DELETE', | |
success: function() { | |
... line 87 | |
} | |
}); | |
}); | |
... lines 91 - 94 | |
}); | |
</script> | |
{% endblock %} |
Hmm, so after this finishes, we probably want the entire row to disappear. Above the AJAX call, find the row with $row = $(this).closest('tr')
:
... lines 1 - 64 | |
{% block javascripts %} | |
... lines 66 - 67 | |
<script> | |
$(document).ready(function() { | |
... lines 70 - 71 | |
$table.find('.js-delete-rep-log').on('click', function (e) { | |
... lines 73 - 80 | |
var deleteUrl = $(this).data('url'); | |
var $row = $(this).closest('tr'); | |
$.ajax({ | |
url: deleteUrl, | |
method: 'DELETE', | |
success: function() { | |
... line 87 | |
} | |
}); | |
}); | |
... lines 91 - 94 | |
}); | |
</script> | |
{% endblock %} |
In other words, start with the link, and go up the DOM tree until you find a tr
element. Oh, and reminder, this is $row
because this is a jQuery object! Inside success
, say $row.fadeOut()
for just a little bit of fancy:
... lines 1 - 64 | |
{% block javascripts %} | |
... lines 66 - 67 | |
<script> | |
$(document).ready(function() { | |
... lines 70 - 71 | |
$table.find('.js-delete-rep-log').on('click', function (e) { | |
... lines 73 - 80 | |
var deleteUrl = $(this).data('url'); | |
var $row = $(this).closest('tr'); | |
$.ajax({ | |
url: deleteUrl, | |
method: 'DELETE', | |
success: function() { | |
$row.fadeOut(); | |
} | |
}); | |
}); | |
... lines 91 - 94 | |
}); | |
</script> | |
{% endblock %} |
Ok, try that out! Refresh, delete my coffee cup and life is good. And if I refresh, it's truly gone. Oh, but dang, if I delete my cup of coffee record, the total weight at the bottom does not change. I need to refresh the page to do that. LAME! I'll re-add my coffee cup. Now, let's fix that!
If we somehow knew what the weight was for this specific row, we could read the total weight and just subtract it when it's deleted. So how can we figure out the weight for this row? Well, we could just read the HTML of the third column... but that's kinda shady. Instead, why not use another data-
attribute?
On the <tr>
element, add a data-weight
attribute set to repLog.totalWeightLifted
:
... lines 1 - 2 | |
{% block body %} | |
<div class="row"> | |
<div class="col-md-7"> | |
... lines 6 - 12 | |
<table class="table table-striped js-rep-log-table"> | |
... lines 14 - 22 | |
{% for repLog in repLogs %} | |
<tr data-weight="{{ repLog.totalWeightLifted }}"> | |
... lines 25 - 35 | |
</tr> | |
... lines 37 - 40 | |
{% endfor %} | |
... lines 42 - 50 | |
</table> | |
... lines 52 - 53 | |
</div> | |
... lines 55 - 61 | |
</div> | |
{% endblock %} | |
... lines 64 - 101 |
Also, so that we know which th
to update, add a class: js-total-weight
:
... lines 1 - 2 | |
{% block body %} | |
<div class="row"> | |
<div class="col-md-7"> | |
... lines 6 - 12 | |
<table class="table table-striped js-rep-log-table"> | |
... lines 14 - 42 | |
<tfoot> | |
<tr> | |
... lines 45 - 46 | |
<th class="js-total-weight">{{ totalWeight }}</th> | |
... line 48 | |
</tr> | |
</tfoot> | |
</table> | |
... lines 52 - 53 | |
</div> | |
... lines 55 - 61 | |
</div> | |
{% endblock %} | |
... lines 64 - 101 |
Let's hook this up! Before the AJAX call - that's important, we'll find out why soon - find the total weight container by saying $table.find('.js-total-weight')
:
... lines 1 - 64 | |
{% block javascripts %} | |
... lines 66 - 67 | |
<script> | |
$(document).ready(function() { | |
... lines 70 - 71 | |
$table.find('.js-delete-rep-log').on('click', function (e) { | |
... lines 73 - 81 | |
var $row = $(this).closest('tr'); | |
var $totalWeightContainer = $table.find('.js-total-weight'); | |
... lines 84 - 92 | |
}); | |
... lines 94 - 97 | |
}); | |
</script> | |
{% endblock %} |
Next add var newWeight
set to $totalWeightContainer.html() -
$row.data('weight'):
... lines 1 - 64 | |
{% block javascripts %} | |
... lines 66 - 67 | |
<script> | |
$(document).ready(function() { | |
... lines 70 - 71 | |
$table.find('.js-delete-rep-log').on('click', function (e) { | |
... lines 73 - 81 | |
var $row = $(this).closest('tr'); | |
var $totalWeightContainer = $table.find('.js-total-weight'); | |
var newWeight = $totalWeightContainer.html() - $row.data('weight'); | |
... lines 85 - 92 | |
}); | |
... lines 94 - 97 | |
}); | |
</script> | |
{% endblock %} |
Use that inside success
: $totalWeightContainer.html(newWeight)
:
... lines 1 - 64 | |
{% block javascripts %} | |
... lines 66 - 67 | |
<script> | |
$(document).ready(function() { | |
... lines 70 - 71 | |
$table.find('.js-delete-rep-log').on('click', function (e) { | |
... lines 73 - 81 | |
var $row = $(this).closest('tr'); | |
var $totalWeightContainer = $table.find('.js-total-weight'); | |
var newWeight = $totalWeightContainer.html() - $row.data('weight'); | |
$.ajax({ | |
... lines 86 - 87 | |
success: function() { | |
$row.fadeOut(); | |
$totalWeightContainer.html(newWeight); | |
} | |
}); | |
}); | |
... lines 94 - 97 | |
}); | |
</script> | |
{% endblock %} |
Let's give this fanciness a try. Go back refresh. 459? Hit delete, it's gone. 454.
Now, how about we get into trouble with some JavaScript objects!
Hey Alessandro,
This sounds like you have a different problem not related to the function you're going to create. Please, double check your syntax, usually PhpStorm gives you hints about syntax errors and show the affected line. If you still need help - please, share the exact error, otherwise it's difficult to guess what's the problem exactly :)
Cheers!
Here is your problem
// base,html.twig
$.ajax({
url: 'deleteUrl', # Here is the problem. You need to pass the real URL to your AJAX request
...
So glad about this excellent class!
I'd like to know how to solve this problem when clicking on the item to be deleted.
"DELETE http://127.0.0.1:8000/deleteUrl 404 (Not Found)"
Hey Yurniel L.
How did you generate the delete URL? You are hitting /deleteUrl
instead of /reps/{ID-of-the-rep}
Cheers!
Hi Ryan,
I have a little question about Symfony ;)
In delete method you have a method denyAccessUnlessGranted, I know this is a new method since sf 2.8 or 2.7 but I didn't know that you can pass 'IS_AUTHENTICATED_REMEMBER'.
Could you explain me a little this?
Thanks
Cheers
Greg
Hey Greg!
Sure thing :). So, denyAccessUnlessGranted - and really, any way that you check security, including is_granted
in Twig and even the roles
you list for each access_control
in security.yml - they all, ultimately call the isGranted()
method on the security.authorization_checker
service (this service was called security.context
before 2.8). In other words, whatever you can pass to denyAccessUnlessGranted
is the same thing you can pass in any of these other situations.
Now, at first, the security system really only supports passing one type of thing to these functions: roles (e.g. anything starting with ROLE_
). I'm guessing that makes good sense to you :). But, the security system also supports passing three other special things: IS_AUTHENTICATED_ANONYMOUSLY
, IS_AUTHENTICATED_REMEMBERED
and IS_AUTHENTICATED_FULLY
. These strings correspond to the level of authentication of the user - e.g. "are they logged in?" or "are they logged in via a remember me cookie only?". We actually explain this best in an older Symfony 2 tutorial: https://knpuniversity.com/screencast/symfony2-ep2/twig-security-is-authenticated.
So, that's the basic answer: all security functions ultimately call the same core function, and that function supports ROLE_ stuff and these 3 ISAUTHENTICATED items as well. But, to go deeper, the real answer is voters :). https://knpuniversity.com/screencast/new-in-symfony3/voter. Symfony's core comes with 2 (this is not 100% true, but just pretend it is) voters: one that handles ROLE_ items and one that handles ISAUTHENTICATED. So actually, you can pass ANYTHING you want to denyAccessUnlessGranted
(or the other functions), as long as you create a new voter that can handle that string.
That was probably more info than you wanted - I hope some of it was what you were looking for :).
Cheers!
Hello Ryan,
so I assume that in this case I have to write a special Voter so that nobody else than the creator himself can delete his own RepRow post?
Otherwhise, even with IS_AUTHENTICATED_REMEMBERED everybody else, who is logged in can delete his post?Am I right?
Thank you :)
Hey Chris!
That's correct, you need to write your own Voter that checks if the user is the owner of that post. Checking for "IS_AUTHENTICATED_***" only tells you if the user is logged in via a form, cookie or anonymous
Have a nice day :)
"Before the AJAX call - that's important, we'll find out why soon - find the total weight container by saying $table.find('.js-total-weight')"
Sorry but I haven't found a reason behind this. Shame on me :)
Yo Ivan!
Hahaha - actually, this is our fault! When I originally wrote the code for this line, I was using this
. And so, putting it before the AJAX callback was important - since this
is replaced. But, with the final code, we're not using this
, so it is actually not important where this code lives. My mistake for not catching that! Thanks for bringing it up!
Cheers!
// composer.json
{
"require": {
"php": "^7.2.0",
"symfony/symfony": "3.1.*", // v3.1.10
"twig/twig": "2.10.*", // v2.10.0
"doctrine/orm": "^2.5", // v2.7.1
"doctrine/doctrine-bundle": "^1.6", // 1.10.3
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.2
"symfony/swiftmailer-bundle": "^2.3", // v2.4.0
"symfony/monolog-bundle": "^2.8", // 2.12.0
"symfony/polyfill-apcu": "^1.0", // v1.2.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"friendsofsymfony/user-bundle": "~2.0@dev", // dev-master
"doctrine/doctrine-fixtures-bundle": "~2.3", // v2.4.1
"doctrine/doctrine-migrations-bundle": "^1.2", // v1.2.1
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"friendsofsymfony/jsrouting-bundle": "^1.6" // 1.6.0
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.1
"symfony/phpunit-bridge": "^3.0" // v3.1.6
}
}
Hi, i'm trying to make the function in controller but symfony doesn't find the right class. How can i resolve?