This Meteor client code is expected to show a progress bar animation when a button is clicked. The problem is that the progress element is null when the animation code tries to use it.
How can it be fixed? thx
Template.registerHelper('session', (value) => {
return Session.get(value);
})
Template.footer.events({
'click button[data-action=carSearch]': function (e) {
e.preventDefault();
clickInfo();
}
});
'clickInfo': function () {
Session.set('inProgress', true); // makes the element visiable
progressBarStart(); // start the animation
}
'progressBarStart': function () {
let bar = document.getElementById('bar'); //<-- bar is always null
if (bar.value < 70) {
setTimeout(lib.progressBarStart(), 1000); controls the speed
bar.value += 1;
}
}
<template name="nameSearch">
<div id="nameSearch">
<p id="result" data-id={{_id}}>
{{{display.result}}} <br>
{{#if (session 'inProgress')}}
<progress id="bar" value="0" max="70"></progress>
{{/if}}
</p>
</div>
</template>
You should give the render a time to re-render after calling Session.set('inProgress', true);, 50ms will be enough.
Next, you should declare clickInfo and progressBarStart correctly, not like you have them now.
function clickInfo() {
Session.set('inProgress', true);
Meteor.setTimeout(progressBarStart, 50);
}
function progressBarStart() {
...
}
I'd also recommend to use dedicated helper to obtain inProgress status:
Template.footer.helpers({
isInProgress: function() {
return Session.equal('inProgress', true);
}
});
And use it in your template:
{{#if isInProgress}
...
{{/if}}
You must not use Session variables for such a small reason. Meteor JS has given a facility to use Reactive Variables at template level manipulations.
You should give the render a time to re-render after calling Template.instance().inProgress.set(true);.
I'd also recommend to use dedicated helper to obtain inProgress status:
Template.footer.onCreated(function(){
this.inProgress = new ReactiveVar(false);
});
Template.footer.helpers({
isInProgress: function() {
return Template.instance().inProgress.get();
}
});
And use it in your template:
{{#if isInProgress}
...
{{/if}}
Related
How can I render two different views in a one page app without changing URLs. I'm using meteor with the default blaze as well as the flow:router package. Right now I have it set up like this:
routes.js..
FlowRouter.route("/", {
name: "App.home",
action() {
BlazeLayout.render("App_body", {
main: "App_home",
mainContent: "calendar"
});
}
});
FlowRouter.route("/list", {
name: "App.list",
action() {
BlazeLayout.render("App_body", { main: "App_home", mainContent: "list" });
}
});
but this way I'm using the url /list and i dont want that. I would like to simply render an alternate component template in the same url. I'm very new to coding so forgive me if this is obvious. Essentially I just want two different view styles: a list and a calendar. So I would like a way to set it up so that a spacebars template can be rendered if a certain button is clicked, and a different one can be rendered instead if another button is clicked.
Thanks so much for any help, i've been at this for a couple of days :)
Create another template, which renders particular view conditionally. Something like this:
FlowRouter.route("/", {
name: "App.home",
action() {
BlazeLayout.render("App_body", {
main: "App_home",
mainContent: "listOrCal"
});
}
});
<template name="listOrCal">
{{#if showList}}
{{> list}}
{{else}}
{{> calendar}}
{{/if}}
<button id="switchView">Switch view</button>
</template>
Template.listOrCal.onCreated(function listOrCalOnCreated() {
this.showList = new ReactiveVar(true);
})
Template.listOrCal.helpers({
showList() {
return Template.instance().showList.get();
}
})
Template.listOrCal.events({
'click #switchView' {
let showList = Template.instance().showList.get();
Template.instance().showList.set(!showList);
}
})
You can handle this within a single template like so:
FlowRouter.route('/', {
name: 'App.home',
action() {
BlazeLayout.render('App_body', { main: 'App_home', mainContent: 'ListOrCalendar' });
}
And then the ListOrCalendar template would look like this:
{{#if displayList}}
{{> List}}
{{else}}
{{> Calendar}}
{{/if}}
<button>Switch</button>
You would set up a ReactiveVar in the ListOrCalendar template:
Template.ListOrCalendar.onCreated(function() {
const instance = this;
instance.displayList = new ReactiveVar(true);
});
See ReactiveVar explanation here (ignore Session)
Then you would have a helper which returns the value of your ReactiveVar:
Template.ListOrCalendar.helpers({
displayList() {
const instance = Template.instance();
return instance.displayList.get();
}
});
Finally, you would hook up an event to change the value of displayList to switch between templates:
Template.ListOrCalendar.events({
"click button"(event, instance) {
const displayListCurrent = instance.displayList.get();
const displayListNew = !displayListCurrent;
instance.displayList.set(displayListNew);
// or, more concisely, instance.displayList.set(!instance.displayList.get());
}
});
So, in summary:
When the template is created, your ReactiveVar is true
so your displayList returns true
so the #if displayList condition in the template is satisfied
and so the List template is displayed
When the button is clicked
The ReactiveVar is set to false
so the displayList helper returns false
so the #if displayList condition in the template is not satisfied and it goes to the else statement
and so, finally, the Calendar template is displayed
When the button is clicked again, the ReactiveVar is toggled back to true, and on we go as above
This might seem daunting or over-complicated, but there's nothing fancy going on here at all. You'll get used to it pretty quickly
How to get attribute value from tag like width, color, value ...
<template>
{{#if body.width > 979px }}
{{> tmp1 }}
{{else}}
{{> tmp2 }}
{{/if}}
</template>
<template name="tmp1">...</template>
<template name="tmp2">...</template>
You can't access tag attributes directly from Spacebars template. You need to create a template helper for that.
Template.templateXY.helpers({
bigBody: function() {
return $("body").width() > 979;
}
});
then you use it like this:
<template name="templateXY">
{{#if bigBody}}
{{> tmp1}}
{{else}}
{{> tmp2}}
{{/if}}
</template>
UPDATE: for the helper to recompute on window resize event, you need to modify it a little bit. You can use a Dependency object for this.
Template.templateXY.onCreated(function() {
// create a dependency
this.resizeDep = new Dependency();
});
Template.templateXY.onRendered(function() {
let tmpl = this;
// invalidate the dependency on resize event (every 200ms)
$(window).on("resize.myEvent", _.throttle(function() {
tmpl.resizeDep.changed();
}, 200));
});
Template.templateXY.helpers({
bigBody: function() {
// recompute when the dependency changes
Template.instance().resizeDep.depend();
return $("body").width() > 979;
}
})
Template.templateXY.onDestroyed(function() {
$(window).unbind("resize.myEvent");
});
Other possible solution would be to store the window width to a ReactiveVar (which is a reactive data source itself) and use .on("resize", fn) to change the ReactiveVar
After some researches I found that there's no spacebars proper solution and the best option is to use js code.
So here's the code:
Session.set("width", $(window).innerWidth());
window.onresize = function () { Session.set("width", $(window).innerWidth()); };
if(Meteor.isClient) {
Template.body.helpers({ 'dynamicTemplateName': function () { return Session.get("width"); } });
}
I have chat app, and if you use Slack you know that when you enter to the room, you will automatically find yourself at the bottom of your chat room.
So I decided to do this and what I have
Template.room.onCreated(function() {
console.log($(document).height(),$(window).height());
$('html, body').scrollTop($(document).height()-$(window).height());
});
it output 437 , 437
BUT when I do this in console:
$('html, body').animate({ scrollTop: $(document).height()-$(window).height() + 64}, "fast");
it outputs 2000,437 , and that means that my messages is not loaded fully in my template. (If someone want more code, just ask.)
Any idea how to build this ?
EDIT:
This part of template.html
<div class="messages-wrap">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
{{#if haseMoreMessages}}
<div class="loadmore text-center" id="incLimit">Load More</div>
{{/if}}
{{#if Template.subscriptionsReady }}
{{#each messages}}
{{> message}}
{{/each}}
{{/if}}
</div>
</div>
</div>
</div>
And template.js (part of it)
Template.room.onRendered(function() {
Session.setDefault('messageLimit', 200);
});
Template.room.onCreated(function() {
var self = this;
self.autorun(function() {
if (self.subscriptionsReady()) {
Tracker.afterFlush(function () {
$('html, body').scrollTop($(document).height() - $(window).height());
});
}
});
});
Template.room.events({
'click #incLimit' : function(e,template){
Session.set('messageLimit',Session.get('messageLimit') + 100);
}
});
Template.room.helpers({
messages: function(){
return Messages.find({},{sort:{createdAt:1}});
},
haseMoreMessages:function(){
if (Session.get('messageLimit') > Messages.find().count()) return false;
return true;
},
});
This is one very frustrating aspect of Blaze. Try this, though:
Template.room.onRendered(function () {
var template = this;
this.autorun(function () {
if (template.subscriptionsReady()) {
Tracker.afterFlush(function () {
$('html, body').scrollTop($(document).height() - $(window).height());
});
}
});
});
This waits till all the template subscriptions are ready first, and then waits till any computations are fully complete (Tracker.afterFlush), and then executes the scroll. Tracker.afterFlush is usually necessary if your template has {{#if}} blocks that depend on other things before they get evaluated and rendered.
UPDATE:
Without seeing all your code and knowing why or when you want to scroll to the top, it's hard to say what you're aiming for. But see the Meteorpad link below for a working version of what you were trying to do with the message limits (I'm only incrementing the limit by 1 since there are 3 messages).
Meteorpad Link
A few things you should note:
Set default variables and subscribe to things in Template.x.onCreated, not Template.x.onRendered.
You forgot to actually subscribe to your collection.
Limit messages on the server side, in the Meteor.publish callback.
Different Approach via tracking of Template helpers:
I took advantage of the Template helper, as it already tracks reactively all changes (and new) messages. Hence, there you can place your scroll-down command.
Template.room.helpers({
messages: function(){
//put your scroll-up command here
//as this helper is keeping track on changes already
$('html, body').scrollTop($(document).height() - $(window).height());
return Messages.find({},{sort:{createdAt:1}});
},
haseMoreMessages:function(){
if (Session.get('messageLimit') > Messages.find().count()) return false;
return true;
},
});
Hence, you save the resources to have an additional tracker observing the collection (you would be double-tracking the same collection).
I have a template helper
multiple_emails: ->
Meteor.user().emails.length > 1
It is not rerun when objects are added or removed from the emails array. Why is this? How should I write this helper to that it is rerun?
Something does not work somewhere else in your code.
I made you a small pad : http://meteorpad.com/pad/B6L3cCXAPwPSdqc5s
Template :
<head>
<title>Leaderboard</title>
</head>
<body>
{{> loginButtons}}
{{#if currentUser}}
{{> addEmail}}
{{/if}}
</body>
<template name="addEmail">
<button>addEmail</button>
nb: {{emails}}
</template>
Client Js :
Template.addEmail.events({
'click button': function() {
Meteor.users.update({
_id: Meteor.userId()
}, {
$addToSet: {
emails: {address: 'email' + Math.random()}
}
})
}
});
Template.addEmail.helpers({
emails: function() {
return Meteor.user().emails.length;
}
});
You'll need the accounts-base, password and ui packages.
Signup with an email and click le addemail button. As you can see, when clicking on the button, it updates the number (and quickly goes back to 1 because you cannot update it that way, but this is just to make a general point).
Most times when a collection does not appear to be reactive on the client side, there is an issue/mistake somewhere in the pub/sub portion.
If I have an {{# each}} binding in Meteor, and I want to update a property on only one instance of the template inside the #each. How would I do that? I've tried setting a value on the "template" object inside the events map, but that doesn't seem to be reactive. I've also tried binding to a Session property, but that will cause every instance to update instead of just the one I want...
for example:
{{#each dates}}
{{> dateTemplate}}
{{/each}}
<template name="dateTemplate">
{{date}}
<span style="color: red;">{{errorMsg}}</span> <--- how do i update errorMsg?
</template>
Template.dateTemplate.events({
'click': function(event, template) {
template.errorMsg = 'not valid'; <--- this doesn't do anything
}
});
EDIT TO ADDRESS ANSWER BELOW:
Template.dateTemplate.events({
'click': function(event, template) {
template.errorMsg = function() { return 'not valid';} <--- this also doesn't do anything
}
});
You don't have to use handlebars for this, because its not something that needs reactivity to pass the message through, reactive variables work best with db data, or data that would be updated by another client over the air.
You could use JQuery (included by default) to update it, it can also get a bit fancier:
<template name="dateTemplate">
{{date}}
<span style="color: red;display: none" class="errorMessage"></span>
</template>
Template.dateTemplate.events({
'click': function(event, template) {
$(template.find('.errorMessage')).html('Your Error Message').slideDown();
}
});
Ive edited it so the error is hidden by default, and slides down with an animation
I'm experimenting handling this by passing a different reactive object to each instance of the template. Then the template can bind to the reactive object (which is unique per instance) and we don't have any extra boilerplate.
It ends up looking like this:
Initial render:
Template.firstTemplateWithPoll(ContextProvider.getContext())
Template.secondTemplateWithPoll(ContextProvider.getContext())
// (I actually pass getContext an identifier so I always get the same context for the same template)
JS:
Template.poll.events = {
'click .yes' : function() {
this.reactive.set('selection', 'yes');
},
'click .no' : function() {
this.reactive.set('selection', 'no');
}
};
Template.poll.selection = function(arg) {
return this.reactive.get('selection');
}
Template:
<template name="poll">
<blockquote>
<p>
Your selection on this poll is {{selection}}
</p>
</blockquote>
<button class='yes'>YES</button>
<button class='no'>NO</button>
</template>
template.errorMsg should be a function that returns your error.
Template.dateTemplate.events({
'click': function(event, template) {
template.errorMsg = function() { return 'not valid'; };
}
});