Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Array, Set and ES2016

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

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():

... 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():

... 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):

... 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]):

... 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 []:

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

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

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

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

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

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

12
Login or Register to join the conversation

I like this course but, why some videos do not have subtitles? ,thx

Reply

Hey Sergio,

Good question! This Spanish subtitles feature appeared recently on SymfonyCasts, and we decided to do not translate all previous tutorials because it might be time consumed for us, but we're going to translate all our future tutorials. That's why you can miss Spanish subtitles in our past tutorials. Though some popular old courses were translated into Spanish.

What exactly tutorial do you want to have Spanish subtitles? Please, let us know and we will try to consider this request in the future.

Btw, you will be able read more about our Spanish subtitles initiative here soon, we're working on writing a new blog post about it: https://symfonycasts.com/blog

Cheers!

Reply
Thao L. Avatar
Thao L. Avatar Thao L. | posted 5 years ago

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.

Reply

Hey Thao L.
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!

Reply
Jeroen V. Avatar
Jeroen V. Avatar Jeroen V. | posted 5 years ago

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?

Reply

Hey Jeroen V.!

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/knpuniversity/javascript/blob/master/_tuts/array-fix-removing-replogs.diff

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!

Reply
Thao L. Avatar

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.

Reply

Hey Thao L.,

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

Cheers!

Reply

Hey Ryan, I just went through this tutorial, and about the fix note, I don't think it works. Let's say we have 3 items: they would have "data-key"s 0, 1, 2. We then remove 1. Then we add another one. The keys should be now 0, 2, 2. What am I missing?

Reply
MolloKhan Avatar MolloKhan | SFCASTS | Beo | posted 4 years ago | edited

Hey Beo
How are you removing/adding those items? If you use the splice function you should not have that problem

Reply

Hey MolloKhan , look at this example... It's not a 1:1 with the solution, but it should be very similar: https://jsfiddle.net/ormpLb....

Reply

Ohh now I see the problem, and the fix is easy, instead of using the array's length for determine the row's key value, you should use its ID coming from the database

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses Symfony 3. But, since this is a JavaScript tutorial, all the concepts work fine in newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "symfony/symfony": "3.2.*", // v3.2.14
        "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.2
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.3.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.19
        "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.2
        "symfony/phpunit-bridge": "^3.0" // v3.2.2
    }
}
userVoice