Buy

Watch... if I refresh... there's a slight delay before the table loads. That's because... we designed it that way! Open assets/js/Components/RepLogApp.js. In the constructor, we call this.loadRepLogs(), which... surprise! Loads the initial rep logs... by making an AJAX call:

217 lines assets/js/Components/RepLogApp.js
... lines 1 - 9
class RepLogApp {
constructor($wrapper) {
... lines 12 - 16
this.loadRepLogs();
... lines 18 - 33
}
... lines 35 - 44
loadRepLogs() {
$.ajax({
url: Routing.generate('rep_log_list'),
}).then(data => {
for (let repLog of data.items) {
this._addRow(repLog);
}
})
}
... lines 54 - 197
}
... lines 199 - 217

This builds the table.

That's cool! But... I'm going to be unreasonable and make our life complicated. I demand that this table load instantly! To make crazy me happy, let's update our app so that when the page loads, our JavaScript already knows what the initial rep logs are... without needing that AJAX call.

Well... if we still wrote JavaScript in our template... this would be pretty easy. We could create a JavaScript variable and use Twig to print out all the rep logs into that variable. Yep, mixing Twig and JavaScript is kinda handy.

But obviously... we can't start writing Twig code right in the middle of RepLogApp.js. Sigh, nope, we need a new way to communicate from our server to JavaScript. And there is a great... and simple solution.

Refactoring initialRepLogs to an Argument

First, remove loadRepLogs():

217 lines assets/js/Components/RepLogApp.js
... lines 1 - 9
class RepLogApp {
... lines 11 - 44
loadRepLogs() {
$.ajax({
url: Routing.generate('rep_log_list'),
}).then(data => {
for (let repLog of data.items) {
this._addRow(repLog);
}
})
}
... lines 54 - 197
}
... lines 199 - 217

Instead, add initialRepLogs as a second argument to the constructor:

209 lines assets/js/Components/RepLogApp.js
... lines 1 - 9
class RepLogApp {
constructor($wrapper, initialRepLogs) {
... lines 12 - 35
}
... lines 37 - 189
}
... lines 191 - 209

Whoever calls me will need to pass this in.

Down below, loop over these: for (let repLog of initialRepLogs), then, this._addRow(repLog):

209 lines assets/js/Components/RepLogApp.js
... lines 1 - 9
class RepLogApp {
constructor($wrapper, initialRepLogs) {
... lines 12 - 16
for (let repLog of initialRepLogs) {
this._addRow(repLog);
}
... lines 20 - 35
}
... lines 37 - 189
}
... lines 191 - 209

Ok! Who creates this object? Ah yes, it's our entry file: assets/js/rep_log.js:

13 lines assets/js/rep_log.js
... lines 1 - 8
$(document).ready(function() {
... line 10
var repLogApp = new RepLogApp($wrapper);
});

Dang it! We can't put Twig code here either. For now, just to see if this is working, I'll paste two hard-coded logs above this:

30 lines assets/js/rep_log.js
... lines 1 - 8
const logs = [
{
"links": {"_self": "\/reps\/78"},
"id": 78,
"reps": 1,
"itemLabel": "Big Fat Cat",
"totalWeightLifted": 18
},
{
"links": {"_self": "\/reps\/79"},
"id": 79,
"reps": 2,
"itemLabel": "Big Fat Cat",
"totalWeightLifted": 36
}
];
$(document).ready(function() {
... lines 27 - 28
});

This is the exact format returned by the AJAX endpoint. Pass logs as the second argument:

30 lines assets/js/rep_log.js
... lines 1 - 8
const logs = [
{
"links": {"_self": "\/reps\/78"},
"id": 78,
"reps": 1,
"itemLabel": "Big Fat Cat",
"totalWeightLifted": 18
},
{
"links": {"_self": "\/reps\/79"},
"id": 79,
"reps": 2,
"itemLabel": "Big Fat Cat",
"totalWeightLifted": 36
}
];
$(document).ready(function() {
... line 27
var repLogApp = new RepLogApp($wrapper, logs);
});

In theory, this should work. Side note: you should always worry when a programmer says:

In theory, this should work!

But actually, it does this time! Sometimes we get lucky. The table starts with the two hard-coded logs. Ok, we are close!

Rendering initialLogs as a data- Attribute

Open up the server code: src/AppBundle/Controller/RepLogController.php. This is the code for the AJAX endpoint that we were using before:

98 lines src/AppBundle/Controller/RepLogController.php
... lines 1 - 13
class RepLogController extends BaseController
{
/**
* @Route("/reps", name="rep_log_list", options={"expose" = true})
* @Method("GET")
*/
public function getRepLogsAction()
{
$models = $this->findAllUsersRepLogModels();
return $this->createApiResponse([
'items' => $models
]);
}
... lines 28 - 96
}

Open LiftController, indexAction(): this is the method that renders the current page:

71 lines src/AppBundle/Controller/LiftController.php
... lines 1 - 10
class LiftController extends BaseController
{
/**
* @Route("/lift", name="lift")
*/
public function indexAction(Request $request)
{
... lines 18 - 35
return $this->render('lift/index.html.twig', array(
... lines 37 - 38
));
}
... lines 41 - 69
}

Here's the plan: I want to get all the of the initial rep logs as JSON, pass that into the template, then render it in a way that our JavaScript can read.

Let's steal the first line of code from the AJAX controller. Paste it in indexAction(), but rename the variable to $repLogModels:

76 lines src/AppBundle/Controller/LiftController.php
... lines 1 - 10
class LiftController extends BaseController
{
... lines 13 - 15
public function indexAction(Request $request)
{
... lines 18 - 35
$repLogModels = $this->findAllUsersRepLogModels();
... lines 37 - 39
return $this->render('lift/index.html.twig', array(
... lines 41 - 43
));
}
... lines 46 - 74
}

Then, create another variable called $repLogsJson set to $this->get('serializer')->serialize($repLogModels, 'json'):

76 lines src/AppBundle/Controller/LiftController.php
... lines 1 - 10
class LiftController extends BaseController
{
... lines 13 - 15
public function indexAction(Request $request)
{
... lines 18 - 35
$repLogModels = $this->findAllUsersRepLogModels();
$repLogsJson = $this->get('serializer')
->serialize($repLogModels, 'json');
return $this->render('lift/index.html.twig', array(
... lines 41 - 43
));
}
... lines 46 - 74
}

Cool! Pass that into the template: repLogsJson set to $repLogsJson:

76 lines src/AppBundle/Controller/LiftController.php
... lines 1 - 10
class LiftController extends BaseController
{
... lines 13 - 15
public function indexAction(Request $request)
{
... lines 18 - 35
$repLogModels = $this->findAllUsersRepLogModels();
$repLogsJson = $this->get('serializer')
->serialize($repLogModels, 'json');
return $this->render('lift/index.html.twig', array(
... lines 41 - 42
'repLogsJson' => $repLogsJson,
));
}
... lines 46 - 74
}

Get that template open: app/Resources/views/lift/index.html.twig. Ok, so how can we pass the repLogsJson variable to JavaScript?

My favorite way is by leveraging data attributes. If you look inside rep_log.js, we look for a .js-rep-log-table element:

30 lines assets/js/rep_log.js
... lines 1 - 25
$(document).ready(function() {
var $wrapper = $('.js-rep-log-table');
... line 28
});

This lives near the top of the template. On it, add a new attribute: data-rep-logs="{{ repLogsJson|e('html_attr') }}":

62 lines app/Resources/views/lift/index.html.twig
... lines 1 - 2
{% block body %}
<div class="row">
<div class="col-md-7 js-rep-log-table" data-rep-logs="{{ repLogsJson|e('html_attr') }}">
... lines 6 - 43
</div>
</div>
{% endblock %}
... lines 47 - 62

Brilliant! That prints the rep logs and escapes them so that they can live safely on an attribute.

Dude! Now our job is easy. In rep_log.js, delete the old logs variable. And instead, say $wrapper.data('rep-logs'):

13 lines assets/js/rep_log.js
... lines 1 - 8
$(document).ready(function() {
... line 10
var repLogApp = new RepLogApp($wrapper, $wrapper.data('rep-logs'));
});

That's it! Refresh and... yes! We instantly have a real table. My unreasonable demands have been met!

So, whenever you want to pass data from your server to JavaScript, a great option is to leverage data attributes.

Leave a comment!