How to reach _id of document when it's in the parent data context? - mongodb

This seems like an open-and-shut case for Template.parentData(), but to this day I've never once managed to get that bad boy working properly.
What I want is an event that updates a document depending on which button was clicked, but the buttons are themselves dependent on an array buried deeper in the document, where the _id doesn't exist.
Here's what I have:
First, a helper that sets the context peopleList:
Template.people.helpers({
peopleList: function() {
return People.find()
}
Which I use to iterate through in the HTML, printing out the first and last name of each person stored in the database, as well as their favorite colors (extraneous markup removed):
{{#each peopleList}}
<li>
{{firstName}} {{lastName}}
{{#each favoriteColors}} <button>{{this}}</button> {{/each}}
</li>
{{/each}}
It should be noted at this point that favoriteColors is a key inside the document which holds an array. So the whole thing looks something like this:
{
firstName: "Johnny",
lastName: "Boy",
favoriteColors: ["red", "blue", "blanchedAlmond"]
}
Imagine now that I want to be able to press any of these buttons, which hold the favorite colors, to set the, uh, super-duper favorite color or something. So a button click on blanchedAlmond should update the document, adding the key masterColor with the value blanchedAlmond.
The event:
'click button': function() {
var masterColor = ????
var docId = ????
Meteor.call('setMasterColor', masterColor, docId)
}
I could provide HTML data-tags that hold the color value (because this inside the event spits out some weird array with each letter separated for some reason) and even the _id with {{../_id}}, but that feels like cheating, and I really want to learn how to do the same thing inside a helper or an event.
I strongly feel like this would be a case for Template.parentData() but it returns nothing at all when I console.dir it. What should I do?

The confusion around parentData has to do with the event context. The event is attached to the template whose context is something that isn't a person or a color. Whenever you get the feeling that you need to start littering your code with data- attributes, the answer is nearly always to add more templates. For example:
html
<template name="myTemplate">
<ul>
{{#each peopleList}}
{{> person}}
{{/each}}
</ul>
</template>
<template name="person">
<li>
{{firstName}} {{lastName}}
{{#each favoriteColors}}
{{> color}}
{{/each}}
</li>
</template>
<template name="color">
<button>{{this}}</button>
</template>
js
Template.color.events({
'click button': function() {
// this context is a color - remember to convert it to a string
var masterColor = String(this);
// the parent context is a person
var docId = Template.parentData(1)._id;
return Meteor.call('setMasterColor', masterColor, docId);
}
});

Related

Meteor templates. How can I not render a HTML element based on an {{#if}}?

I have a collection that stores phone numbers for companies.
If a company has a phone number, draw those phone numbers.
If a company has no phone number, don't draw any.
Currently it half works. It will not draw the phone numbers if no numbers are in the collection, but it still draws the < h4 >Phone< /h4 > heading and I don't want it to.
Here's the code:
<template name="orgPage">
<h2>Organisation Name: {{name}}</h2>
<h3>Contact Details</h3>
<ul>
{{#if phone}}
<h4>Phone</h4>
{{#each phone}}
<li>{{number}} ({{type}})</li>
{{/each}}
{{else}}
<p>No contact numbers</p>
{{/if}}
</ul>
</template>
and
Template.orgPage.helpers({
'phone': function() {
return organisationsPhoneNumbers.find({ orgId: currentOrgId })
}
});
How can I get it to NOT draw the < h4 >Phone< /h4 > if there is no data returned from the collection?
short answer
Keep all of your original code and replace {{#if phone}} with {{#if phone.count}}
long answer
Spacebars has a really cool path evaluation feature, which is best explained with an example.
Imagine you have a post document in your current context. Each post is modeled to have a fetchAuthor helper, which returns a user document. Let's suppose you need the lower cased version of the author's last name. In JavaScript you could write something like:
post.fetchAuthor().profile.firstName.toLowerCase()
Now if we need that value in a template we can write:
{{post.fetchAuthor.profile.firstName.toLowerCase}}
As spacebars evaluates each identifier in the path, it checks to see if it's a function - if it is, it invokes it. Note this only works if the called functions take no arguments.
Circling back to our original example, the phone helper returns a cursor, which has a count function. We can write {{#if phone.count}} and spacebars will figure out that we mean phone.count() because count is a function.
I faced this problem early on, here's a simple approach where you return an object from the helper that includes the count:
js:
Template.orgPage.helpers({
'phone': function() {
var cursor = organisationsPhoneNumbers.find({ orgId: currentOrgId });
return { count: cursor.count(), items: cursor };
}
})
html:
{{#if phone.count}}
<h4>Phone</h4>
{{#each phone.items}}
<li>{{number}} ({{type}})</li>
{{/each}}
{{/if}}
There is a fairly standard pattern for this kind of scenarios that avoids re-running the same helper multiple times:
<template name="orgPage">
<h2>Organisation Name: {{name}}</h2>
<h3>Contact Details</h3>
{{#with phone}}
{{#if count}}
<h4>Phone</h4>
<ul>
{{#each .}}
<li>{{number}} ({{type}})</li>
{{/each}}
</ul>
{{else}}
<p>No contact numbers</p>
{{/if}}
{{/with}}
</template>
The with block sets the scope for its content to the result of thephone helper, which is a cursor.
It then checks if the count() helper/method is truth-y. If so, it uses an each iterator tor render the list of items, else - the message indicating no numbers is displayed.
Note that there is an each...else clause that works if you don't need anything outside the each block.

About how to use array to make a meteor tag app

After read the discovermeteror, I want to make a tags app.
It will be like how Stack Overflow's questions are tagged.
So, my code is:
Tags.insert ({ food : ['apple','hotdog','meat','bean']});
food = function () { return Tags.find() }
your food is<span class="tag"> {{food}}</span>
However, all tags will appear in one tag.
What should I do to divide them?
each accepts arrays too not only cursors
{{#each food}}
<span class="tag"> {{this}}</span>
{{/each}}
food = function () {
return Tags.findOne({yourselector}).food;
}
or
{{#each alltags}}
{{#each food}}
<span class="tag"> {{this}}</span>
{{/each}}
{{/each}}
alltags = function () {
return Tags.find();
}
Here is the working Meteorpad, you can see how they are sorted on a <ul> element, just to show how they are not siameses twins

Couldn't manipulate Images.find() in CollectionFS for MeteorJS app

My app is sort of like TelescopeJS, but a lot simpler. I'm trying to echo the particular image that has been added in the post-adding form which takes an input of the name of the post, picture, categories and description. It has 2 collections, one for Articles and the other for Images (NOT a mongo collection, it's an FS collection.) The articles collection stores the name,description and category name and the other one stores image. **My Problem is: ** in the FS collection doc, the loop
{{#each images}}
<img src="{{this.url}}" alt="" class="thumbnail" />
{{/each}}
Where images: returns Images.find({}) and my articles code is :
{{#each articles}}
<li style="margin-right: 1%;">{{>article}}</li>
{{/each}}
Where articles: returns Articles.find({})
MY articles template HAS the images loop and this causes ALL THE IMAGES in the collection to be shown in one post. I just want specific images to be shown for the specific post.
These are the events:
'change .img': function(event, template) {
FS.Utility.eachFile(event, function(file) {
Images.insert(file, function (err, fileObj) {
//Inserted new doc with ID fileObj._id, and kicked off the data upload using HTTP
});
});
},
'click .save':function(evt,tmpl){
var description = tmpl.find('.description').value;
var name = tmpl.find('.name').value;
var date=new Date();
var cat = tmpl.find('.selectCat').value;
Articles.insert({
description:description,
name:name,
time:date.toLocaleDateString()+' at '+date.toLocaleTimeString(),
author:Meteor.userId(),
userEmail:Meteor.user().username,
category:cat,
});
}
<template name="article">
{{#each images}}
<img src="{{this.url}}" alt="" class="thumbnail" />
{{/each}}
Here goes the {{name_of_post}}
Here {{the_category}}
Here {{the_description}}
</template>
So what happens is, all the images that I've uploaded so far shows in one post and all the posts' picture looks the same. Help please!
You should know that fsFile support Metadata so maybe you don't need the Articles Collection
So we can make a new eventHandler.
'click .save':function(evt,tmpl){
var description = tmpl.find('.description').value,
file = $('#uploadImagePost').get(0).files[0], //here we store the current file on the <input type="file">
name = tmpl.find('.name').value,
date=new Date(),
cat = tmpl.find('.selectCat').value,
fsFile = new FS.File(file); // we create an FS.File instance based on our file
fsFile.metadata = { //this is how we add Metadata aka Text to our files
description:description,
name:name,
time:date.toLocaleDateString()+' at '+date.toLocaleTimeString(),
author:Meteor.userId(),
userEmail:Meteor.user().username,
category:cat,
}
Images.insert(fsFile,function(err,result){
if(!err){
console.log(result) // here you should see the new fsFile instance
}
});
}
This is how our new event will look, now our .save button insert everything on the same collection.
This is how we can access to the FS.File instances fields using the keyword 'metadata.fieldName'.
For example.
Teamplate.name.helpers({
showCategory:function(){
// var category = Session.get('currentCategory') you can pass whatever data
// you want here from a select on the html or whatever.
//lets say our var its equal to 'Music'
return Images.find({'metadata.category':category});
}
})
Now we use that helper on the html like any normal collection
<template name="example">
{{#each showCategory}}
Hi my category is {{metadata.category}} <!-- we access the metadata fields like any normal field on other collection just remember to use the 'metadata'keyword -->
This is my image <img src="{{this.url}}" >
{{/each}}
</template>

Confused as to how the template render action is called

I am trying to make the songs in a playlist appear on screen each time a user enters a song of choice. I have the following action to insert the song that they chose into the database:
Template.search_bar.events({
'keypress #query' : function (evt,template) {
// template data, if any, is available in 'this'
if (evt.which === 13){
var url = template.find('#query').value;
$("#query").val('');
$('#playlist_container').animate({scrollTop: $('#playlist_container')[0].scrollHeight});
Template.list.search_get(url,0); //insert records into the database
}
}
});
Template.list.search_get inserts the record into the database:
Meteor.call('update_record',Template.list.my_playlist_id, song, function(err,message){});
on the server side, I am pushing records into my database with the following format:
update_record: function(sessID, songObj){
Links.update({sess: sessID}, {$push: {songs: {song_title: songObj["title"], videoId: songObj["video_id"], thumbnail: songObj["thumbnail"], index: songObj["index"]}}});
},
basically all my records have the format of:
{_id:,
sess:,
songs: [{song_title:,
videoId:,
thumbnail:,
index:},
{song_title:,
videoId:,
thumbnail:,
index:},...]
}
An array of song objects inside the songs field of the record. What I am trying to do is each time a user hits the search button, make that new song appear in the list. I am not sure how many times the render function gets called or how template renders a database object in hmtl. Currently i have the following html template for my list:
<template name="list">
<div id="playlist_container">
<ul id="playlist">
{{#each my_playlist.songs}}
{{> track}}
{{/each}}
</ul>
</div>
I believe my_playlist should call the following action on the client:
Template.list.my_playlist = function(){
console.log("myplaylist is called");
return Links.findOne({sess: Template.list.my_playlist_id});
}
It should return an object which contains an array of song object, for which i iterate through in #each my_playlist.songs, which should render each of the following track template:
<template name="track">
<li id="{{index}}" class="list_element">
<div class="destroy"> </div>
<div class="element_style">{{song_title}}</div>
</li>
</template>
However, upon successful insertion of record, i am not seeing the new song title appear. Any suggestions on how I might approach this?
This code is the problem.
Template.list.my_playlist = function(){
return Links.findOne({sess: Template.list.my_playlist_id});
}
Template.list.my_playlist_id never updates, and thus, the new template never renders.
Try this approach.
if (Meteor.isServer) {
Meteor.methods({
update_record: function(sessID, songObj){
// update Links
return Links.findOne({sess: sessID});
}
});
} else {
Meteor.call('update_record',Template.list.my_playlist_id, song, function(err, song){
Session.set('playlist', song);
});
Template.list.my_playlist = function(){
return Session.get('playlist');
}
}

Callback for when a child dom element is inserted or has its class changed?

In Ember.js, I have a view that has
{{#if obj.property}}
<div {{bindAttr class="prop"}}>content</div>
{{/if}}
How can I get called back for when this element is inserted into the view, and for when the class is attached onto the element? I want to do this because the CSS class is an animation class, and I'd like to hook onto the onAnimationEnd event of the element so that I get notified when the animation ends.
How about changing the div to be a custom view subclass that implements didInsertElement? e.g.
{{#if obj.property}}
{{view App.MyView}}
{{/if}}
and
App.MyView = Ember.View.extend({
classNameBindings: "prop",
didInsertElement: function() {
// use this.$() to get a jQuery handle for the element and do what you'd like
}
})
In addition to Luke's answer, I found out another way to achieve this, which may be preferable since creating a view is required for Luke's approach.
By exploiting the fact that DOM events bubble up, I can setup an event handler for animationEnd on a parent DOM element that contains whatever may be inserted. E.g.
<div id="container">
{{#if obj.property}}
<div {{bindAttr class="prop"}}>content</div>
{{/if}}
</div>
// view.js
didInsertElment: function() {
this.$('#container').bind('webkitAnimationEnd', function(e) {
// e.target is the element whose animation ended.
}
}