Buy

The Map object is perfect for maps, or associative arrays as we call them in the PHP biz. But what about true, indexed arrays? Well actually, JavaScript has always had a great way to handle these - it's not new! It's the Array object.

Well, the Array object isn't new, but it does have a new trick. Let's check out an example: when the page loads, we call loadRepLogs():

235 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 6
class RepLogApp {
constructor($wrapper) {
... lines 9 - 11
this.loadRepLogs();
... lines 13 - 28
}
... lines 30 - 39
loadRepLogs() {
$.ajax({
url: Routing.generate('rep_log_list'),
}).then(data => {
for (let repLog of data.items) {
this._addRow(repLog);
}
})
}
... lines 49 - 170
_addRow(repLog) {
... lines 172 - 175
const html = rowTemplate(repLog);
this.$wrapper.find('tbody').append($.parseHTML(html));
this.updateTotalWeightLifted();
}
}
... lines 182 - 233
})(window, jQuery, Routing, swal);

This fetches an array of repLog data via AJAX and then calls _addRow() on each to add the <tr> elements to the table.

But once we add the table rows... we don't actually store those repLog objects anywhere. Yep, we use them to build the page... then say: Adios!

Now, I do want to start storing this data on my object, and you'll see why in a minute. Up in the constructor, create a repLogs property set to new Array():

239 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 6
class RepLogApp {
constructor($wrapper) {
... line 9
this.repLogs = new Array();
... lines 11 - 30
}
... lines 32 - 184
}
... lines 186 - 237
})(window, jQuery, Routing, swal);

If you've never seen that Array object before... there's a reason - stay tuned! Then, down in _addRow(), say this.repLogs() - which is the Array object - this.repLogs.push(repLog):

239 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 6
class RepLogApp {
... lines 8 - 173
_addRow(repLog) {
this.repLogs.push(repLog);
... lines 176 - 183
}
}
... lines 186 - 237
})(window, jQuery, Routing, swal);

Back up in loadRepLogs(), after the for loop, let's see how this looks: console.log(this.repLogs). Oh, and let's also use one of its helper methods: this.repLogs.includes(data.items[0]):

239 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 6
class RepLogApp {
... lines 8 - 41
loadRepLogs() {
$.ajax({
... line 44
}).then(data => {
... lines 46 - 48
console.log(this.repLogs, this.repLogs.includes(data.items[0]));
})
}
... lines 52 - 184
}
... lines 186 - 237
})(window, jQuery, Routing, swal);

Obviously, this item should have been added to the Array!

Refresh! Yea! We see the fancy Array and the word true. Awesome!

But hold on! The Array object may not be new, but the includes() function is new. In fact, it's really new - it wasn't added in ES2015, it was added in ES2016! ES2015 came with a ton of new features. And now, new ECMAScript releases happen yearly, but with many fewer new things. The Array' includes() function is one of those few things in ES2016. Cool!

Oh, and by the way, you don't typically say new Array()... and PHPStorm is yelling at us! In the wild, you just use []:

239 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 6
class RepLogApp {
constructor($wrapper) {
... line 9
this.repLogs = [];
... lines 11 - 30
}
... lines 32 - 218
}
... lines 220 - 237
})(window, jQuery, Routing, swal);

That's right, when you create an array in JavaScript, it's actually this Array object.

Calculating the Total Weight

But... why are we keeping track of the repLogs? Because now, we can more easily calculate the total weight. Before, we passed the Helper object the $wrapper element so that it could find all the tr elements and read the weight from them. We can simplify this! Instead, pass it our Array: this.repLogs:

238 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 6
class RepLogApp {
constructor($wrapper) {
... line 9
this.repLogs = [];
HelperInstances.set(this, new Helper(this.repLogs));
... lines 13 - 30
}
... lines 32 - 183
}
... lines 185 - 236
})(window, jQuery, Routing, swal);

At the bottom of this file, change the constructor() for Helper to have a repLogs argument. Set that on a repLogs property:

238 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 185
/**
* A "private" object
*/
class Helper {
constructor(repLogs) {
this.repLogs = repLogs;
}
... lines 193 - 217
}
... lines 219 - 236
})(window, jQuery, Routing, swal);

Below in calculateTotalWeight(), instead of using the $wrapper to find all the tr elements, just pass this.repLogs to the static function. Inside of that, update the argument to repLogs:

238 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 185
/**
* A "private" object
*/
class Helper {
... lines 190 - 193
calculateTotalWeight() {
return Helper._calculateWeights(
this.repLogs
);
}
... lines 199 - 209
static _calculateWeights(repLogs) {
... lines 211 - 216
}
}
... lines 219 - 236
})(window, jQuery, Routing, swal);

Previously, _calculateWeights() would loop over the $elements and read the data-weight attribute on each. Now, loop over repLog of repLogs. Inside, set totalWeight += repLog.totalWeightLifted:

238 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 185
/**
* A "private" object
*/
class Helper {
... lines 190 - 209
static _calculateWeights(repLogs) {
let totalWeight = 0;
for (let repLog of repLogs) {
totalWeight += repLog.totalWeightLifted;
}
return totalWeight;
}
}
... lines 219 - 236
})(window, jQuery, Routing, swal);

It's nice to calculate the total weight from our source data, rather than reading it from somewhere on the DOM.

Okay! Try that out! The table still loads... and the total still prints!

Tip

Actually, we made a mistake! When you delete a rep log, the total weight will no longer update! That's because we now need to remove the deleted repLog from the this.repLogs array.

No problem! The fix is kinda cool: it involves adding a reference to the $row element: the index on the this.repLogs array that the row corresponds to. This follows a pattern that's somewhat similar to what you'll see in ReactJS.

249 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing, swal) {
... lines 4 - 6
class RepLogApp {
... lines 8 - 73
_deleteRepLog($link) {
... lines 75 - 83
return $.ajax({
... lines 85 - 86
}).then(() => {
$row.fadeOut('normal', () => {
// we need to remove the repLog from this.repLogs
// the "key" is the index to this repLog on this.repLogs
this.repLogs.splice(
$row.data('key'),
1
);
$row.remove();
this.updateTotalWeightLifted();
});
})
}
... lines 102 - 180
_addRow(repLog) {
this.repLogs.push(repLog);
... lines 183 - 186
const html = rowTemplate(repLog);
const $row = $($.parseHTML(html));
// store the repLogs index
$row.data('key', this.repLogs.length - 1);
this.$wrapper.find('tbody').append($row);
this.updateTotalWeightLifted();
}
}
... lines 196 - 247
})(window, jQuery, Routing, swal);

Introducing Set

But, ES2015 added one more new object that's related to all of this: Set. It's a lot like Array: it holds items... but with one important difference.

Open up play.js and set foods to an array:

7 lines play.js
let foods = [];
... lines 2 - 7

Let's add gelato to the array and tortas. Clear everything else out:

7 lines play.js
let foods = [];
foods.push('gelato');
foods.push('tortas');
... lines 4 - 7

And ya know what? Gelato is so good, we should add it again. At the bottom, log foods:

7 lines play.js
let foods = [];
foods.push('gelato');
foods.push('tortas');
foods.push('gelato');
console.log(foods);

When you run the script, there are no surprises: gelato, tortas, gelato.

But now, change the array to be a new Set(). To add items to a Set, you'll use add() instead of push() - but it's the same idea:

7 lines play.js
let foods = new Set();
foods.add('gelato');
foods.add('tortas');
foods.add('gelato');
console.log(foods);

Try the script now.

Woh! Just two items! That's the key difference between Array and Set: Set should be used when you need a unique collection of items. It automatically makes sure that duplicates aren't added.

Oh, and there is also a WeakSet, which has the same super powers of WeakMap - all that garbage collection stuff. But, I haven't seen any decent use-case for it. Just use Set... or Array if values don't need to be unique.

Leave a comment!

  • 2017-03-31 Diego Aguiar

    Hey Thao Truong,

    you have a tiny typo in there, just change $row.date() by $row.data()

    Cheers!

  • 2017-03-31 Diego Aguiar

    Hey Thao Truong
    Could you open your console and in the network tab show me what happens when you delete a row ?
    Because that's not the case, fixtures aren't loaded every time, you might have an error in your delete action or the RepLogApiModel is not generating the URL's correctly

    Have a nice day!

  • 2017-03-31 Thao Truong

    BTW does this app run fixture reload every time I reload the page? Because it seems like the deleted rows come back after I reload the page.

  • 2017-03-31 Thao Truong

    Somehow $row.date('key', this.repLogs.length - 1); didn't work for my browser and I had to write $row.attr('data-key', this.repLogs.length - 1); instead. Weird.

  • 2017-03-22 weaverryan

    Hey Jeroen van den Nieuwenhuisen!

    Ah, you're 100% right! Nice catch! I've just pushed a code fix for this (you might see yourself pinged on an obscure GitHub commit) and we'll add a note and code block to go with it (and a note in the video). Obviously, there are a lot of ways to solve this, but here is the diff for the one I settled on: https://github.com/knpunive...

    It's inspired by a pattern followed in ReactJS: when you create your "view" that's attached to some item in an array, you often will add a key to that "view", which is the index of the item in the array (so that you can handle situations exactly like this - i.e. when I delete/update that "view", I know which data to remove/update).

    Thanks for asking / letting us know about this! Cheers!

  • 2017-03-22 Jeroen van den Nieuwenhuisen

    The repLog array breaks the total weigth lifted calculation, when an row is deleted. What is the best approach to remove an deleted row from the array? Should each table row keeps the array index in a data property?