Meteor: Tracker.autorun / observeChanges & collections not working as expected - javascript

I am new to using meteor, so I am hoping to receive a very basic explanation of how these functions work, and how I am supposed to be using them. Otherwise, if there is a method that would be better suited to what I am hoping to achieve, the knowledge would be appreciated.
The functionality I was hoping to achieve:
I have a Mongo collection that contains a number value within documents that are assigned to specific users.
I will use the value that I get from the document to populate a width: xx% in some css in-line styling on a 'progressbar'. But the other use I have for it is performing some kind of 'reactive' function that runs whenever this value changes which can update the background color of this progress bar dynamically based off of the current value of the progressbar. Think 'red' for low and 'green' for high:
project.html:
<template name="progressBar">
<div id="progress-bar" style="width:{{curValue}}; background-color:*dynamicColor*;"></div>
</template>
project.js:
Progress = new Mongo.Collection("progress");
Template.progressBar.helpers({
curValue: function () {
return Progress.findOne({user: Meteor.userId()}).curValue;
}
});
The above sometimes works. But it doesn't seem to be reliable and isn't working for me right now. I get errors about cannot read property 'curValue' of undefined. From what I have researched online, that means that I am trying to access this document before the collection has loaded. But I really cannot find a direct solution or wrap my head around how I am supposed to be structuring this to avoid that error.
The next problem is observing changes to that value and running a function to change the background color if it does change.
Here are a few of the types of autorun/observe pieces of code I have tried to make work:
Session.set('currentValue', Progress.findOne({user:Meteor.userId()}).curValue);
Tracker.autorun(function(){
var currentValue = Session.get('currentValue');
updateColor(currentValue);
});
var currentValue = Progress.findOne({user:Meteor.userId()}).curValue);
var handle = currentValue.observeChanges({
changed: function (id, currentValue) {
updateColor(currentValue);
}
});
To sum up the question/problem:
I want to use a value from a mongo db document in some in-line css, and also track changes on that value. When the value changes, I want a function to run that will update the background color of a div.
Update
Using #Ethaan 's answer below, I was able to correct my subscription/template usage of my collection data. I did a bit more digging and come to a greater understanding of the publish/subscribe methods and learned how to properly use the callbacks on subscriptions to run my Tracker.autorun function at the appropriate time after my collection had loaded. I was able to expand on the answer given to me below to include a reactive Tracker.autorun that will run a function for me to update my color based on my document value.
The code that I ultimately got working is as follows:
project.js
if (Meteor.isClient) {
Progress = new Mongo.Collection("progress");
Template.content.onCreated(function () {
var self = this;
self.autorun(function () {
self.subscribe("progress", function(){
Tracker.autorun(function(){
var query = Progress.findOne({user: Meteor.userId()}).level;
changeColor(query);
});
});
});
});
Template.content.helpers({
value: function(){
return Progress.findOne({user: Meteor.userId()}).level;
}
});
function changeColor(value){
//run some code
}
}
if (Meteor.isServer) {
Progress = new Mongo.Collection("progress");
Meteor.publish("progress", function () {
return Progress.find({user: this.userId});
});
}
project.html
<head>
<title>Project</title>
</head>
<body>
{{> standard}}
</body>
<template name="standard">
...
{{> content}}
</template>
<template name="content">
{{#if Template.subscriptionsReady}}
{{value}}
{{else}}
0
{{/if}}
</template>

that means that I am trying to access this document before the
collection has loaded
Seems like you get the problem, now lets get ride to some possible solutions.
Meteor version 1.1
If you are using the new meteor version 1.1 (you can check running meteor --version)
use this.
First on the onCreated function use this.
Template.progressBar.onCreated(function () {
var self = this;
self.autorun(function () {
self.subscribe("Progress");
});
});
See more about subscriptionReady on the DOCS.
Now on the HTML use like this.
<template name="progress">
{{#if Template.subscriptionsReady}}
<div id="progress-bar" style="width:{{curValue}}; background-color:*dynamicColor*;"></div>
{{else}}
{{> spinner}} <!-- or whatever you have to put on the loading -->
{{/if}}
</template>
Meteor under 1.0.4
You can have on the router something like a waitOn:function(){}
waitOn:function(){
Meteor.subscribe("Progress");
}
or since helper are asynchronous do something like this (not recommendable).
Template.progressBar.helpers({
curValue: function () {
query = Progress.findOne({user: Meteor.userId()}).curValue;
if(query != undefined){
return query;
}else{
console.log("collection isn't ready")
}
}
});

Related

display search results made in Template events in Meteor

I have a meteor project in which, with 3 radio buttons, I can choose different variables of my search; then, with a click on the button submit of the form, the query to do in mongo is created.
This happens in events:
//global var
var resultCursor;
...
Template.device.events({
click .btn-search": function (event) {
event.preventDefault();
[...]
resultsCURSOR = myCollection.find(query, queryOptions);
})
Now, I have to display the results in the page with an helper.
I tried saving the cursor in a global var, and pass it to the helper (I cannot save in a Session var, as described here - it doesn't work)
Template.device.helpers({
searchResults: function() {
return resultCursor;
}
})
And in html
{{#each flightSearchResults}}
<div class="flight-list">
<p>Pilot: {{this.pilot}}</p>
<p>Time: {{this.date}}</p>
</div>
{{/each}}
But this does not work. I found a kind of solution here but I have to move the entire query to helpers and before doing this... is this the only solution? And is that a good practice solution?
Put the query (not the cursor) in a Session variable, like this:
Template.device.events({
click .btn-search": function (event) {
event.preventDefault();
[...]
Session.set('query', query);
})
Change your helper (note that the name was wrong):
Template.device.helpers({
flightSearchResults: function() {
return myCollection.find(Session.get('query'), queryOptions);
}
});
You don't need to change your html, and you can now remove all occurrences of resultCursor.

Meteor: onhashchange does not fire

I have a weird situation. It seems Meteor with Iron::Router is overriding somewhere the onhashchange event however I was unsuccessfully to track it down.
Basically if I listen for that event, it never fires for some reason. I looked and searched everywhere and can not even find any reference to onhashchange in the Meteor code base.
if(Meteor.isClient) {
window.addEventListener('hashchange', function() {
alert('changed');
});
}
This never fires - although the event is properly registered. In plain vanilla it works fine .. so I assume it's somewhere being overwritten.. any insights will be appreciated.
http://jsfiddle.net/L2dj3o7n/
Oh one more thing, this is how my URL's look like right now for testing:
http://localhost:3000/#/workflow
http://localhost:3000/#/settings/account
http://localhost:3000/#/group/add
etc
From the Router Parameters section of the Iron Router guide:
If there is a query string or hash fragment in the url, you can access
those using the query and hash properties of the this.params object.
// given the url: "/post/5?q=s#hashFrag"
Router.route('/post/:_id',
function () {
var id = this.params._id;
var query = this.params.query; // query.q -> "s"
var hash = this.params.hash; // "hashFrag"
});
Note: If you want to rerun a function when the hash changes you can do
this:
// get a handle for the controller.
// in a template helper this would be
// var controller = Iron.controller();
var controller = this;
// reactive getParams method which will invalidate the comp if any part of the params change
// including the hash.
var params = controller.getParams();
getParams is reactive, so if the hash updates, the getParams() call should invalidate and trigger a new computation for whatever you're using it for. For instance, if you wanted to dynamically render a template depending on the hash value, you should be able to do something like this...
HTML:
<template name='myTemplate'>
{{> Template.dynamic template=getTemplateFromHash}}
</template>
JS:
Template.myTemplate.helpers({
getTemplateFromHash: function() {
var hash = Iron.controller().getParams().hash;
... // do whatever you need to do with the hash to figure out the template to render
}
});

$route.reload(); Issue

Hi im currenty using $route.reload to refresh the content of my controller Every time I update my Database. the problem is when updating huge list of data, Every Time I update my Database and run $route.reload my browser lose its ability to scroll up or down my browser, it works fine with smaller list of Data.
below is a sample of my code
$scope.Undone = function(id){
$scope.index = $scope.GetID ;
CRUD.put('/UndoJda/'+$scope.index).then(function(response){
toastr.info('Jda has been activated.', 'Information');
$route.reload();
});
}
Your best bet would be some sort of lazy loading/pagination. So in case it's a really large list, like in the tenths of thousands, it might even be a DOM rendering problem. Also, if that isn't the case, you should try using AngularJS's bind once(Available since 1.3), as well as track by which does not create a watcher for each object on the scope, in your template. Assuming you are using ngRepeat, let's say something like this:
...<ul>
<li ng-repeat="item in Items">
<b>{{item.name}}</b>
</li>
</ul>
Change that to something like the following, in case the data does not update often:
...<ul>
<li ng-repeat="item in Items track by $index">
<b>{{::item.name}}</b>
</li>
</ul>
As a side note, try to always have a dot in your model's name. $scope.Something.list, for eaxample. ("If you don't have a dot, you are doing it wrong" - Misko Hevery himself said this.).
When the data is huge, try to use $timeout and reload the page.
This would prevent very fast refreshes and will keep your page responsive.
$scope.Undone = function(id){
$scope.index = $scope.GetID ;
CRUD.put('/UndoJda/'+$scope.index).then(function(response){
toastr.info('Jda has been activated.', 'Information');
$timeout(function() {
$route.reload();
}, 200);
});
}
You can do it by using $interval
$interval(function() {
CRUD.put('/UndoJda/'+$scope.index).then(function(response){
toastr.info('Jda has been activated.', 'Information');
// Update scope variable
});
}, 2000);
and also don't use $route.reload();. because Angularjs supporting SPA (Single Page Application). if you using $route.reload();. Every time page will loading, So it's not good. you need just call the Service code in inside of interval.
First I would recommend removing usage of $route.reload(), your use case doesn't require the view re-instantiating the controller. Instead you should update the $scope variable that holds the collection of entities your presenting in the view. You will also want to consider adding UX features such as a loading indicator to inform the user about the long running task.
Something similar too the code below would achieve what your looking for. I am unaware of what your CRUD js object instance is, but as long as its Angular aware you will not need to use $timeout. Angular aware usually means non third party APIs, but you can use $q to assist in exposing third party ajax results to angular.
// angular module.controller()
function Controller($scope, EntityService) {
$scope.entityCollection = [];
$scope.loadingData = false; // used for loading indicator
// Something will initialize the entity collection
// this is however your already getting the entity collection
function initController() {
$scope.refreshCollection();
}
initController();
$scope.refreshCollection = function() {
$scope.loadingData = true;
EntityService.getEntitites().then(function(resp) {
$scope.entityCollection = resp;
$scope.loadingData = false;
});
}
$scope.Undone = function(id) {
$scope.index = $scope.GetID ;
CRUD.put('/UndoJda/' + $scope.index).then(function(response){
toastr.info('Jda has been activated.', 'Information');
$scope.refreshCollection();
});
}
}
// angular module.factory()
function EntityService($q, $http) {
return {
getEntitites: function() {
var deferred = $q.defer();
$http.post('/some/service/endpoint').then(function(resp) {
deferred.resolve(resp);
});
return deferred.promise;
}
}
}

How to animate unchanged ng-repeat with AngularJS

I have a template that looks like this:
<p ng-repeat="item in myobj.items" class="toAnimate">{{item}}</p>
and I would like to use the animate module do a jQueryUI addClass/removeClass animation on the element using the JavaScript method described in the docs:
ngModule.animation('.toAnimate', function() {
return {
enter: function(element) {
element.addClass('pulse').removeClass('pulse', 2000);
}
};
});
This works beautifully, but the problem is that, since I want to use the p.toAnimate element to display status messages, it will not change the content according to angular.
To break it down a little further, say I have a name field. When I click Save the message Name was saved successfully. is displayed. Now if I modify the name and click save again, assuming the save was successful, the message should be re-displayed to give the user feedback of the newly edited name. The pulse does not happen, however, because the items in myobj.items didn't technically change.
I realize that I could remove the item after a period of time (and that is probably the route I will take to implement the real solution), but I'm still interested to see if this sort of thing can be done using AngularJS.
What I want to do is register with angular that the message should be treated as new even though it is not. Is there any way to do this?
A fiddle to go along with this: http://jsfiddle.net/Jw3AT/
UPDATE
There is a problem with the $scope.$$phase approach in my answer, so I'm still looking for the "right" way to do this. Basically, $scope.$$phase is always returning $digest, which causes the conditional to fail. Removing the conditional gives the correct result in the interface, but throws a $rootScope:inprog.
One solution I found is to add a $apply in the middle of the controller function:
$scope.updateThingy = function () {
$scope.myobj.items = [];
if (!$scope.$$phase) {
$scope.$apply();
}
$scope.myobj.items = ['Your name was updated.'];
};
Updated fiddle: http://jsfiddle.net/744Rv/
May not be the best way, but it's an answer.

how to make a meteor template helper re-run/render after another template has rendered?

I have a template helper called {{renderNav}} in a template Nav
e.g.
Template.Nav.renderNav
and within that helper function I want to parse the rendered output of another helper within a different template
For example the helper
Template.contentWindow.content
which provides the html for
{{content}}
and my renderNav helper wants to part the html that replaces {{content}} to generate the html for
{{renderNav}}
how would I do this? right now the {{renderNav}} helper executes for or runs more quickly and so it is unable to parse the html that replaces {{content}}
#Hugo - I did the following in my code as you suggested
Template.contentWindow.rendered = function() {
debugger;
return Session.set('entryRendered', true);
};
Template.Nav.renderNav = function() {
debugger;
var forceDependency;
return forceDependency = Session.get('entryRendered');
};
When I run it, the debugger first stops when executing the renderNav helper. (Which makes sense with what I am seeing in terms of the race condition). Then contentWindow renders and I hit the breakpoint above the Session.set('entryRendered', true). But then the renderNav doesn't run again as you suggest it should. Did I misinterpret or incorrectly implement your suggestion?
You need a dependency in the template that you want to rerun. There are few possibilities, depending on what data you want to get.
For example, you can set a reactive marker in the content template that will notify renderNav that it's done with drawing.
Template.contentWidnow.rendered = function() {
...
// Set this on the very end of rendered callback.
Session.set('contentWindowRenderMark', '' +
new Date().getTime() +
Math.floor(Math.random() * 1000000) );
}
Template.renderNav.contentData = function() {
// You don't have to actually use the mark value,
// but you need to obtain it so that the dependency
// is registered for this helper.
var mark = Session.get('contentWindowRenderMark');
// Get the data you need and prepare for displaying
...
}
With further information you've provided, we can create such code:
content.js
Content = {};
Content._dep = new Deps.Dependency;
contentWindow.js
Template.contentWidnow.rendered = function() {
Content.headers = this.findAll(':header');
Content._dep.changed();
}
renderNav.js
Template.renderNav.contentData = function() {
Content._dep.depend();
// use Content.headers here
...
}
If you want the navigation to be automatically rebuilt when contentWindow renders, as Hubert OG suggested, you can also use a cleaner, lower level way of invalidating contexts:
var navDep = new Deps.Dependency;
Template.contentWindow.rendered = function() {
...
navDep.changed();
}
Template.renderNav.contentData = function() {
navDep.depend();
// Get the data you need and prepare for displaying
...
}
See http://docs.meteor.com/#deps for more info.
If, on the other hand, you want to render another template manually, you can call it as a function:
var html = Template.contentWindow();
The returned html will not be reactive. If you need reactivity, use:
var reactiveFragment = Meteor.render(Template.contentWindow);
See the screencasts at http://www.eventedmind.com/ on Spark and reactivity for details on how this works.
UPDATE
To add a rendered fragment to your DOM:
document.body.appendChild(Meteor.render(function () {
return '<h1>hello</h1><b>hello world</b>';
}));
You can also access the rendered nodes directly using the DOM API:
console.log(reactiveFragment.childNodes[0]);

Categories

Resources