3

I have a simple <firebase-query> tag, and I'd like to manipulate some of the data before having it displayed through a <dom-repeat>. For example, I need to turn some fields into links, and also parse some dates.

So, I need to get the data once it's ready, loop through each item, and change some of the values.

To do that, I have an observer on the data to detect when it's ready. However, I can't figure out how to loop through the data from that JavaScript function. For some reason, for(var i in items) doesn't work, although the items do exist.

Here is the component:

<dom-module id="cool-stuff">

  <template>

    <firebase-query id="query" path="/items" data="{{items}}"></firebase-query>

    <template is="dom-repeat" items="{{items}}" as="item">
      [[item.name]]<br />
      [[item.date]]<br />
    </template>

  </template>

  <script>
    Polymer({
      is: 'ix-table',
      properties: {
        items: {type: Object, observer: "_itemsChanged"},
      }
      itemsChanged: function(data) {
        // how do I loop through the data received from firebase-query?
        console.log(data);
      }
    });
  </script>

</dom-module>

Ideally, all I'd want to do in the observer function is something like:

for(var i in data) {
  obj = data[i];
  obj.name = '<a href="/item/"+obj.key>'+ojb.name+'</a>';
}

But I can't seem to be able to loop through the data.

Inside the observer function, console.log(data) returns some weird stuff like this:

Array[o]
0: Object (which contains a proper item)
1: Object (same)
2: Object (same)

Update:

Here is a screenshot of what console.log(data) returns (from inside the observer):

enter image description here

The array seems to be populated with all the objects, but it shows as Array[0]. So it won't let me loop through them.

Update 2:

Thanks to arfost here is the solution:

<script>
  Polymer({
    is: 'ix-table',
    properties: {
      items: {type: Object},

    }
    observers: [
      '_itemsChanged(items.splices)'
    ],
    _itemsChanged: function(changeRecord) {
      if (changeRecord) {
        changeRecord.indexSplices.forEach(function(s) {
          for (var i=0; i<s.addedCount; i++) {
            var index = s.index + i;
            var item = s.object[index];
            console.log('Item ' + item.name + ' added at index ' + index);
            // do whatever needed with the item here:
            this.items[index].name = "New name";
          }
        }, this);
      }
    },
  });
</script>
Hubert
  • 369
  • 3
  • 21
  • Nice but I think you have made a mistake in calculating index, why are you adding 1 to it? the index 0 is matched with the object 0 also for each already loop through why would you have another for loop. – TheBen Apr 07 '17 at 21:06
  • To be honest I don't really get the intracies of this splice business... all I know is that this piece of code works and enables me to get the items returned by Firebase. – Hubert Apr 09 '17 at 17:38
  • I think you won't need splice if your path ends with a node that has branched nodes with nested children but when your path ends with immediate children splice is needed. Take it with a pinch of salt though – TheBen Apr 10 '17 at 01:56
  • I don't know if this is an elegant solution, but you sir, have solved my problem. Thanks! – luckyging3r Jun 21 '17 at 20:00

3 Answers3

4

<firebase-query> results

Note that <firebase-query> results in an array of objects. Let's say your database contained the following items under /notes/<USER_ID>/:

enter image description here

Your <firebase-query> would look similar to this:

<firebase-query
    id="query"
    app-name="notes"
    path="/notes/[[user.uid]]"
    data="{{notes}}">
</firebase-query>

(where user is bound to <firebase-auth>.user).

Assuming the user is logged in, <firebase-query> would then populate its data property (i.e., bound to notes) with the following array:

enter image description here

Note how each object contains a $key property, which corresponds to the item's key seen in the Firebase console's Database view.

You could then iterate notes directly with <dom-repeat>:

<template is="dom-repeat" items="[[notes]]">
  <li>
    <div>key: [[item.$key]]</div>
    <div>body: [[item.body]]</div>
    <div>title: [[item.title]]</div>
  </li>
</template>

Binding to HTML strings

You should be aware that the string data bindings are rendered literally in this case, so attempting to set name to obj.name = '<a href="...">' would render the literal string instead of an anchor. Instead, you should declare the tags in your template, and bind the key and name properties inside those tags. So, your observer could be replaced with this:

<template is="dom-repeat" items="{{items}}" as="item">
  <a href$="/item/[[item.key]]">[[item.name]]</a><br />
  [[item.date]]<br />
</template>

Iterating an array

The following note is only relevant if you prefer to mutate the data before displaying it...

When iterating an array, you should avoid for..in because it doesn't guarantee order of iteration, and because it may iterate over enumerable properties you might not necessarily care about. Instead, you could use for..of (assuming ES6 is available to your app):

for (let note of notes) {
  note.title += ' ...';
}

or Array.prototype.forEach():

notes.forEach(function(note) {
  note.title += ' ...';
});
tony19
  • 125,647
  • 18
  • 229
  • 307
  • Thanks a lot, that's very informative and much appreciated. In my case I did need to mutate the data before displaying, and for some reason I'm still not able to loop through the data from the observer function. I've added a screenshot of what console.log(data) shows from the observer in my original post. – Hubert Jan 03 '17 at 21:54
2

I thinks I have run into the same issue as you.

It come from the way firebase query is getting the array, the way polymer obersvers works, and is hidden by the fact that the javascript console is reference based when it show the objects.

In fact what really happen here, is that firebase query is creating an empty array, which trigger your polymer observer.

So your function is called as soon as the array is created, but still empty and you can't iterate through, since it's empty. You then log it, where the primitives sub-properties are correctly displayed (array[0])

Then firebase begin to populate the array with the datas. The arrays reference stay the same, so polymer won't fire the observer again, and in the console, when it try to display the array it display the array referenced in the log, which now contains the datas.

I recommend that you use a array mutation observer in place of your simple one as follow

`properties: {
    items: {type: Object},
  },
,
observers: [
       '_itemsChanged(items.splices)'
],`

It will fire every time an object is added to your array, and you would be able to do the work you need :)

I had the link for the documentation on array mutation observer :) polymer array mutation observer

I hope this will solve your issue, have a good day.

Arfost
  • 506
  • 1
  • 4
  • 16
  • Ah, awesome, that was exactly the solution! Many thanks for pointing to the right place. I've updated the first post with the solution. – Hubert Jan 04 '17 at 20:15
  • I run into this too. I don't want to act on every 'splice' but take the whole dataset at once, when it is finished loading all items. How can I do this? – Riël Apr 07 '17 at 10:58
  • With the real time capabilities of firebase, you never really have the "whole" dataset since there can be a new node push in it at every moment from others clients. If you want to treat it as a whole and not a list of X independant elements, firebase-document is better suited for that I think – Arfost Apr 07 '17 at 11:39
  • Yes, if you don't need to loop through each item (which in my case I did) then it's much easier, and firebase-document will be more appropriate – Hubert Apr 09 '17 at 17:39
0

i don't think i can think of a scenario where you'd need to mutate the data by looping through the array rather than just using computed bindings. like this:

<template is="dom-repeat" items="{{items}}" as="item">
  <child-el date="{{_computeDate(item.date)}}"></child-el><br />
  <child-el attr1="{{_someOtherConversion(item.prop1)}}"></child-el><br />
  <child-el attr2="{{_iPromiseAnyConversionCanBeDoneLikeThis(item.prop2)}}"></child-el><br />
</template>
<script>
  _computeDate: function(item) {
    //do date converstion
}
Ryan Tyler
  • 117
  • 1
  • 4