If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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.
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); |
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:
let foods = []; | |
... lines 2 - 7 |
Let's add gelato
to the array and tortas
. Clear everything else out:
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
:
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:
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.
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!
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.
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!
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?
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!
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?
Hey Beo
How are you removing/adding those items? If you use the splice
function you should not have that problem
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....
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
// 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
}
}
I like this course but, why some videos do not have subtitles? ,thx