Is there a way to set and preserve data attributes on elements even when re-rendered?


#1

This question has been posted on StackOverflow but hasn’t had much luck: http://stackoverflow.com/questions/29481403/is-there-a-way-to-preserve-data-attributes-on-elements-of-a-reactive-meteor-temp

In learning Meteor I’m trying to clone a small game (7x7, http://static.giantbomb.com/uploads/original/1/14761/2433052-7x7-screen03.png). The game is a grid of 7x7 squares, and a square has a background colour (modelled as a 7x7 array in the database with array[i][j] being the colour of row i/col j). I need to model this so that I can ‘move’ a square, by clicking on it then somewhere else - this would of course clear its background colour in its current space and change the background colour where it moves to. My first thought on how to do this was to set a data attribute on each square of the grid which I could then map back to the collection storing its state (in the database). All the detail is in that SO question, but the code for that was:

 currentSpaceId = 0;

 Template.gameSpace.rendered = function(){
  if(!this.rendered){
    this.$("td").data("spaceId", currentSpaceId);
    currentSpaceId++;
    this.rendered = true;
  }
};

Ie, set a data attribute at render time so the squares are numbered 0-48, which I can trivially convert to a location in the 7x7 state table.

This of course doesn’t work because Meteor re-renders stuff a lot (and more generally I’m not sure I can rely on it to run this callback on cells in a table precisely in order?).

Can anyone give me any pointers on how to either preserve the data attributes when Meteor re-renders things, or on how to re-formulate my approach here which is maybe just plain wrong?

Thanks!


#2

In general, I set dynamic data attribute and style values using template helpers so they are reactive.

<td data-myda="{{mydaVal}}" style="color:{{mystyleColor}}">{{mycellData}}</td>

The helper feeds the reactive data and keeps all the cells reflecting the real-time values and colors.

May not answer your question, but you have to think differently on when to use Jquery with Meteor and when to use reactive template helpers.

Don


#3

Hey Don,

Thanks for replying! That seems reasonable but I’m a bit unsure how to map it to what I’m doing. All I’m storing is the style colours, just because storing these IDs seemed pointless (and I like the 7x7 array in the DB). So I’m binding to {{this}} which is at that point just an entry in the array (I’m just using it to build up a class name for the style).

Would you suggest using a different data structure where I store these IDs in the database is the right approach here?

Obviously it’s not much data and doesn’t really matter, it just does feel like a bit of a messy solution to store those IDs (which will always be the same 0-48/1-49 or whatever) in the database for each game.


#4

I’m trying to follow this approach, but when I change my variable that sets the text on a button element it’s not updating in the html. My html is:

<div class="dropdown" id="mt-command-dd">
    <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">{{selectedCmdText}}
    </button>...

In my event code below, if I don’t have the button.text() call, the button text is not updated. I was thinking if I just updated the selectedCmdTxt variable Meteor would take care of updating the button text for me.

 'click #mt-command-dd .dropdown-menu li a': function (e, t) {
            var button = $('#mt-command-dd > button');
            var eventTarg = event.target;
            var eventText = eventTarg.text;
            selectedCmdText = eventText;
            button.text(selectedCmdText);
        },

#5

@netbrackets

What does your helper for {{selectedCommandText}} look like? For example:

Template.yourTemplate.helpers({
  selectedCmdText: function(){
    //what do you have here?
  }
})

In your code, you’re changing the text attribute using jQuery in non-reactive way. Template helpers are reactive. Changing attributes with jQuery in an event handler is by default not reactive.

To maintain the architecture you have, you could have the event update the value of a reactive var (using the reactive-var package) or even a Session variable (though you’ll get different opinions on this) and your helper could simply return the value of the reactive var or Session variable.


#6

selectedCmdText: function () {
return selectedCmdText;
},


#7

I think @nkrisc has identified the underlying issue in that you need to be using a reactive data construct and use a helper to return the value of that.

There is another issue in your code: you have referenced event.target in your code, but the event object you have declared is e: function(e, t)


#8

I have no idea what selectedCmdText references but unless it’s a reactive data source the helper will only display its initial value and will not rerun by default.


#9

Is a global variable not a reactive data construct? I do declare the variable, and I do have a helper that returns its value (shown above).

Regarding e vs event, That’s a good point, but when I step through the code, both e and event work and the selected text is correctly retrieved by the eventText = eventTarg.text line, which seems pretty strange. Perhaps meteor automatically declares a global ‘event’ variable when inside a Template.xxx.events construct?


#10

It’s a global variable declared above. I guess I’m unsure exactly what constitutes a reactive data source.


#11

The easiest way to introduce a reactive variable is use Session, but there are varied opinions regarding its suitability in some situations.

Your event handler (I used e - I prefer to rely on what’s definitely there :wink:):

'click #mt-command-dd .dropdown-menu li a': function (e, t) {
            var button = $('#mt-command-dd > button');
            var eventTarg = e.target;
            var eventText = eventTarg.text;
            Session.set('selectedCmdText', eventText);
        },

Your helper:

selectedCmdText: function () {
return Session.get('selectedCmdText');
},

#12

Thanks, that works, although it does seem a bit like we’re using a Session variable for its side effect (reactivity) rather than what Session variables are meant for.
One last related question, when I was trying to manipulate the DOM, I was successful in changing the text of the button with the button.text() function, but I never could figure out how to set the value of one of the buttons attributes. Is there a way to do that in the event handler?


#13

You can use jQuery’s .attr() method but you’re going to run into the same reactivity issue.

Also if you don’t want to use Session variable, take a look at reactive-var: http://docs.meteor.com/#/full/reactivevar_pkg


#14

Thanks, reactivevar works great for me. Here’s what I have ultimately ended up with after adding the reactive-var module to my project:

var selectedCmdTextRv = new ReactiveVar(COMMAND_DD_DEFAULT);

Template.mtCommandModal.helpers({
    selectedCmdText: function () {
        return selectedCmdTextRv.get();
    }, ...

Template.mtCommandModal.events({
    'click #mt-command-dd .dropdown-menu li a': function (e, t) {
        var button = $('#mt-command-dd > button');
        var eventTarg = e.target;
        selectedCmdTextRv.set(eventTarg.text);
},  ...

#15

Nice! That’s pretty much what I ended up doing when in a similar situation where I needed reactive DOM manipulation.


#16

Since this has been bumped back up, I figure I might as well mention my solution to the original problem in case it helps anyone.

Basically, instead of adding the data while creating the elements - or after they’ve rendered, I added the data I need before in the template helper.

  UI.registerHelper('addGridPositions',           
    function(grid){
    // grid is 7x7
    var out = new Array(7);
    for(var i=0; i<7; i++){
        out[i] = new Array(7);
    }

    for(var i=0; i<7; i++){
       for(var j=0; j<7; j++){
          out[i][j] = {row: i, column: j, value: grid[i][j]}
       }
    }

    console.log(out);
    return out;
});

Basically I convert each element of the grid to an object containing its row, column and the original element.

I can then access the rows with

{{#each addGridPositions state}}

And then the cells with

{{#each this}}

And then finally at that point I have access to {{row}}, {{column}} and {{value}}.

Not sure how optimal this really is but it does feel like at least the template helper bit is probably right.