I'm using a simple template block helper that displays the a loading animation, hiding whatever is within it. This is very helpful for submitting forms and preventing the button from being pressed twice.
<template name="waiting">
{{#if isLoading}}
{{> loader}}
{{else}}
{{> UI.contentBlock}}
{{/if}}
</template>
I decided to put it in a block helper because I've never tried it before and I wanted to keep my code DRY -- I'm using this pattern in several places.
I'm running into issues using this block helper to access the Template.instance() not of the block helper but of the template it is within. I'm also running into problems with the template context. Here are the details of my problem:
I'm setting up a confirm-delete button like so:
{{#waiting isLoading=loadingDelete}}
{{#if confirmDelete}}
<button class="confirm">Are you sure?</button>
{{else}}
<button class="delete">Delete</button>
{{/if}}
{{/waiting}}
I'm also putting the loadingDelete and confirmDelete reactive variables in the template instance so I don't contaminate the Session:
Template.editRecord.created = ->
#loadingDelete = new ReactiveVar(false)
#confirmDelete = new ReactiveVar(false)
Template.editRecord.helpers
loadingDelete: () -> Template.instance().loadingDelete.get()
confirmDelete: () -> Template.instance().confirmDelete.get()
Then I create some events like this:
'click .delete': (e,t) ->
t.confirm.set(true)
'click .confirm': (e,t) ->
t.loadingDelete.set(true)
_id = #_id
Meteor.call 'deleteRecord', _id, (err) ->
t.loadingDelete.set(false)
if err
console.log err
else
Router.go "home"
The problem with this implementation is that the Template.instance() refers to the waiting template in the confirmDelete helper here:
confirmDelete: () -> Template.instance().confirmDelete.get()
The second problem is that the data context in the event refers to the context of the waiting template and thus I need to use _id = Template.parentData()_id instead of _id = #_id. But I am not able to access the editRecord template instance or any of its variables from here which is frustrating.
I changed the template to this which fixes first problem.
{{#if confirmDelete}}
<button class="confirm">Are you sure?</button>
{{else}}
{{#waiting isLoading=loadingDelete}}
<button class="delete">Delete</button>
{{/waiting}}
{{/if}}
But this is ugly and not what I was intending. I was hoping just to replace my #if block with this block helper but I've ended up writing much more code while trying to be DRY with my templates. This just doesn't seem right.
I noticed that #if blocks don't cause these context and Template instance problems. Is there something I am missing?
So it turns out that the Meteor team has recognized this error and are working to fix it. See this PR.
Related
I have the following templates (.html) with their respected managers (.js files):
adminManageCategories
adminAddCategory
adminUpdateCategory
Consider the following:
<template name="adminManageCategories">
{{#each category}}
<div class="clickme">{{title}}</div>
{{/each}}
{{> adminUpdateCategory}}
</template>
Notice the {{> adminUpdateCategory}} is outside of the iteration. This is also a form, and I want to keep it on the same page.
And admin_manage_categories.js
Template.adminManageCategories.events({
"click .clickme": function(event) {
event.preventDefault();
console.log(this._id);
}
});
Notice the console.log() function, which works, as the template manager is smart enough to know the ID of the item that was clicked.
What I want to do is load this items values into the form when clicked. My example above is slim, but in my real data I have a title, sort order, among other things.
So my question is, what is the proper way to pass the _id from the adminManageCategories template to the adminUpdateCategory template, which is the form?
I can hack at this with JavaScript and make things happen, but I think I'm missing a "Meteor way" of doing things.
You need to use a ReactiveVar to store the currently clicked item.
First you need to run meteor add reactive-var, as it's not a package added by default in a standard meteor web app.
JS:
Template.adminManageCategories.created=function(){
// instantiate the reactive-var in the created callback
// we store it as a property of the template instance
this.currentItemId=new ReactiveVar(null);
};
Template.adminManageCategories.helpers({
// this helper reactively returns the currently clicked item
currentItem:function(){
// retrieve the reactive-var from the template instance...
var currentItemId=Template.instance().currentItemId.get();
// ...to fetch the correct collection document
return Items.findOne(currentItemId);
}
});
Template.adminManageCategories.events({
"click .clickme": function(event,template) {
event.preventDefault();
// assign the correct item id to the reactive-var attached to this template instance
template.currentItemId.set(this._id);
}
});
HTML:
<template name="adminManageCategories">
{{#each category}}
<div class="clickme">{{title}}</div>
{{/each}}
<p>Current item title is : {{currentItem.title}}</p>
{{! pass the currentItem as a parameter to your child template this will be
accessible as {{item}} in the HTML and "this.item" in JS helpers or
"this.data.item" in created/rendered/destroyed callbacks}}
{{> adminUpdateCategory item=currentItem}}
</template>
EDIT:
When I initialize the reactive-var in the created callback, I set it to null, this means that until one item is clicked, the helper will return null too and when you'll try to access this.item._id in the adminUpdateCategory this will fail.
The simplest way to solve this issue is maybe to not initialize the variable to null but to the first item in the collection.
Template.adminManageCategories.created=function(){
var firstItem=Items.findOne({},{
sort:{
sortedField:1
}
});
this.currentItemId=new ReactiveVar(firstItem && firstItem._id);
};
There may still be a case when you have 0 items in the collection, so you'll probably end up having to guard against the existence of the item in the JS.
Template.adminUpdateCategory.helpers({
itemProperty:function(){
return this.item && this.item.property;
}
});
I am working on an edit form that has two paths. One is when the user clicks a "New" button, the other is when they click "Edit".
When they click "New", the code sets a form_id Session var to null and a client_id session variable to null, then does a Router.go('formEdit') to load the formEdit template/route.
In the formEdit.js, I do a reactive Template helper (I think that's what they are called, but anyway) like so:
Template.formEdit.form = function() {
var form;
if (Session.equals('form_id', null)) {
// Create empty form
form = {
title: null,
client_id: Session.get('client_id'),
header_fields: [],
form_fields: []
};
} else {
// Load form
form = Forms.findOne({_id: Session.get('form_id')});
}
return form;
}
Basically I check if the form_id was set or not, if so I load it from the Forms collection, if not I create a blank one. I thought this would be pretty simple, really.
The problem is that the created/found form object does not behave in a "reactive" way. If I add header_fields or form_fields the subsequent template code never updates. Both are in a {{#each}} like so:
<template name="formEdit">
...
{{#each header_fields}}
{{> headerFieldOutput}}
{{/each}}
...
{{#each form_fields}}
{{> formFieldOutput}}
{{/each}}
</template>
How do I make it such that I can push header_fields and form_fields onto the form and have the underlying template reactively update the {{#each}}'s?
I think you're going about it a little differently than what the reactive programming methodology in Meteor is expecting.
You're putting the 'display' logic in your template helper, rather than using the template scaffolding itself to do it.
So, declare a very simple template helper, something like this:
Template.formEdit.form = function () {
return forms.findOne(Session.get("form_id"));
};
And then, in your template scaffolding have something like this:
{{#if form}}
{{#with form}}
{{#each header_fields}}
etc...
{{/with}}
{{#else}}
[[insert your blank form scaffolding in here]]...
{{/if}}
Then, as you set your Session form_id variable, you can set it to null to invoke the {{#else}} portion.
There are more details than this (logic in the form submit click handler to identify if you are performing an update or an insert, for example) but hopefully you get the gist of it from this.
You should try to gain a better understanding about how cursors and reactive computations work, as it will help you better understand how to best use the reactive methodology. A good starting place is the parties example (watch the video and walk through the code manually). It's similar to what you're doing, and shows a good way of building your templates for when you don't have a 'selected' object.
Hope this helps!
Previously I used the rendered function to initialize videoJS when the Video Changed.
Now in Meteor 0.8. the rendered is only fired once, when the template was created.
The problem is: the template is used in each page, I navigate through, so it is never rerendered and just changed the attributes.
Is there any way to get VideoJS working in Meteor 0.8?
Something like rendered function for template fields that fires always
or a way to force the complete template to rerender
or a way to run a callback when the content of a helper is inserted.
I have found a solution to force the behaviour of Meteor < 0.8:
Make 2 Handlers:
UI.registerHelper "handler1", (data) ->
Session.set("loading", true);
UI.registerHelper "handler2", (data) ->
if Session.get("loading")
Session.set("loading", false);
return true
else
return false
Define 2 Template:
<template name="videoPlayer">
{{handler1 seed="videourl"}}
{{#if handler2}}
Loading Player...
{{else}}
{{> videoPlayerInternal}}
{{/if}}
</template>
<template name="videoPlayerInternal">
<!-- VideoJS-Player-Code -->
</template>
Now, I can hook the rendered method of videoPlayerInternal and this works like meteor < 0.8.
This works because:
In Initialization, only handler1 is called. This sets the variable "loading" to true.
After that I have a if-then-else that in the initial state shows a loading text. Because the condition is calculated by a helper that has a side effect (setting the session variable loading to false) it will be called immediatly after that.
So If I load another exactly same page with another video url, it needs to re run the handler1, wich sets the session variable to true again, that deletes the template from the dom and reinserts that. The rendered is called again and voila it works ;-)
Oh... and of course, the rendered method:
Template.videoPlayerInternal.rendered = ->
videojs.options.flash.swf = "/video-js.swf"
# Delete old players and reset.
if videojs.players.videoJsPlayer
videojs.players.videoJsPlayer.dispose()
# Initialize the player
$vid_obj = _V_ "videoJsPlayer", {}, ()->
console.log "video is ready.";
return true
My template is getting rendered twice on first load. I notice this because in
Template.home.rendered = function() {
console.log('rendered'); // => this is printed out twice
console.log(Data.find({}).count()); // => this is initially 0, then 1 the second time
}
Furthermore, on the first load, no Data is available. Yet on the second load, the Data is there.
Does anyone know what this problem might be, and why the data only appears the second time?
You need to find a way to render the template when your data is available.
Using this template structure, the if block content, which happens to be the template displaying your data, will be rendered only when the myDataIsReady helper returns true. (thus triggering the rendered callback only once, with data immediately available).
<template name="displayData">
<p>This is my data : {{this}}</p>
</template>
<template name="home">
{{#if myDataIsReady}}
{{#each data}}
{{> displayData}}
{{/each}}
{{/if}}
</template>
You have to define a subscription handle (an object returned by Meteor.subscribe) in order to use it's reactive ready method : we'll reference it in the myDataIsReady helper to track data availability, and the helper will automatically rerun when the state of ready changes.
Meteor.startup(function(){
// this subscription should return your data subset
myDataHandle=Meteor.subscribe("myData");
});
Template.home.myDataIsReady=function(){
return myDataHandle.ready();
}
Template.home.data=function(){
return Data.find({});
}
But that's quite annoying for such a simple task.
That's why I suggest using the Iron Router which makes things way simpler !
Add it to your project using "mrt add iron-router", then in a client/router.js and client/router.html, use this boilerplate code :
Router.configure({
loadingTemplate:"loading"
});
Router.map(function(){
this.route("home",{
path:"/",
// we indicate which subscription has to be marked ready in order to load the template
waitOn:function(){
return Meteor.subscribe("myData");
}
// the result of this function will become our target template data context
data:function(){
return Data.find({});
}
});
});
<template name="home">
{{#each this}}
{{> displayData}}
{{/each}}
</template>
<template name="loading">
<p>Data isn't ready yet...</p>
</template>
As you can see, the Iron Router allows us to specify simply what we painfully achieved manually in the first code example (waiting on a particular subscription to render a template), and of course we get free routing, loading mechanisme, layout management, etc...
Search the web for a complete iron-router tutorial (my code is untested, but I hope it is ok and should get you started), it's so awesome that it's gonna be merged to Meteor ultimately.
I had a body.html in /client and a appBody.html in /client/templates, with, in iron router:
Router.configure({
layoutTemplate: 'appBody',
});
Both body templates were rendered (and happened to be the same). Obviously, the body.html in /client needed to be removed.
I am new to Meteor and wondering how to solve what seems to me is a common problem.
Let's say I have a handlebars template listing restaurants:
<template name="Restaurants">
{{#each Restaurant}}
{{name}}
{{/each}}
</template>
Now when user clicks on a restaurant template I want to display a menu for that restaurant.
I added a subtemplate named "menuItems" that contains all menu items for a given restaurant:
<template name="Restaurants">
{{#each Restaurant}}
{{name}}
{{> menuItems}}
{{/each}}
</template>
I want to render only one instance of menuItems subtemplate when user clicks anywhere on Restaurant template (render only the menu items for the selected restaurant).
It should go something like:
Template.Restaurants.events({
'click' : function (e) {
// This is where I need help - what's the right way to display only one subtemplate instance?
}
});
My question is - how I can select and display only the correct menuItems template instance?
Also I would like to place menuItems template instance in DOM only after the click and not before (having all the menu items for all restaurants and only hiding those divs is not an option because of high number of those items in db).
If you think I should approach the solution in some other way please let me know, thanks!
You should use {{#if}} and Session. Like this:
<template name="Restaurants">
{{#each Restaurant}}
{{name}}
{{#if restaurantSelected}}
{{> menuItems}}
{{/if}}
{{/each}}
</template>
By using Session, a reactive data source, you can set a global flag indicating whether a restaurant is selected.
Template.Restaurants.restaurantSelected = function() {
// check whether this restaurant is selected. "this" refers to the current
// context, eg. the current restaurant in the loop
return Session.equals("restaurantSelected", this._id);
}
Whenever you change that session key, the value will update and the template will be redrawn. So, you can toggle it when clicking a restaurant:
Template.Restaurants.events({
'click' : function (e) {
// store the current restaurant ID
// make sure the event selector is correct!
Session.set("restaurantSelected", this._id);
}
});
Edit For clarity's sake I created a complete example that you can copy into your project and try out.
I almost always avoid Session. I think it pollutes the global scope. Also it prevents you from running multiple instances of the template. I recommend using a reactiveVar or reactiveDict scoped to the template instance. Thanks to Rahul for starting a demo project. I took his example and modified it to show my recommended approach.
attach a reactiveDict to the template instance onCreate. Use this to store state instead of global Session var!
Template.Restaurants.onCreated(function() {
this.state = new ReactiveDict;
this.state.set('currentRestaurant', null); // could set a init value here
});
this event handler will set the state of the reactiveDict on click
'click': function(e, t) {
t.state.set('currentRestaurant', this._id);
}
this helper is used to show/hide the menu template
currentRestaurant: function() {
// check whether this restaurant is selected. "this" refers to the current
// context, eg. the current restaurant in the loop
return Template.instance().state.equals("currentRestaurant", this._id);
},
menu template receives the selected id from data context instead of from Session
<template name="Restaurants">
<ul>
{{#each Restaurant}}
<li>
{{name}}
{{#if currentRestaurant}}
{{> menuItems restaurant=_id}}
{{/if}}
</li>
{{/each}}
</ul>
</template>
<template name="menuItems">
<ul>
<li class="menu">I'm a menu for {{restaurantName}}!</li>
</ul>
</template>
added this helper just to show we really got the id
Template.menuItems.helpers({
restaurantName: function() {
var restaurantMenu = Restaurants.findOne(this.restaurant);
return restaurantMenu.name;
},
})
Posted a fully working project to github.
https://github.com/white-rabbit-japan/scopedReactivityDemo
App is hosted on meteor.com
http://scopedreactitivydemo.meteor.com/