Template helper gets called multiple times - javascript

My intention is to retrieve one random entry from a collection and display it on the website - if all sentences are through (read: the user has "seen" them), display something else (therefore a dummy sentence gets returned). But, on server start and on button-click events, this helper gets fired at least twice. Here is some code:
In client.js:
Template.registerHelper('random_sentence', function() {
fetched = _.shuffle(Sentences.find({
users: {
$nin: [this.userId]
}
}).fetch())[0];
if (fetched === undefined) {
return {
sentence: "done",
_id: 0,
done: true
};
}
Session.set('question', fetched._id);
console.log(fetched);
return fetched;
});
The helper function for the template:
sent: function(){
sent = Session.get('question');
return Sentences.findOne(sent);
}
in main template:
{{#with random_sentence}}
{{#if done}}
<!-- Display something else -->
{{else}}
<div class="container">
{{> question}}
</div>
{{/if}}
{{/with}}
the "question" template:
<div class="well">
<div class="panel-body text-center">
<h3>{{sent.sentence}}</h3>
</div>
</div>
If I don't return anything in the "random_sentences"-function,nothing get's displayed.
I don't know where my "logic failure" is situated? I'm new to meteor - so I might overlook something obvious.
Thanks in advance :-)
UPDATE: This is how I intended to get the new sentence and display it:
Template.answer.events({
'click': function(event) {
var text = event.target.getAttribute('id');
if (text !== null) {
var question = Session.get('question');
var setModifier = {
$inc: {}
};
setModifier.$inc[text] = 1;
Sentences.update(question, setModifier);
Meteor.call('update_user', question);
Notifications.success('Danke!', 'Deine Beurteilung wurde gespeichert.');
Blaze.render(Template.question, document.head);
}
}
});
In server.js (updating the question and a counter on the user):
Meteor.methods({
update_user: function(question) {
Sentences.update(question, {
$push: {
"users": this.userId
}
});
Meteor.users.update({
_id: this.userId
}, {
$inc: {
"profile.counter": 1
}
});
},
});
I found the Blaze.render function somewhere on the web. the "document.head" part is simply because this function needs a DOM Element to render to, and since document.body just "multiplies" the body, I ust moved it to the head. (DOM logic isn't my strong part).
An Idea I had: would it make the whole idea simpler to implement with iron-router? atm. I wanted to create a "one-page app" - I therefore thought that I don't need a router there.
Another problem: Getting this logic to work (User gets one random sentence, which he has not seen) and publishing small sets of the collection (so the Client don't have to download 5 MB of data before using).

Template helpers can be called multiple times so it's good to avoid making them stateful. You're better off selecting the random entry in an onCreated or onRendered template handler. There you can do your random select, update the state, and put your choice in a Session variable to be retrieved by the helper.

Related

Document does not display only on the first page

I am experiencing this weird scenario that I am unable to figure out what the problem is. There is a pagination for a collection which works fine when navigating. I have 5 documents in a collection with each to display per 2 on a page sing the pagination. Each document has a url link that when clicked it displays the full page for the document.
The challenge now is that if I click a document on the first page, it displays the full record, but if I navigate to the next page and click a document, it displays a blank page. I have tried all I could but haven't gotten what is to be made right.
These earlier posts are a build up to this present one: Publish and subscribe to a single object Meteor js, Meteor js custom pagination.
This is the helper
singleSchool: function () {
if (Meteor.userId()) {
let myslug = FlowRouter.getParam('myslug');
var subValues = Meteor.subscribe('SingleSchool', myslug );
if (myslug ) {
let Schools = SchoolDb.findOne({slug: myslug});
if (Schools && subValues.ready()) {
return Schools;
}
}
}
},
This is the blaze template
<template name="view">
{{#if currentUser}}
{{#if Template.subscriptionsReady }}
{{#with singleSchool}}
{{singleSchool._id}}
{{singleSchool.addschoolname}}
{{/with}}
{{/if}}
{{/if}}
</template>
try this;
onCreated function:
Template.view.onCreated(function(){
this.dynamicSlug = new ReactiveVar("");
this.autorun(()=>{
// When `myslug` changes, subscription will change dynamically.
this.dynamicSlug.set(FlowRouter.getParam('myslug'));
Meteor.subcribe('SingleSchool', this.dynamicSlug.get());
});
});
Helper
Template.view.helpers({
singleSchool(){
if (Meteor.userId()) {
let school = SchoolDb.findOne({slug: Template.instance().dynamicSlug.get()});
if (school) {
return school;
}
}
}
});

With KnockoutJS, how can I scroll to a component after it's rendered in a foreach?

I have deferred updates enabled.
I have two components.
The first is a list, which is simply implemented as a div with a foreach data binding:
<div class="list-people" data-bind="foreach: { data: people, afterRender: afterRenderPeople }">
<!-- ko component: { name: "listitem-person", params: { person: $data } } --><!-- /ko -->
</div>
The second is the list item:
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
afterRender is called for each item in the foreach.
My afterRenderPerson function is simple enough:
public afterRenderPerson = (elements: any[], data: Person) => {
let top = $(element[0]).offset().top;
scrollTo(top);
};
The problem is that when afterRenderPerson is called the sub-component listitem-person hasn't yet been rendered.
Which means the element array passed to afterRenderPerson has 4 nodes:
A text node containing \n i.e. a new line.
A comment node containing <!-- ko component: { name: "listitem-person", params: { person: $data } } -->.
A comment node containing <!-- /ko -->.
A text node containing \n i.e. a new line.
None of these are suitable for getting the top pixel, and even if they were, the sub-component being rendered could affect the layout at that location changing the value of the pixel I'm trying to get.
Unfortunately it seems that the documentation for foreach doesn't take in to account the delayed nature of components.
If you need to run some further custom logic on the generated DOM elements, you can use any of the afterRender/afterAdd/beforeRemove/beforeMove/afterMove callbacks described below.
Note: These callbacks are only intended for triggering animations related to changes in a list.
There are two workarounds I've come across, neither of which are great, but that's why they're workarounds and not solutions!
user3297291 gave the suggestion in a comment of making a scrollTo binding that's placed on the child components.
Only workaround I can think of is to define a custom scrollTo binding and include it in the component template... Quite easy to implement, but still feels hacky and makes your inner component harder to reuse. You might also want to track this feature request – user3297291
This would simply be a custom binding that conditionally executes some code based on a value provided to it.
The bindings aren't called until the HTML has been inserted in to the DOM. That's not perfect, as later changes to the DOM could affect the position of the inserted HTML elements, but it should work for many situations.
I wasn't very keen on having to modify the child components though, I preferred a solution when remained encapsulated in the parent component.
The second workaround is to check to see if the child component HTML element exists in the DOM by it's ID. Since I don't know when they will come in to existence this has to be done in some sort of loop.
A while loop isn't suitable as it'll run the check far too often, in a "tight" loop, so instead setTimeout is used.
setTimeout is a horrid hack, and it makes me feel dirty to use it, but it does work for this situation.
private _scrollToOffset = -100;
private _detectScrollToDelayInMS = 200;
private _detectScrollToCountMax = 40;
private _detectScrollToCount = 0;
private _detectScrollTo = (scrollToContainerSelector: string, scrollToChildSelector: string) => {
//AJ: If we've tried too many times then give up.
if (this._detectScrollToCount >= this._detectScrollToCountMax)
return;
setTimeout(() => {
let foundElements = $(scrollToChildSelector);
if (foundElements.length > 0) {
//AJ: Scroll to it
$(scrollToContainerSelector).animate({ scrollTop: foundElements.offset().top + this._scrollToOffset });
//AJ: Give it a highlight
foundElements.addClass("highlight");
} else {
//AJ: Try again
this._detectScrollTo(scrollToContainerSelector, scrollToChildSelector);
}
}, this._detectScrollToDelayInMS);
this._detectScrollToCount++;
};
I made sure to put a limit on how long it can run for, so if something goes wrong it won't loop forever.
It should probably be noted that there is an "Ultimate" solution to this problem, and that's TKO, AKA Knockout 4.
But that's not "production ready" yet.
How to know when a component has finished updating DOM?
brianmhunt commented on Jun 20
knockout/tko (ko 4 candidate) latest master branch has this.
More specifically, the applyBindings family of functions now return a Promise that resolves when sub-children (including asynchronous ones) are bound.
The API isn't set or documented yet, but the bones have been set up.
This appears to work. I made a binding handler that runs a callback in its init (it uses tasks.schedule to allow a rendering cycle). Attaching it at the parent level does not get the children rendered in time, but attaching it to the virtual element does.
I designed it to work with a function whose signature is like afterRender. Because it runs for each of the elements, the callback function has to test that the data is for the first one of them.
ko.options.deferUpdates = true;
ko.bindingHandlers.notify = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// Make it asynchronous, to allow Knockout to render the child component
ko.tasks.schedule(() => {
const onMounted = valueAccessor().onMounted;
const data = valueAccessor().data;
const elements = [];
// Collect the real DOM nodes (ones with a tagName)
for(let child=ko.virtualElements.firstChild(element);
child;
child=ko.virtualElements.nextSibling(child)) {
if (child.tagName) { elements.push(child); }
}
onMounted(elements, data);
});
}
};
ko.virtualElements.allowedBindings.notify = true;
function ParentVM(params) {
this.people = params.people;
this.afterRenderPeople = (elements, data) => {
console.log("Elements:", elements.map(e => e.tagName));
if (data === this.people[0]) {
console.log("Scroll to", elements[0].outerHTML);
//let top = $(element[0]).offset().top;
//scrollTo(top);
}
};
}
ko.components.register('parent-component', {
viewModel: ParentVM,
template: {
element: 'parent-template'
}
});
function ChildVM(params) {
this.Name = params.person;
}
ko.components.register('listitem-person', {
viewModel: ChildVM,
template: {
element: 'child-template'
}
});
vm = {
names: ['One', 'Two', 'Three']
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<template id="parent-template">
<div class="list-people" data-bind="foreach: people">
<!-- ko component: { name: "listitem-person", params: { person: $data } }, notify: {onMounted: $parent.afterRenderPeople, data: $data} -->
<!-- /ko -->
</div>
</template>
<template id="child-template">
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
</template>
<parent-component params="{ people: names }">
</parent-component>

How do I detect the first time the user logs in and the first time a specific page is loaded?

I would like to Trigger some JS only the first time a user logs in, and only the first time a specific page is loaded.
I believe I can deal with the first time they log in, by simply checking user.sign_in_count < 2, but I don't know how to specify just on the first page load only.
i.e. I don't want the JS to be triggered after the user logs in for the first time and refreshes the page without logging out.
I am using Turbolinks and $(document).on('turbolinks:load', function() { to trigger it.
Edit 1
So what I am trying to do is execute Bootstrap Tour on a number of pages. But I only want that tour to be automatically executed, on the first page load. The tour itself will lead the user to other specific pages within my app, but each of those pages will have page-specific tour JS on each page.
Right now, in my HTML I have something like this:
<script type="text/javascript">
$(document).on('turbolinks:load', function() {
var tour = new Tour({
storage: false,
backdrop: true,
onStart: function(){
$('body').addClass('is-touring');
},
onEnd: function(){
$('body').removeClass('is-touring');
},
steps: [
{
element: "#navbar-logo",
title: "Go Home",
content: "All throughout the app, you can click our logo to get back to the main page."
},
{
element: "input#top-search",
title: "Search",
content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
}
]});
// Initialize the tour
tour.init();
// Start the tour
tour.start();
});
</script>
So all I really want to do is the following:
Not bombard the user with executing a new tour, on their first login, whenever they reload the page.
Allow them to be able to manually execute the tour at a later date if they want, by simple pressing a link.
I don't want to store anything in my DB if I don't have to -- so preferably this should be a cookie-based approach or localStorage
Assume that I will use Rails to track the number of sign-ins they have done. So once they sign in more than once, I can not trigger this JS.
The real problem is just within that first sign in, if they refresh the main page 10 times, this tour gets executed 10 times. That's what I am trying to stop.
I hope that provides some more clarity.
Preface
It's my understanding that you have:
multiple pages that contain a single tour (each page's tour is different)
a way to detect first signin to an account (ruby login count)
ability to add a script value based upon first signin
Solution Overview
The solution below uses localStorage to store a key value pair of each tour's identifier and if it has been seen or not. localStorage persists between page refreshes and sessions, as the name suggests, localStorage is unique to each domain, device, and browser (ie. chrome's localStorage cannot access firefox's localStorage even for the same domain, nor can chrome's localStorage on your laptop access chrome's localStorage on your mobile even for the same domain). I raise this to illustrate the reliance upon Preface 3 to toggle a JS flag for if the user has logged in previously.
For the tour to start, the code checks localStorage for if its corresponding key value pair is not set to true (representing having been "seen"). If it does exist and is set to true, the tour does not start, otherwise it runs. When each tour begins, using its onStart method, we update/add the tour's identifier to localStorage and set its value to true.
Manual execution of the tour can be performed by either manually calling the tour's start method if you would like only the current page's tour to execute, otherwise, you can clear out all of the localStorage related to the tour and send the user back to the first page/if you're on the first page, again just call the start method.
JSFiddle (HTML based off other question's you've asked regarding touring)
HTML (this could be any element with the id="tourAgain" attribute for the following code to work.
<button class="btn btn-sm btn-default" id="tourAgain">Take Tour Again</button>
JS
var isFirstLogin = true; // this value is populated by ruby based upon first login
var userID = 12345; // this value is populated by ruby based upon current_user.id, change this value to reset localStorage if isFirstLogin is true
// jquery on ready function
$(function() {
var $els = {}; // storage for our jQuery elements
var tour; // variable that will become our tour
var tourLocalStorage = JSON.parse(localStorage.getItem('myTour')) || {};
function activate(){
populateEls();
setupTour();
$els.tourAgain.on('click', tourAgain);
// only check check if we should start the tour if this is the first time we've logged in
if(isFirstLogin){
// if we have a stored userID and its different from the one passed to us from ruby
if(typeof tourLocalStorage.userID !== "undefined" && tourLocalStorage.userID !== userID){
// reset the localStorage
localStorage.removeItem('myTour');
tourLocalStorage = {};
}else if(typeof tourLocalStorage.userID === "undefined"){ // if we dont have a userID set, set it and save it to localStorage
tourLocalStorage.userID = userID;
localStorage.setItem('myTour', JSON.stringify(tourLocalStorage));
}
checkShouldStartTour();
}
}
// helper function that creates a cache of our jQuery elements for faster lookup and less DOM traversal
function populateEls(){
$els.body = $('body');
$els.document = $(document);
$els.tourAgain = $('#tourAgain');
}
// creates and initialises a new tour
function setupTour(){
tour = new Tour({
name: 'homepage', // unique identifier for each tour (used as key in localStorage)
storage: false,
backdrop: true,
onStart: function() {
tourHasBeenSeen(this.name);
$els.body.addClass('is-touring');
},
onEnd: function() {
console.log('ending tour');
$els.body.removeClass('is-touring');
},
steps: [{
element: "div.navbar-header img.navbar-brand",
title: "Go Home",
content: "Go home to the main page."
}, {
element: "div.navbar-header input#top-search",
title: "Search",
content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
}, {
element: "span.num-players",
title: "Number of Players",
content: "This is the number of players that are in our database for this Tournament"
}, {
element: '#page-wrapper div.contact-box.profile-24',
title: "Player Info",
content: "Here we have a quick snapshot of the player stats"
}]
});
// Initialize the tour
tour.init();
}
// function that checks if the current tour has already been taken, and starts it if not
function checkShouldStartTour(){
var tourName = tour._options.name;
if(typeof tourLocalStorage[tourName] !== "undefined" && tourLocalStorage[tourName] === true){
// if we have detected that the tour has already been taken, short circuit
console.log('tour detected as having started previously');
return;
}else{
console.log('tour starting');
tour.start();
}
}
// updates localStorage with the current tour's name to have a true value
function tourHasBeenSeen(key){
tourLocalStorage[key] = true;
localStorage.setItem('myTour', JSON.stringify(tourLocalStorage));
}
function tourAgain(){
// if you want to tour multiple pages again, clear our localStorage
localStorage.removeItem('myTour');
// and if this is the first part of the tour, just continue below otherwise, send the user to the first page instead of using the function below
// if you just want to tour this page again just do the following line
tour.start();
}
activate();
});
PS. the reason we dont use onEnd to trigger the tourHasBeenSeen function is that there is currently a bug with bootstrap tour where if the last step's element doesnt exist, the tour ends without triggering the onEnd callback, BUG.
You could try using Javascript's sessionStorage, which is deleted when the user closes the tab, but survives through refreshes. Just use sessionStorage.setItem(key, value and sessionStorage.getItem(key). Remember that sessionStorage can only store strings!
Using your code:
<script type="text/javascript">
$(document).on('turbolinks:load', function() {
var tour = new Tour({
storage: false,
backdrop: true,
onStart: function(){
$('body').addClass('is-touring');
},
onEnd: function(){
$('body').removeClass('is-touring');
},
steps: [
{
element: "#navbar-logo",
title: "Go Home",
content: "All throughout the app, you can click our logo to get back to the main page."
},
{
element: "input#top-search",
title: "Search",
content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
}
]});
if(sessionStorage.getItem("loggedIn") !== "yes"){//Remember that sessionStorage can only store strings!
//Initialize the tour
tour.init();
// Start the tour
tour.start();
}
else{
//Set item "loggedIn" in sessionStorage to "yes"
sessionStorage.putItem("loggedIn", "yes");
}
var goBackToTour = function(e){
//You can also make a "fake" link, so that it looks like a link, but is not, and you don't have to put the following line:
e.preventDefault();
tour.init();
tour.start();
};
document.getElementById("goBackToTourLink").addEventListener("click", goBackToTour);
});
//On the logout
var logout = function(){
sessionStorage.setItem("loggedIn", "no");
};
</script>
You can store if user has seen the tour or not in the cookie. You can maintain a "TrackingCookie" which has all the user tracking information (eg. tour_shown, promotion_shown etc, which is accessed by your javascript
code. Following TrackingCookie code is to maintain all such tracking information in one cookie. I am calling it tracking_cookie.
Cookies can be accessed server-side using
cookies[:tracking_cookie]
tracking_cookie.js
var TrackingCookie = (function() {
function TrackingCookie() {
this.name = 'tracking_cookie';
this.expires = new Date(new Date().setYear(new Date().getFullYear() + 1));
}
TrackingCookie.prototype.set = function(name, value) {
var data={};
if(!this.readFromStore()) {
data = this.readFromStore();
}
data[name] = value;
return this.writeToStore(data);
};
TrackingCookie.prototype.set_if_unset = function(name, value) {
if (!this.get(name)) {
return this.set(name, value);
}
};
TrackingCookie.prototype.get = function(name) {
return this.readFromStore()[name];
};
TrackingCookie.prototype.writeToStore = function(data) {
return $.cookie(this.name, JSON.stringify(data), {
path: '/',
expires: this.expires
});
};
TrackingCookie.prototype.readFromStore = function() {
return $.parseJSON($.cookie(this.name));
};
return TrackingCookie;
})();
In your HTML
<script type="text/javascript">
$(document).on('turbolinks:load', function() {
//Instantiate the cookie
var tracking_cookie = new TrackingCookie();
//Cookie value not set means, it is a new user.
if(!tracking_cookie.get("tour_shown")){
//Set the value to be true.
tracking_cookie.set("tour_shown",true)
var tour = new Tour({
storage: false,
backdrop: true,
onStart: function(){
$('body').addClass('is-touring');
},
onEnd: function(){
$('body').removeClass('is-touring');
},
steps: [
{
element: "#navbar-logo",
title: "Go Home",
content: "All throughout the app, you can click our logo to get back to the main page."
},
{
element: "input#top-search",
title: "Search",
content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
}
]});
// Initialize the tour
tour.init();
// Start the tour
tour.start();
};
});
</script>
The cookie class is verbose. You can just use $.cookie to achieve simple one toggle behavior. The above code works for all first time users, logged-in as well as logged-out. If you just want it for logged-in user, set the flag on user log-in on server-side.
To use local storage:
if (typeof(Storage) !== "undefined") {
var takenTour = localStorage.getItem("takenTour");
if (!takenTour) {
localStorage.setItem("takenTour", true);
// Take the tour
}
}
We use this solution because our users don't log in, and it is a bit lighter than using cookies. As mentioned above it doesn't work when users switch machines or clear the cache, but you have that covered off by your login count.
Based on your comment, I think you're going to want to track this in your data (which is effectively what you're doing with the user.sign_in_count > 1 check). My recommendation would be to use a lightweight key-value data store like Redis.
In this model, each time a user visits a page that has this feature, you check for a "visited" value associated with that user in Redis. If it doesn't exist, you trigger the JS event and add "visited": true to Redis for that user, which will prevent the JS from triggering in the future.
Local storage is not a cross browser solution. Try this cross browser SQL implementation which uses different methods (including localstorage) to store 'databases' on the users hard drive indefinitely.
var visited;
jSQL.load(function(){
// create a table
jSQL.query("create table if not exists visits (time date)").execute();
// check if the user visited
visited = jSQL.query("select * from visits").execute().fetchAll("ASSOC").length;
// update the table so we know they visited already next time
jSQL.query("insert into visits values (?)").execute([new Date()]);
jSQL.persist();
});
This should work if what you want to do is gate the page for its life. If you need to prevent re-execution for longer periods, consider localStorage.
var triggered;
$(document).on('turbolinks:load', function() {
if (triggered === undefined) {
triggered = "yes";
...code...
}}
You're going to have to communicate with the backend somehow to get sign-in count. Either in a injected variable, or as json route you hit with ajax, do logic like:
if !session[:seen_tour] && current_user.sign_in_count == 1
#show_tour = true
session[:seen_tour] = true
else
#show_tour = false
end
respond_to do |format|
format.html {}
format.json { render json: {show_tour: #show_tour } }
end
Values in session will persist however you've configured your session store, by default that is stored in cookies.

Meteor, reactive array rendering issues on update

I have a nested template, using a ReactiveDict to store the data, which is an object that includes variables (color, type...) and an array of children nodes.
I'm having an issue on refresh: the array displays reactively, but when I update the array, it does not properly render.
in short (cleaned up code):
<body>
{{#with data}}
{{>nested}}
{{/with}}
</body>
<template name="nested">
<div>{{color}}<div>
<div class="ui dropdown">
<!-- drop down stuff goes here-->
</div>
{{#if children}}
{{#each children}}
{{>nested scope=this}}
{{/each}}
{{/if}}
</template>
Template.body.helpers({
"data": { color: "blue",
children: [{color: "green", children: [{color: "teal"}]},
{color:"red", children:[{color: "cyan"}],{color: "magenta"}]]}}
})
Template.nested.onCreated(function(){
this.scope = new ReactiveDict();
this.scope.set('scope', this.data.scope);
})
Template.nested.helpers({
"color": function () { Template.instance().scope.get('scope').color;},
"children": function () {
return Template.instance().scope.get('scope').children;
}
})
Template.nested.events({
"click .ui.dropdown > .menu > .item": function(e, t) {
e.preventDefault();
e.stopPropagation();
var data = t.scope.get('scope');
//do processing stuff here...
updatedArray = myFunction();
data['children'] = updatedArray;
t.scope.set('scope', data);
}
})
So what's happening is that on update the elements alreayd present do not update, and if there are elements added they show up.
if there are elements removed, they elements will be removed but the data in the variables (color here) does not get updated.
To make it work so far, I had to do the following:
Template.nested.events({
"click .ui.dropdown > .menu > .item": function(e, t) {
e.preventDefault();
e.stopPropagation();
var data = t.scope.get('scope');
//do processing stuff here...
updatedArray = myFunction();
delete data.children;
t.scope.set('scope', data);
Meteor.setTimeout(function() {
data['children'] = updatedArray;
t.scope.set('scope', data);
},10);
}
})
That works but it's a total hack forcing the array to nothing and then refreshing after a short timeout.
How am I supposed to do this the proper way?
PS: I tried using allDeps.changed() on the ReactiveDict, and i tried forcing a re-render but it's in the render loop so it won't render the view twice.
Can't seem to understand why the array elements are not updated. I know when using collections MiniMongo checks for _id's of the objects to see if they changed or not, but there are no _id in my objects. I also tried to add one but without much luck
well I guess I asked just before figuring it out...
the '_id' thing did the trick. I had tried before but I was actually using the same _id for the same elements so they did not appear to change (duh!)
So by adding a { "_id": Meteor.uuid() } in my generated objects, the update works fine.

Meteor: Hide or remove element? What is the best way

I am quite new with Meteor but have really been enjoying it and this is my first reactive app that I am building.
I would like to know a way that I can remove the .main element when the user clicks or maybe a better way would be to remove the existing template (with main content) and then replace with another meteor template? Something like this would be simple and straightforward in html/js app (user clicks-> remove el from dom) but here it is not all that clear.
I am just looking to learn and for some insight on best practice.
//gallery.html
<template name="gallery">
<div class="main">First run info.... Only on first visit should user see this info.</div>
<div id="gallery">
<img src="{{selectedPhoto.url}}">
</div>
</template>
//gallery.js
firstRun = true;
Template.gallery.events({
'click .main' : function(){
$(".main").fadeOut();
firstRun = false;
}
})
if (Meteor.isClient) {
function showSelectedPhoto(photo){
var container = $('#gallery');
container.fadeOut(1000, function(){
Session.set('selectedPhoto', photo);
Template.gallery.rendered = function(){
var $gallery = $(this.lastNode);
if(!firstRun){
$(".main").css({display:"none"});
console.log("not");
}
setTimeout(function(){
$gallery.fadeIn(1000);
}, 1000)
}
});
}
Deps.autorun(function(){
selectedPhoto = Photos.findOne({active : true});
showSelectedPhoto(selectedPhoto);
});
Meteor.setInterval(function(){
selectedPhoto = Session.get('selectedPhoto');
//some selections happen here for getting photos.
Photos.update({_id: selectedPhoto._id}, { $set: { active: false } });
Photos.update({_id: newPhoto._id}, { $set: { active: true } });
}, 10000 );
}
If you want to hide or show an element conditionaly you should use the reactive behavior of Meteor: Add a condition to your template:
<template name="gallery">
{{#if isFirstRun}}
<div class="main">First run info.... Only on first visit should user see this info.</div>
{{/if}}
<div id="gallery">
<img src="{{selectedPhoto.url}}">
</div>
</template>
then add a helper to your template:
Template.gallery.isFirstRun = function(){
// because the Session variable will most probably be undefined the first time
return !Session.get("hasRun");
}
and change the action on click:
Template.gallery.events({
'click .main' : function(){
$(".main").fadeOut();
Session.set("hasRun", true);
}
})
you still get to fade out the element but then instead of hiding it or removing it and having it come back on the next render you ensure that it will never come back.
the render is triggered by changing the Sessionvariable, which is reactive.
I think using conditional templates is a better approach,
{{#if firstRun }}
<div class="main">First run info.... Only on first visit should user see this info.</div>
{{else}}
gallery ...
{{/if}}
You'll have to make firstRun a session variable, so that it'll trigger DOM updates.
Meteor is reactive. You don't need to write the logic for redrawing the DOM when the data changes. Just write the code that when X button is clicked, Y is removed from the database. That's it; you don't need to trouble yourself with any interface/DOM changes or template removal/redrawing or any of that. Whenever the data that underpins a template changes, Meteor automatically rerenders the template with the updated data. This is Meteor’s core feature.

Categories