zberry zberry - 7 months ago 229
Javascript Question

Inform Polymer dom-repeat of changes to local DOM

I have a pretty basic custom element

tri-playlist
that I'm using to display a grid of another custom element
tri-clip
.

<dom-module id="tri-playlist">
<template>
<template is="dom-repeat" items={{clips}}>
<tri-clip clip-num={{item.clipNum}}></tri-clip>
</template>
</template>
</dom-module>


clips
is generated dynamically by fetching information from a server. I'm notified of any changes to this information (even my own), and consequently update
clips
, after which the template takes care of updating the view to reflect the new data (a clip was deleted, order was changed, etc). Using jQuery UI Sortable, I can re-arrange clips in a playlist or drag them to a new one, and on
sortstop
I inform the server of any changes so that it stays in sync. That all seems to work fine, separately.

However, after the DOM has been manipulated by my jQuery sort, the template doesn't update correctly. From what I can tell, this is because the template isn't aware of those changes to its (local?/light?) DOM.

So for example, let's say I have three clips [A][B][C] in a playlist, and I move the first one so that it's now last [B][C][A]. When the server informs me of the change (my own) and I update
clips
accordingly, the template notices the shift and performs it a second time. In this example, the final outcome is [C][A][B].

So far the only way I can get it to work is by setting
clips = [];
before setting it correctly. Which results in any
tri-clip
s on the screen to momentarily disappear, and forces Polymer to create the elements all over again, which defeats one of the advantages of using a Polymer template in the first place (TLDR: polymer data binding makes the smallest amount of changes necessary).

I've come across this:


When one of the template helper elements updates the DOM tree, it fires a dom-change event.

In most cases, you should interact with the created DOM by changing the model data, not by interacting directly with the created nodes. For those cases where you need to access the nodes directly, you can use the dom-change event.


but, I'm not really sure how to apply it to my situation. From what I understand, I would use the
dom-change
event if I wanted to do something with the nodes after the template triggered the changes. Or am I wrong about that? Is there anyway to inform the template that changes were made and it needs to update its model accordingly? Am I going about this the wrong way?

Answer

For whatever it's worth, the work-around I managed to come up with (which isn't ideal but fixes the issues I was running in to) is essentially to iterate over the dom-repeat and assess the changes that were made to its children (in my case by jQuery sortable). Then carry out operations that mirror those changes on dom-repeat's items array. I was using a jQuery multisortable plugin so I had to account for an undetermined amount of changes (where as normally jQuery sortable will only ever change one element at a time) but I figure the code below could easily be optimized for such situations. In the jQuery sortable stop event, I did the following:

1 ) Get the indices (index was a property of the items in my dom-repeat array) reflecting the positions of dom elements before sorting

// get indices of selected elements that presumably changed
var target = event.target,
    toRearr = [],
    firstSelected = null;

$( target ).children( '.selected' ).each( function() {
  toRearr.push(this.index);
  // keep reference of first selected for re-ordering start point
  if (firstSelected === null) firstSelected = this;
});

2) See where first selected element is after sorting (note that this is in relation to all of the children, whereas the previous loop was only selected elements)

// find the index of where we need to 'paste' the elements
var toPaste = -1;
$( target ).children( 'selector' ).each( function( index ) {
  if (this === firstSelected) {

    if (firstSelected.index > index) { // elements were moved to the left
      toPaste = index;
      return false; // breaks out of $.each loop
    }
    else if (firstSelected.index < index){ // elements were moved to the right
      toPaste = index + toRearr.length;
      return false; // breaks out of $.each loop
    }
    //else no elements were changed
 }
});

3) Make array mutations on dom-repeat's items array (or whatever you've named it) to reflect DOM changes

if (toPaste !== -1) { // meaning its children were indeed rearranged

  // inform Polymer of the change
  var tempPaste = toPaste;
  for (var i = toRearr.length - 1; i >= 0; i--) { // start from end to preserve order

    // we're moving an item to the right of our 'paste' index, so decrease
    // tempPaste by 1 so it stays consistent
    if (tempPaste > toRearr[i]) tempPaste--;

    // move element from old index to new index, using two array splices
    // that essentially remove and reinsert the element in the array
    target.splice('items', tempPaste, 0, target.splice('items', toRearr[i], 1)[0]);
  }
}

Hopefully that'll help anyone in a similar situation