A Great Place to Hide Things! The data- Attributes

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Time 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.

Adding a data-url Attribute

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

Reading data- Attributes

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!

Adding data-weight Metadata

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!

Leave a comment!

This tutorial uses an older version of Symfony... but since it's a JavaScript tutorial, the concepts are still 💯 valid!

What PHP libraries does this tutorial use?

// 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
        "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
    }
}