Polymer and Polymerfire: how to loop through the data from an observer? - firebase

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):
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>

<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>/:
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:
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 += ' ...';
});

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.

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
}

Related

Know when child templates have been rendered

<template name="FrameItems">
<div class="frame-items">
{{#each frames}}
{{> FrameItem}}
{{/each}}
</div>
</template>
In the above example, I want to know when all FrameItem templates inside FrameItems template have been rendered. I thought onRendered of the parent would be invoked when all the child templates have been rendered, but it was just called right away. What's the conventional way of making sure all the child templates are rendered?
One way to do it is to use a counter and increment it until it reaches a certain value.
Here the counter would in Session and incremented until it reaches the length of your Frames iterable thing:
Template.FrameItems.onRendered(function() {
Session.set('frameCounter', 0);
});
Template.FrameItem.onRendered(function() {
Session.set('frameCounter', Session.get('frameCounter') + 1);
});
Then you simply use a tracker:
//Where template is your template instance, for example 'this' in an onCreated callback
template.autorun(function doStuffWhenFramesRendered(computation) {
if(Session.get('frameCounter') === template.frames.length) {
doStuff();
//Stop observing
computation.stop();
}
});
Note that it takes into account the fact that FrameItem may render at weird times (avoiding race conditions if any), but it doesn't take into account new frames. To take those into account you would not stop the computation.
Here is how I would proceed:
You create a pageSession reactive variable or reactive dictionary entry. Let's call it lastRendered.
You update it in the onRendered function of your FrameItem template using the _id of the related frames item. This way, each time a FrameItem template is rendered, you now which one it is.
You create an helper in your parent template watching your lastRendered reactive variable and checking if it matches your last frames item. It could look like that (untested code):
lastFrameIsRendered: function() {
var lastId = frames.find().limit(1).sort({$natural:-1}).fetch()._id;
return pageSession.get ("lastRendered") === lastId;
},
Alternatively, if you need to get a feedback in your parent template onRendered function, you can wrap this code into a this.autorun(function() { (tracker) like this:
var lastId = frames.find().limit(1).sort({$natural:-1}).fetch()._id;
this.autorun(function() {
if (pageSession.get ("lastRendered") === lastId) {
//do your stuff
}
});
It will be executed each time there is a change in your parent template.

Nested Lists on same Page in MeteorJS

I have three lists on the same page which I want to fill with list-items. The list items are associated to the lists by a field called listId
My publications:
Meteor.publish('lists', function(options) {
return Lists.find({}, options);
});
Meteor.publish('listItems', function(listId) {
return Cards.find({listId: listId});
});
My lists-page.js (this._id param is passed with iron-router.):
Template.listsPage.helpers({
lists: function(){
return Lists.find({listsPageId: this._id});
},
listItems: function(listId){
//??
return ListItems.find({listId: listId})
}
});
My lists-page.html:
<template name="listsPage">
{{#each lists}}
<ul>
<li>{{title}}</li>
{{#each listItems}}
<li>{{listItemTitle}}</li>
{/each}
{{#each lists}}
</template>
Any help is much appreciated!
As a general recommendation, I would avoid collections and publish functions which have generic names like "lists" and "items". I find it makes code incredibly hard to understand. That being said, I think the confusion here is about the name associated with a publish function and the name of the underlying collection.
The name of the publish function (listItems in this case) is used only to facilitate the activation by a subscription, and has no other meaning. The function publishes a set of documents in the Cards collection to the client. Those documents should subsequently be retrieved from the client's local database using the Cards collection.
So I think the code you are looking for is:
listItems: function() {
return Cards.find({listId: this._id});
}
The context in which the listItems helper is run is that of a List document. So the id for listId should be this._id. Let me know if that doesn't work for some reason.

Strange UI behavior for {{#each}} on an array

Meteor newbie here. Working off of the Todos example, I am trying to use a separate Tags collection. It seems to be working except for a strange UI artifact: If I click a tag in the tag filter, and check off the last item in the todo list, the first item gets checked as well. The first item does not get updated to done, and clicking away from the tag filter and then back shows the first item unchecked as it should be. So I am not sure why that is happening.
The code for the todos is the same as in the Todos example
{{#each todos}}
{{> todo_item}}
{{/each}}
And the code for the tags collection filter
var todos = [];
if (!currentTaskId)
return {};
var tag_filter = Session.get('tag_filter');
if (tag_filter){
var tags = Tags.find({taskId: currentTaskId, name: tag_filter});
tags.forEach(function(tag){
var todo = Todos.findOne(tag.todoId);
todos.push(todo);
});
return todos; // this is an array rather than a collection and causes a strange artifact bug when checking bottom todo as done
}
What I have been able to gather is that if you do {{#each}} on an array you create a dependency on the entire scope rather than each individual item in the array, versus a collection cursor that automagically creates a dependency for each document in the collection. Has anybody run into this odd UI behavior? I'd also like to know if there is a way to either make the array into a cursor or at least act like one by registering a dependency for each item in the array?
Appreciate any insights, thank you.
I've revamped your code to return a cursor instead of an array, it may solve your problem but it's untested.
var tagFilter=Session.get("tag_filter");
if(!currentTaskId || !tagFilter){
return null;
}
// find tags and fetch them in an array
var tags=Tags.find({
taskId:currentTaskId,
name:tagFilter
}).fetch();
// build an array of Todos ids by extracting the todoId property from tags
// see underscore docs
var todosIds=_.pluck(tags,"todoId");
// return todos whose id is contained in the array
return Todos.find({
_id:{
$in:todosIds
}
});

Template is re-rendered even though there is no data change

I'm struggling with an issue that I will explain giving a simple demo.
There's following very simple document in People Collection.
{
"_id" : "vxmLRndHgNocZouJg",
"fname" : "John" ,
"nicks" : [ "Johnny" , "Jo"]
}
Now let's consider following templates. Basically I display username and a list of nicknames with input field for adding more nicknames.
<head>
<title>test</title>
</head>
<body>
{{> name}}<br/>
{{> nicks}}
</body>
<template name="name">
<input type="text" value="{{fname}}"/>
</template>
<template name="nicks">
{{#each nicks}}
<div>{{this}}</div>
{{else}}
no nicks yet
{{/each}}
<input type="text" name="nicks"/>
<input type="submit"/>
</template>
My client javascript code is as follows:
Template.name.fname = function() {
return People.findOne({"fname" : "John"},{
transform : function(doc) {
return doc.fname;
}
});
}
Template.name.rendered = function() {
console.log('Template "name" rendered!');
}
Template.nicks.nicks = function() {
var john = People.findOne({"fname" : "John"});
if(john) return john.nicks;
}
Template.nicks.events({
'click input[type="submit"]' : function () {
var johnId = People.findOne({"fname" : "John"})._id; // demo code
People.update(johnId,{
$addToSet : {
nicks : $('input[name="nicks"]').val()
}
})
}
});
My problem is that after adding nickname (update of nicks field in a document) template name is re-rendered (I know because I console.log it). When I query People collection to provide data for name template I use transform option so changes in nicks field shouldn't have impact on name template.
Meteor docs supports this:
Cursors are a reactive data source. The first time you retrieve a cursor's documents with fetch, map, or forEach inside a reactive computation (eg, a template or autorun), Meteor will register a dependency on the underlying data. Any change to the collection that changes the documents in a cursor will trigger a recomputation.
Why template name is re-rendered then?
The template is re-rendered because you change the People collection.
When you alter the People collection, Meteor automatically assumes that everything that it provides data to needs to be recalculated. (Which your name template does via Template.name.fname.
Even though you transform the output of the cursor, the People collection has changed in some way. The query is done before the transform is used, in other words, its not the transform that is looked at but the whole collection.
Meteor thinks that perhaps your document with {'fname':'John'} may have some other field that might have changed and it needs to requery it to check (which the nicks field has been altered). The transform is then applied after the requery.
Your HTML might not actually change at this point, only if the cursor returns something different will the html be changed.
If it becomes an issue in any scenario (i.e forms being cleared or DOM being changed when it shouldn't be) you can use the {{#isolate}} {{/isolate}} blocks to ensure that only everything inside them is re-rendered and nothing outside.

How to refer to Object of current iteration in Handlebars

Is there any way to get the object of the current iteration in Handlebars?
code:
<script id="HandleBarTemplate1" type="text/x-handlebars-template">
{{#each objArr}}
<img src="{{objField1}}"/>
<strong>Name:</strong> {{objField2}}
<input type="button" onclick="processObject({{.}});"/>
{{/each}}
</script>
I've mentioned processObject({{.}}) That is incorrect. That's where I need a replacement/Solution.Hope you get what I'm tryin' to say.
The contents of objArr might look like
var objArr = [{objField1:"smith.jpg",objField2:"Smith"},{objField1:"jane.jpg",objField2:"Jane"},...]
Template compilation code is:
var source = document.getElementById("HandleBarTemplate1").innerHTML;
var compiledTemplate = Handlebars.compile(source);
var html = compiledTemplate({objArr:objArr});
If I could get the reference to the Object, then It's so easy to process the data. Rather than passing a field to the function and searching through entire array to get the required object and then process it.
I Prefer a solution without a custom block helper/custom expression helper but if none exists, I'd rather go for a custom block helper. Any solution without searching through the entire array is welcome!
I would suggest a different route.
Knowing that you already have a reference to objArr, make a global or name-spaced variable pointing to it.
For example: window.objArr = objArr;
Create your click handler that does whatever you wish:
function processObject(key){
}
call that with your key inside your template:
< script id="HandleBarTemplate1" type="text/x-handlebars-template">
{{#each objArr}}
<img src="{{objField1}}"/>
<strong>Name:</strong> {{objField2}}
<input type="button" onclick="processObject({{objField2}});"/>
{{/each}}
</script>
Other alternatives:
Customer Handler.
Other alternatives:
If you can't create a reference to objArray, you might could store the properties of the object within data- attributes if you're using html5. processObject could then retrieve them.
I do this with a Handlebars helper and an array of context objects.
Somewhere we have an array of context objects, which will store a reference to each context object we need to reference:
var ContextObjects = [];
We need to register a Handlebars helper function that stores the current context object in the array for us when we place "{{obj}}" in the template. The helper returns the index of the object in the array, which is what gets rendered:
// Must be function and not lambda to preserve the correct 'this'
Handlebars.registerHelper("obj", function ()
{
var contextObject = this; // the current context object
var index = ContextObjects.indexOf(contextObject);
if (index < 0)
{
index = ContextObjects.length;
ContextObjects[index] = contextObject;
}
return index;
});
Next we need a function to retrieve the object from the index on the element:
// Get the context object for the current event (e.g. click or context).
function GetContextObject(event)
{
var $e = $(event.currentTarget);
var index = $e.attr("data-obj");
return ContextObjects[index];
}
We tie them together in the template something like this:
{{#each Items}}
<div data-obj="{{obj}}" onclick="DoSomething(GetContextObject(event));">...</div>
{{/each}}
Which may render something like this:
<div data-obj="0" onclick="DoSomething(GetContextObject(event));">...</div>
<div data-obj="1" onclick="DoSomething(GetContextObject(event));">...</div>
<div data-obj="2" onclick="DoSomething(GetContextObject(event));">...</div>
...

Resources