Subscribing to changes in a Collection but not in a template - meteor

I'm very new to meteor, so apologies if I'm missing something very basic here.
I thought it would be fun to create a very simple textpad style app to check out meteor. I took the todo app and changed the data structures to be 'folders' and 'docs' rather than 'lists' and 'todos', so I have a list of folders and when you click on the folder you get a list of the documents in that folder.
I've then added some code to show the 'content' attribute of a single 'doc' when one of the docs in the list is clicked.
I'm using ace to add some pretty print to the content of the doc (https://github.com/ajaxorg/ace). I've set ace up to work with a hidden textarea containing the plaintext version of my document, and the editor object takes this text and pretty prints it.
The problem with ace is that I don't want the template containing the ace editor to be replaced every time the contents of the doc changes (as it takes half a second to reinitialise, which is a crappy experience after every character is typed!). Instead, I want to update the textarea template and then use the ace API to tell the editor to update it's input based on what is in the textarea.
Now, this is probably the wrong way to approach the problem, but I've ended up using two templates. The first contains a textarea containing doc.contents, which is reactive to the underlying model:
<template name="doc_content">
<textarea name="editor">{{content}}</textarea>
</template>
The second one contains the 'editor' div which ace uses to display the pretty printed text.
<template name="doc_init">
<div id="editor"></div>
</template>
The idea is that the first template will update every time the user types (on all clients), and the second template is only ever re-loaded for each new doc we load.
Template.doc_content.content = function() {
var doc_id = Session.get('viewing_itemname');
if (!doc_id) {
return {};
}
var doc = Docs.findOne({_id:doc_id});
if (doc && doc.content) {
// #1 Later
var editor = Session.get('editor');
if (editor) {
editor.getSession().setValue(doc.content);
}
return doc.content;
} else {
return '';
}
};
When you enter text into the editor div I make a call to Docs.update(doc_id, {$set: {content: text}});, which updates the value in the textarea on each client. All good so far.
editor.getSession().on('change', function(){
var text = editor.getSession().getValue();
Docs.update(doc_id, {$set: {content: text}});
});
What I want to do, for all clients other than the client which made the change, is to subscribe to the change for that doc and call editor.getSession().setContent() with the text which has just been changed, taking the text from the textarea and using it to fill the editor.
I've tried to do this by making that call from the template containing the textarea (as this changes whenever the doc is updated - see #1 above). However, this puts the clients into an infinite loop because changing the value in the editor causes another call to Docs.update.
Obviously this doesn't happen when you render a template, so I'm assuming there's some magic in meteor which can prevent this happening, but I'm not sure how.
Any thoughts?
TIA!

There's a lot to absorb in your question, but if I understand correctly, you might simply be after Deps.autorun:
Deps.autorun(function () {
var doc_id = Session.get('viewing_itemname');
if (!doc_id) {
return {};
}
var doc = Docs.findOne({_id:doc_id});
// do stuff with doc
});
Deps.autorun is really useful in that it will get re-run if any of its
dependencies change. These dependencies are limited to those that are "reactive"
such as Collections and Sessions, or anything that implements the reactive API.
In your case, both Session.get and findOne are reactive so if their values
change at all, Deps.autorun will run the function again.

Related

How make a dynamic value in MeteorJS

I need to set a variable or array in meteor JS which whenever changes. Wherever it is used on the page changes.
So far I have tried to set values in session.
let in = Session.get("in");
and this in HTML. The following works fine. This changes whenever it detects a change in array
{{#each in}}
<span class="selectable-tags"> {{this}} </span>
{{/each}}
But whenever I try to add these tags to other div using following. Then the ones added to other div does not change.
"click .selectable-tags"(e, t) {
// sets the tags inside the Method box on click
var el = e.target.cloneNode(true);
el.setAttribute('contenteditable', false);
document.querySelector('[contenteditable]').appendChild(el); },
using meteor with blaze UI. This can be used as reference link
Edit: Session.get or set is given for reference . This was to tell that these values are changing as they on any event triggered wherever set.
You need to add a helper that returns the session variable and show the helper in your html code :
Template.yourtemplate.helpers({
inHelper: ()=> {
return Session.get('in');
}
)}
And in html :
<div>{{inHelper}}</div>
Every modification on in by Session.set('in', newValue) will change your HTML.

Meteor - handling events in nested templates... without polluting 'Session' variable

I've come across this situation several times now and I realise I'm not really confident about the 'meteor/right' way to handle it.
Suppose I have a form with several parts - each represented by a template - and within each part there are more templates representing eg. datepickers etc.
<template name='myForm'>
{{>partOne}}
{{>partTwo}}
<button class='submit'>Submit</button>
</template>
<template name='partOne'>
{{>widget}}
{{>widget}}
</template>
<template name='widget'>
<input class='datepicker' />
</template>
I want to keep track of my form as the user fills it out - on the level of the 'myForm' template - but all the events are happening at the level of 'widget'.
One solution I keep seeing (e.g. in this SO answer) is to just put everything in the global Session variable. Like so
Template.widget.events({
'click .select' : function(event, template){
var name = template.data.name;
Session.set(name, $(event.currentTarget).val());
}
});
And then in myForm I should do something like this
Template.myForm.rendered = function(){
Tracker.autorun(function(){
var name = Session.get('name');
// do something
});
}
But as my forms are getting more complicated, I find this is really turning into a mess on the myForm template level, all while filling up my Session variable with data that isn't really application-global.
I'd be really grateful for any ideas on how others deal with this ! Keeping templates and widgets modular while still being able to follow and react to their triggered events from parent templates...
You're not alone in feeling like something just isn't right. This is one of the reasons there's a lot of talk about a Blaze 2. Here's what I do:
Create an app global namespace (e.g. G = {}). I usually use the first letter of the app name & do this in lib/config/_namespace.js
Put your collections in G.Collections or G.C,
Put your shared functions in G.Fx, etc...
Put your template vars in G.T.
Then, save that variable to G.T.varName. In doing so, you can use it in rendered as well as events and helpers. As a perk, it's super easy to find all your "globals" because they're all in the G object. Additionally, you can now 'use strict' again.
Then, to keep it clean:
Template.parentTemplate.destroyed = function() {
G.T = {};
};
So if you need reactivity, just make a ReactiveDict:
Template.parentTemplate.created = function() {
G.T.RD = new ReactiveDict();
};
You can use a file-level ReactiveVar or ReactiveDict, instead of the Session object.

How to change the value of a variable on click in meteor

In my meteor app I need to load an array of items corresponding to the item clicked.As I'm new to meteor, I'm held up here.Here is my code.
Template.templatename.events({
'click .showdiv' : function()
{
Template.templatename.vname = function () {
return Db.find();
}
}
Can I set the variable vname dynamically by this code ? This is not working for me.
I think you're misunderstanding the notion of reactivity. A reactive data source will cause any functions which depend on it (including helpers) to rerun when its value is changed, which seems to be the behavior you're looking for here. Instead, you're rewriting the helper function itself every time an item is clicked, which kind of defeats the object of Meteor's reactive data model. Session variables could help:
Template.templatename.events({
'click .showdiv' : function() {
Session.set('vname', Db.find());
}
});
Template.templatename.vname = function () {
return Session.get('vname');
}
If you use an {{#each vname}} block in the templatename template, it will automatically update with the results of the Db.find() query when a .showdiv is clicked. If all you want to do is show the result of that query regardless of whether a click has been registered it would be as simple as:
Template.templatename.vname = function () {
return Db.find();
}
Note that it's still not clear exactly what data you're trying to populate here since the query will return a cursor (which is fine, but you need to loop through it using {{#each ...}} - use findOne if you only want one item), and its contents aren't going to depend on anything intrinsic to the click event (like which .showdiv you clicked). In the former example it will however fail to show anything until the first click (after which you would have to reset with Session.set('vname', null) to stop it showing anything again).

How to update using angularFireCollection which items not exist previously

I have this parent node with text and I want to add a media into the parent node also if the text is changed will update as well.
{
parent: {
text: 'this is content'
}
}
The media value from a third-party callback. How to pass it to update()? I tried {media: callbackVal} but not working.
$scope.parent = angularFireCollection(firebaseRef.child('parent'));
$scope.parent.update(What_to_do_here, function(error){
//something...
});
UPDATE
Maybe my question is not clear enough.
In Firebase JS, we can do this to update or insert media into the node.
new Firebase(firebaseRef).update({ media: 'value'} );
How to do this in `angularFireCollection ?
Check out the annotated AngularFire source: http://angularfire.com/src/angularFire.html#section-38
It looks like AngularFireCollection.update() takes a key/item and a callback function. So you need to edit the entry you want to update directly in your AngularFireCollection, (e.g. $scope.parent.getByName('media').value = 'alligator scrimshaw') and then call $scope.parent.update($scope.parent.getByName('media'), function(err) { ... }).
NOTE: One rather confusing thing (which maybe I am misunderstanding) is that there doesn't seem to be a way to add data to an AngularFireCollection by id. So if media doesn't already exist in your Firebase, $scope.parent.getByName('media') will return undefined. For this reason, if you don't need explicit syncing, and implicit syncing is fine, I would use an AngularFire object instead of an AngularFireCollection.

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
}
});

Resources