Best practice for using JS with multiple blocks on same page?

Permalink
What's the best practice for using JS with multiple blocks on the same page?

Is it better to embed JS in view.php and use php variables or in view.js and global variables? Or variables with unique block IDs? What if block JS variable values change from block to block?

linuxoid
 
Gondwana replied on at Permalink Reply
Gondwana
I'm not sure I fully understand the question or your suggestions, but an approach that I've used is to declare most of the JS in an object definition, and then instantiate an instance of that object in each block that required it. Each such object was given a unique name based on the block's ID. This approach prevents the JS in one block from interfering with that in another.

One little gotcha is that block IDs ($bID) aren't necessarily unique, but this can be overcome.
linuxoid replied on at Permalink Reply
linuxoid
Gondwana, could you please point to an example?
JohntheFish replied on at Permalink Reply
JohntheFish
My starting point design is to use php to embed block-specific data as json in one or more html data attributes.

Then have a single .js file that finds all blocks of your type, loops through them (.each), extracts the json from the attribute(s) and does its stuff for each of the blocks.

The js can be a view.js, or another js loaded as an asset.

That way there is only one copy of the js across all blocks. It only gets loaded once. Loading can be left to the footer.

That is a staring point, but not always the best solution. Sometimes js is so tightly coupled to a block it is easier to put in the block view. Sometimes it is so trivial that its not worth a separate file so could be in the block view or embedded directly into the page footer.

For example, a block controller can 'addFooterItem' for a snippet of script rather than a js file. As long as it doesn't have <?php ?> within it, that snippet will be identical for all blocks and only get loaded once across all blocks.

Global js variables are rarely necessary. For some large applications, creating your own global js object can be useful. For example, I have a global JtF js object that I attach various shared resources to.
linuxoid replied on at Permalink Reply
linuxoid
"to use php to embed block-specific data as json in one or more html data attributes" but if that email attribute appears multiple times on a page due to multiple blocks, wouldn't all them be loaded but only the last one loaded will be used by all blocks?
JohntheFish replied on at Permalink Best Answer Reply
JohntheFish
No. Wrong.

A jQuery selector will always return a list of everything it matches. Its just that you are in the incorrect habit of assuming these lists have a length of 1 (hence the bug in your event handlers).

So for each block you can do:
<div class="my_block_marker_class" data-my_block_data="<?php echo h(json_encode($my_block_data_array_or_object))?>">
    <?php
       // content of block
   ?>
</div>


Then in a single js file or snippet of script in the footer:
// a list of your blocks
$('.my_block_marker_class').each(function(ix,el){
    // you now have 1 block
    var data = $.parseJSON($(this).attr('data-my_block_data')); 
    //do stuff with data for block.
});


There are many alternatives for extracting a data attribute.
If its simple data, sometimes it can be a plain int or string, so not needing json encoding.
linuxoid replied on at Permalink Reply
linuxoid
You have, say, 10 blocks with the same variable $my_block_data_array_or_object values of which are different in all these blocks. So the page loads with these 10 variables but the JS will read all of them and the values of the previous blocks will be overridden by the following blocks. Isn't this so?
Gondwana replied on at Permalink Reply
Gondwana
Don't use global variables, for the reason you gave. If the variable is within an object definition, and a differently-named instance of the object is instantiated for each block, there will be no overlaps.

As John points out, there are other ways too.
omars786 replied on at Permalink Reply
omars786
Thanks John, I found this post helpful :)
TheRealSean replied on at Permalink Reply
TheRealSean
It's may also be worth pointing out that if you are using the same JS in multiple blocks(and it does not require unique data for each block) that you can load the JS using the AssetLoader.

For blocks like the the image slider block. You then register/require your assets in the registerViewAssets method.
linuxoid replied on at Permalink Reply
linuxoid
but even if you separate and load the assets, all variables will be loaded as many times as the number of same blocks on a page, won't they?
JohntheFish replied on at Permalink Reply
JohntheFish
No. You can require an asset 10 times in c5, but it will only be loaded once. Hence no replication. Just like a block having a view.js, the view.js will only be loaded once, even if there are 10 blocks on the page.

Best practice when developing any script asset (or view.js) is to either place it within an anonymous closure, so it has no impact on the global namespace, or give it its own namespace - like jQuery has loads of functions all under the jQuery name, with an alias of $, so you get all of jQuery for only 2 global symbols (and one of those is optional).
linuxoid replied on at Permalink Reply
linuxoid
"Just like a block having a view.js, the view.js will only be loaded once, even if there are 10 blocks on the page." - so how in this case can I pass a unique $bUID to the view.js without a form submission?
JohntheFish replied on at Permalink Reply
JohntheFish
Your mind is stuck in a rut thinking of this the wrong way round.

Put a marker class on an element in your view.php. Attach some data attributes to that element with block specific details. The view.js can then select on that class and loop through that element in each instance of the block. With this approach you don't need a bID or ubID in the view.

- minimal code
- configured separately for each block instance by the data elements
- js can be compressed by c5
- js can be combined by c5
- js can be cached by proxies or browsers
- no debris in global namespace

This isn't always the best solution, but in the case of your blocks currently under review it is.
Gondwana replied on at Permalink Reply
Gondwana
It would be fabulous if there were a code snippet for this somewhere. Would be a good candidate for a brief tutorial, or a github c5 snippet repo entry (which doesn't exist yet).
linuxoid replied on at Permalink Reply
linuxoid
I had that problem with my maps block and if multiple maps were on the same page. I've just solved it with adding a $buID to each map <div> and each JS function for the block. I couldn't figure out another way with a single view.js file.

You can see that in the bock's view.php:
https://www.concrete5.org/marketplace/addons/free-yandex-maps...
linuxoid replied on at Permalink Reply
linuxoid
"The view.js can then select on that class and loop through that element in each instance of the block" - but how would the JS in view.js know which block to process if all of them have the same variables, same classes, same attributes, same items. Without buIDs, all blocks look the same. That's easily done in a form because on its submission you know which one got submitted and loop through its items. Without a form I can't imagine what will cause the JS to loop through a right block.
JohntheFish replied on at Permalink Reply
JohntheFish
The view.php for each block places values in data attributes attached to the block markup. The JavaScript looks in the data attributes.
<div class="my_block_marker" data-my_block_data_item="<?php echo h(json_encode($my_block_data_item));?> ">


// inside a ready handler
$('.my_block_marker').each(.....
    var my_block_data_item = $.parseJSON($(this).attr('data-my_block_data_item'));
    // now we have data specific to this particular block
....);


There are many other ways to get at data- in jQuery, most correct for purists is usually $(...).data(...); . That method can in some cases decode json automatically. Personally I hesitate on that because its behaviour has changed across jQuery versions.

For simple strings or numbers, you don't need the json. A simple h() can suffice.
linuxoid replied on at Permalink Reply
linuxoid
Ok, so something like this, for example:

view.php:
<div class="my-block-class" data-my_block_data_item="<?php echo h(json_encode($my_block_data_item));?> ">

view.js:
$.each($('.my-block-class[data-my_block_data_item]'), function(index, element){
    var my_block_data_item = $.parseJSON($(element).data('my_block_data_item'));
)};
JohntheFish replied on at Permalink Reply
JohntheFish
Yes, that's it.
If data is simple string or numeric, you can skip the json encode/decode if you want to.
linuxoid replied on at Permalink Reply
linuxoid
There's something wrong in the code, because this No1 works but No 2 doesn't:

controller.php:
$js_data = array(
    'name' => 'test',
}
$this->set('js_data', $this->app->make('helper/json')->encode($js_data, JSON_UNESCAPED_UNICODE));

view.php
<div id="2gis_map_<?php echo $bUID; ?>" class="2gis-map" data-buid="<?php echo $bUID; ?>" data-js_data="<?php echo $js_data; ?>" style="height: <?php echo $height; ?>; width: <?php echo $width; ?>;"></div>

view.js No 1:
$(document).ready(function(e) {
    $.each($('.2gis-map [data-buid'), function(index, element){
        alert($(element).data('buid'));
    });
});

view.js No 2:
$(document).ready(function(e) {
    $.each($('.2gis-map [data-js_data'), function(index, element){
        var js_data = $.parseJSON($(element).data('js_data'));
        alert(js_data.name);
    });
});
The No 1 alert shows the 'buid' number, while the No 2 alert doesn't even pop up.
linuxoid replied on at Permalink Reply
linuxoid
It throws an error:

SyntaxError: JSON.parse: unexpected character at line 1 column 2 of the JSON data

which points to the position just before the $:
var js_data = $.parseJSON($(element).data('js_data'));
            ^

What's wrong here?
linuxoid replied on at Permalink Reply
linuxoid
This works:
var js_data = $.parseJSON(JSON.stringify($(element).data('js_data')));

JSON.stringify!!!
JohntheFish replied on at Permalink Reply
JohntheFish
.data(...) may already be parsing json, so you end up double-parsing it.

I tend to use .attr(....) because it doesn't do any pre-processing, you know exactly what you are getting.

Also, wrap the echo in an h() or quotes within the json will really screw it up.
linuxoid replied on at Permalink Reply
linuxoid
no, .attr() doesn't work at all

PS. Yes, I have the h() but forgot to paste it here:
echo '<div id="2gis_map_' . $bUID . '" class="2gis-map" data-buid="' . $bUID . '" data-js_data="' . h($js_data) . '" style="height: ' . $height . '; width: ' . $width . ';"></div>';
JohntheFish replied on at Permalink Reply
JohntheFish
with .attr, you need the full attribute name, including the data- prefix.

Have you re-read the jquery docs on .data() and .attr() since beginning this adventure?
linuxoid replied on at Permalink Reply
linuxoid
oh, yes, of course. now it works! ))) thank you very much.

I read about the data, not the attr