Pass context to yield when using Meteor Iron Router - meteor

I am starting to use Iron Router in my Meteor app and its yields for templating.
I've recently run into a problem where I can't start a named yield with a context, as follows:
{{#with context}}
{{yield 'subtemplate'}}
{{/with}}
and get this error Sorry, couldn't find a yield named "subtemplate". Did you define it in one of the rendered templates like this: {{yield "subtemplate"}}?
If I remove the {{#with}} block expression, I am able to render the yield.
Does anyone know of a good way to pass the context to a named yield?
I have posted my problem as an issue on the iron-router github project, but haven't gotten any solution yet.
Would appreciate any help.
EDIT 1/1/2014:
So my code looks like this:
// main template for the route
<div class="form-container">
<div class="form">
{{yield 'section'}}
</div>
</div>
The logic to get the yield section to display
// router.js
ApplyController = RouteController.extend({
before: function() {
var section = this.params.section || 'personal-info';
Session.set('current', section);
},
action: function() {
var section = Session.get('current');
this.render();
this.render(section, {
to: 'section'
});
},
data: function() {
return {
app: Applications.findOne({user: Meteor.userId()})
}
}
});
Example of one of the section template:
<template name="education">
{{#with app}}
<form id="education" name="education" class="fragment" method="post" action="">
<h2>Education</h2>
<div class="form-group">
<label for="college" class="control-label">College/ University</label>
<select class="form-control" id="college" name="college" placeholder="Select a College/ University">
<option value="">Select a College/ University</option>
{{#each colleges}}
<option value="{{slug}}" {{selected slug ../college}}>{{name}}</option>
{{/each}}
</select>
</div>
<!-- other content here -->
</form>
{{/with}}
</template>
Using the {{#with app}} block is how I currently get around this issue, but because I have 10 different section templates, I have to put that in all of them.

You pass a data context in the router using ironrouter. You can't pass it this way because if you pass a route in the router it would override the route's data context.
It might however work with the shark branch of ironRouter which is based off Meteor UI since it uses {{>yield}} instead of {{yield}}.
You can use this though:
Route specific data context
Router.map(function() {
this.route('template', data: function() { return Something.find() });
});
You basically pass the context using the data param. It might be easier to do it this way than to use {{#with context}} because you can use more dynamic data which is different for each route.
You might have tried this, I'm a bit unsure on whether it would go to a named yield's template.
Using an ordinary Template helper for the template
Template.templateInYieldName.helper = function() {
return Something.find();
}
Then you can use something like {{helper.name}} in your named yield.
Global data context with handlebars helper
If you intend to use data for all the routes you can use a Handlebars global helper. i.e
Handlebars.registerHelper('todaysDate', function() {
return (new Date).toString();
});
then just use {{todaysDate}} in any of your templates. You can use your data instead of a date instead.

Related

This.data from #each-iteration

I'm trying to access a value inside an {{#each in}}-iteration:
{{#each room in channels}}
<form class="enterRoom">
<button type="submit" class="roomJoin">
<b>{{room.name}}</b>
<img src="{{room.roomBanner}}" alt=".">
<input type="hidden" value="{{room.name}}" name="name">
</button>
<div class="inRoom">
{{#each name in room.inRoom}}
{{name}}
{{/each}}
</div>
</form>
{{/each}}
Normally I would use this.name, for example, to get the name of it inside an event to use it further, like so
'submit .enterRoom'(event) {
event.preventDefault();
const isClosed = this.name; // room.name example here
}
But this doesn't work in this scenario. What I tried before was:
room.name
this.room.name
But those give the same error
chat.js:86 Uncaught ReferenceError: room is not defined
at Object.submit .enterRoom (chat.js:86)
at blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:3818
at Function.Template._withTemplateInstanceFunc (blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:3769)
at Blaze.View.<anonymous> (blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:3817)
at blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:2617
at Object.Blaze._withCurrentView (blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:2271)
at Blaze._DOMRange.<anonymous> (blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:2616)
at HTMLFormElement.<anonymous> (blaze.js?hash=51f4a3bdae106610ee48d8eff291f3628713d847:863)
at HTMLDivElement.dispatch (modules.js?hash=8331598f8baf48556a442a64933e9b70b778274a:9685)
at HTMLDivElement.elemData.handle (modules.js?hash=8331598f8baf48556a442a64933e9b70b778274a:9492)
Could someone explain to me how I could do it in this {{each in}}-setting properly?
The error has nothing to do with the each iterations of your template. What you try is to get the form data within the submit event handle. However, there is no context bound to this or room.
In order to get the room value, you need to access the input value.
Blaze offers a fast way of doing so, by using the Template's builtin jQuery (using templateInstance.$), which automatically scopes to the Template root instead of the whole document:
'submit .enterRoom'(event, templateInstance) {
event.preventDefault();
const roomName = templateInstance.$(event.currentTarget).find('input[name="name"]').val();
// ...
}

Meteor: single post view, via id and flow router is not parsing any data from the collection

Im trying to open a single record from my #each loop of items into its own view by clicking a link that says 'see more', which will take me to the single article. I set up my Flow router and its working but i cannot see the data that's supposed to come in.
the template helper for the single article looks like this
Template.collectionSingle.helpers({
articles: function () {
var id = FlowRouter.getParam('_id')
return theCollection.findOne({_id: id});
}
});
}
my route looks like this
FlowRouter.route('/collection/:_id', {
name: 'collection',
action() {
BlazeLayout.render("AppLayout", {main: "collectionSingle"});
}
});
and the template "collectionSingle" looks like this
<template name="collectionSingle">
<h1>{{title}}</h1>
<h1>This is a test</h1>
<img src="{{thumbnail}}" alt="" />
</template>
when i navigate to http://localhost:3000/collection/thePublication-0
all i see is the test message 'This is a test', but i don't see the {{title}} nor the thumbnail.
furthermore, when i change:
return theCollection.findOne({_id: id});
to another one of my collections :
return OtherCollection.findOne({_id: id});
http://localhost:3000/collection/thePublication-0
remains the same.
how can i successfully have a single articles page for each of my articles and have them linked properly with flow router?
You need to actually use your template helper that is returning the data context:
<template name="collectionSingle">
<h1>{{title}}</h1>
<h1>This is a test</h1>
{{#with articles}}
<img src="{{thumbnail}}" alt="" />
{{/with}}
</template>
Since your articles helper returns a single document you use {{#with articles}}. If it returned a cursor or array you would loop over that with {{#each articles}}. The normal convention is to use the singular form for the former, the plural for the latter.

Meteor.js Iron Routing :_id dynamic route confusion

I'm currently working my way though "Your Second Meteor Application" and have been enjoying it so far. Everything I have created works but I do not understand why the following works but the code at the end does not.
Template
<template name="list">
<ul>
{{#each list}}
<li>{{name}}</li>
{{/each}}
</ul>
</template>
<template name="listPage">
<h2>Tasks: {{name}}</h2>
</template>
Route
Router.route('/list/:_id', {
template: 'listPage',
data: function(){
var currentList = this.params._id;
return Lists.findOne({_id: currentList});
}
});
This is giving the expected results. However, I was curious why the following will not work as it seems to be passing the exact same thing. The only differences with the following are:
changing the Router.route('lists/:_id') to Router.route('lists/randomParm')
this.params._id to this.params.randomParm
Template
<template name="list">
<ul>
{{#each list}}
<li>{{name}}</li>
{{/each}}
</ul>
</template>
<template name="listPage">
<h2>Tasks: {{name}}</h2>
</template>
Route
Router.route('/list/randomParm', {
template: 'listPage',
data: function(){
var currentList = this.params.randomParm;
return Lists.findOne({_id: currentList});
}
});
The message I am getting is:
Oops, looks like there's no route on the client or the server for url: "http://localhost:3000/list/TGM9dbRRtspyJy7AR."
Isn't :_id and randomParm holding the same values? An id of list items from the HTML links that are being passed to the routing url and being used to make a mongo call? I don't quite understand how :_id and randomParm are different when I am hitting the same routing URL.
Param shold be with :
So your route will be
Router.route('/list/:randomParm', {
If this param is optional then leave ? after
Router.route('/list/:randomParm?', {

Meteor Block Helper that acts like a template

Here's what I want, a custom block helper that can act like a template, monitoring for it's own events etc. The html would look like this:
{{#expandable}}
{{#if expanded}}
Content!!!
<div id="toggle-area"></div>
{{else}}
<div id="toggle-area"></div>
{{/if}}
{{/expandable}}
And here's some javascript I have put together. This would work if I just declared the above as a template, but I want it to apply to whatever input is given to that expandable block helper.
Template.expandableView.created = function() {
this.data._isExpanded = false;
this.data._isExpandedDep = new Deps.Dependency();
}
Template.expandableView.events({
'click .toggle-area': function(e, t) {
t.data._isExpanded = !t.data._isExpanded;
t.data._isExpandedDep.changed();
}
});
Template.expandableView.expanded = function() {
this._isExpandedDep.depend();
return this._isExpanded;
};
I know I can declare block helpers with syntax like this:
Handlebars.registerHelper('expandable', function() {
var contents = options.fn(this);
// boring block helper that unconditionally returns the content
return contents;
});
But that wouldn't have the template behavior.
Thanks in advance! This might not be really possible with the current Meteor implementation.
Update
The implementation given by HubertOG is super cool, but the expanded helper isn't accessible from within the content below:
<template name="expandableView">
{{expanded}} <!-- this works correctly -->
{{content}}
</template>
<!-- in an appropriate 'home' template -->
{{#expandable}}
{{expanded}} <!-- this doesn't work correctly. Expanded is undefined. -->
<button class="toggle-thing">Toggle</button>
{{#if expanded}}
Content is showing!
{{else}}
Nope....
{{/if}}
{{/expandable}}
In the actual block helper, expanded is undefined, since the real thing is a level up in the context. I tried things like {{../expanded}} and {{this.expanded}}, but to no avail.
Strangely, the event handler is correctly wired up.... it fires when I click that button, but the expanded helper is simply never called from within the content, so even console.log() calls are never fired.
Meteor's new Blaze template engine solves this problem quite nicely.
Here's an excerpt from the Blaze docs showing how they can be used.
Definition:
<template name="ifEven">
{{#if isEven value}}
{{> UI.contentBlock}}
{{else}}
{{> UI.elseBlock}}
{{/if}}
</template>
Template.ifEven.isEven = function (value) {
return (value % 2) === 0;
}
Usage:
{{#ifEven value=2}}
2 is even
{{else}}
2 is odd
{{/ifEven}}
You can achieve this by making a helper that returns a template, and passing the helper options as a data to that template.
First, make your helper template:
<template name="helperTemplate">
<div class="this-is-a-helper-box">
<p>I'm a helper!</p>
{{helperContents}}
</div>
</template>
This will work as a typical template, i.e. it can respond to events:
Template.helperTemplate.events({
'click .click-me': function(e, t) {
alert('CLICK!');
},
});
Finally, make a helper that will return this template.
Handlebars.registerHelper('blockHelper', function(options) {
return new Handlebars.SafeString(Template.helperTemplate({
helperContents: new Handlebars.SafeString(options.fn(this)),
}));
});
The helper options are passed as a helperContents param inside the template data. We used that param in the template to display the contents. Notice also that you need to wrap the returned HTML code in Handlebars.SafeString, both in the case of the template helper and its data.
Then you can use it just as intended:
<template name="example">
{{#blockHelper}}
Blah blah blah
<div class="click-me">CLICK</div>
{{/blockHelper}}
</template>

Accessing parent context in Meteor templates and template helpers

I'm running into a template context situation that I'm having a hard time finding a way around.
Here's the template in question:
{{#each votes}}
<h3>{{question}}</h3>
<ul>
{{#each participants}}
<li>
<p>{{email}}</p>
<select name="option-select">
{{#each ../options}}
<option value="{{option}}" class="{{is_selected_option}}">{{option}}</option>
{{/each}}
</select>
</li>
{{/each}}
</ul>
</div>
{{/each}}
And here's an example of a vote document:
{
_id: '32093ufsdj90j234',
question: 'What is the best food of all time?'
options: [
'Pizza',
'Tacos',
'Salad',
'Thai'
],
participants: [
{
id: '2f537a74-3ce0-47b3-80fc-97a4189b2c15'
vote: 0
},
{
id: '8bffafa7-8736-4c4b-968e-82900b82c266'
vote: 1
}
]
}
And here's the issue...
When the template drops into the #each for participants, it no longer has access to the vote context, and therefore doesn't have access to the available options for each vote.
I can somewhat get around this by using the ../options handlebars path to jump back into the parent context, but this doesn't affect the context of the template helper, so this in Template.vote.is_selected_option refers to the current participant, not to the current vote or option, and has no way of knowing which option we are currently iterating through.
Any suggestions on how to get around this, without resorting to DOM manipulation and jQuery shenanigans?
This is a templating issue that has come up multiple times for me. We need a formal way of reaching up the template context hierarchy, in templates, template helpers, and template events.
It seems since Spacebars (Meteor's new template engine), you have access to the parent context within {{#each}} blocks using ../.
In Meteor 0.9.1, you can also write a helper and use Template.parentData() in its implementation.
It's not particularly pretty, but I've done something like this:
<template name='forLoop'>
{{#each augmentedParticipants}}
{{> participant }}
{{/each}}
</template>
<template name='participant'>
...
Question: {{this.parent.question}}
...
</template>
// and in the js:
Template.forLoop.helpers({
augmentedParticipants: function() {
var self = this;
return _.map(self.participants,function(p) {
p.parent = self;
return p;
});
}
});
It's similar to the approach that AVGP suggested, but augments the data at the helper level instead of the db level, which I think is a little lighter-weight.
If you get fancy, you could try to write a Handlebars block helper eachWithParent that would abstract this functionality. Meteor's extensions to handlebars are documented here: https://github.com/meteor/meteor/wiki/Handlebars
I don't know the formal way (if there is one), but to solve your issue, I would link the participants with the parent ID like this:
{
_id: "1234",
question: "Whats up?",
...
participants: [
{
_id: "abcd",
parent_id: "1234",
vote: 0
}
]
}
and use this parent_id in helpers, events, etc. to jump back to the parent using findOne.
That is obviously a sub optimal thing to do, but it's the easiest way that comes to my mind as long as there is no way of referencing the parent context.
Maybe there is a way but it is very well hidden in the inner workings of Meteor without mention in the docs, if so: Please update this question if you find one.
It's a long shot, but maybe this could work:
{{#with ../}}
{{#each options}}
{{this}}
{{/each}}
{{/with}}
This should make life easier.
// use #eachWithParent instead of #each and the parent._id will be passed into the context as parent.
Handlebars.registerHelper('eachWithParent', function(context, options) {
var self = this;
var contextWithParent = _.map(context,function(p) {
p.parent = self._id;
return p;
});
var ret = "";
for(var i=0, j=contextWithParent.length; i<j; i++) {
ret = ret + options.fn( contextWithParent[i] );
}
return ret;
});
Go ahead and change
p.parent = self._id;
to whatever you want to access in the parent context.
Fixed it:
// https://github.com/meteor/handlebars.js/blob/master/lib/handlebars/base.js
// use #eachWithParent instead of #each and the parent._id will be passed into the context as parent.
Handlebars.registerHelper('eachWithParent', function(context, options) {
var self = this;
var contextWithParent = _.map(context,function(p) {
p.parent = self._id;
return p;
});
return Handlebars._default_helpers.each(contextWithParent, options);
});
This works :) with no error
Simply register a global template helper:
Template.registerHelper('parentData',
function () {
return Template.parentData(1);
}
);
and use it in your HTML templates as:
{{#each someRecords}}
{{parentData.someValue}}
{{/each}}
======= EDIT
For Meteor 1.2+, you shold use:
UI.registerHelper('parentData', function() {
return Template.parentData(1);
});
I was stuck in a similar way and found that the Template.parentData() approach suggested in other answers currently doesn't work within event handlers (see https://github.com/meteor/meteor/issues/5491). User Lirbank posted this simple workaround:
Pass the data from the outer context to an html element in the inner context, in the same template:
{{#each companies}}
{{#each employees}}
Do something
{{/each}}
{{/each}}
Now the company ID can be accessed from the event handler with something like
$(event.currentTarget).attr('companyId')
"click .selected":function(e){
var parent_id = $(e.currentTarget).parent().attr("uid");
return parent_id
},
<td id="" class="staff_docs" uid="{{_id}}">
{{#each all_req_doc}}
<div class="icheckbox selected "></div>
{{/each}}
</td>
{{#each parent}}
{{#each child}}
<input type="hidden" name="child_id" value="{{_id}}" />
<input type="hidden" name="parent_id" value="{{../_id}}" />
{{/each}}
{{/each}}
The _id is NOT the _did of the thing, it's the id of the parent!

Resources