Is it possible to switch collection focus in Meteor spacebars templates? - javascript

I am trying to display some relational data in a Meteor spacebars template. Specifically I two collections, Location and Weather. They look something like this:
Location {
_id: 'full-name',
title: 'Full name',
lat: '123.456',
long: '123.456',
order: 1
}
Weather {
locName: 'full-name', // this name always matches a location _id
temperature: '41.3'
}
I'd like to display information from both of these collections on a single page. So that I can show the latest weather from each location (there are 4-20 of them per page). To do this, I've published a Mongo request of both collections like so on the server side:
Meteor.publish('allLocations', function() {
return [
Locations.find({}, { sort: { order: 1 } }),
Weather.find({}) // The weather
]
});
I then subscribe to this publication in my router (iron-router):
Router.map(function() {
this.route('locations', {
waitOn: function () {
return Meteor.subscribe('allLocations');
}
}
});
However, I get stuck when I get to my spacebars template. I can't figure out the syntax for switching collection focus in spacebars.
Here's the psuedo-code for the template that I'm trying to parse, but I know that this doesn't currently work.
<template name="locations">
<div class="locations-grid">
{{#each locations}}
<div class="location {{_id}}">
This is the location template
<h1>{{title}}</h1>
{{#each weather}}
<!-- Trying to pass the _id along to the weather template for filtering -->
{{> weather _id}}
{{/each}}
</div>
{{/each}}
</div>
</template>
<template name="weather">
This is the weather template
{{#with weather}}
<!-- Ideally, we've now switched contexts to the weather collection -->
<h2>Temperature: <div class="temp">{{temperature}}</div></h2>
{{/with}}
</template>
So my question is, where do I tell spacebars to switch contexts to the weather collection? How can I pass along the _id variable to the weather template so that I can select the right data from the collection? I know I'm missing a big step here, I just can't figure out which portion of the Meteor space to examine. I know I might need to specify a subscription for the weather template, but I'm not sure where to do that since it's not really a route, since it won't have its own page. It just lives as a sub-template within the locations template.
Thanks for any tips, or possible suggestions on restructuring.

Before we begin, please read A Guide to Meteor Templates & Data Contexts - that will correctly orient you on contexts inside of #each blocks.
Your goal is to join the correct weather documents to their corresponding location documents. This is most easily accomplished by introducing sub-templates for both types. Let's begin with the top-level template:
<template name="locations">
<div class="locations-grid">
{{#each locations}}
{{> location}}
{{/each}}
</div>
</template>
Which has a locations helper like this:
Template.locations.helpers({
locations: function() {
return Locations.find();
}
});
Next, the location template:
<template name="location">
<div class="location">
<h1>{{title}}</h1>
{{#each weathers}}
{{> weather}}
{{/each}}
</div>
</template>
Which has a weather helper like this:
Template.location.helpers({
weathers: function() {
return Weather.find({locName: this._id});
}
});
The key insight here is that the context of a location template is a single location document so weather will return only the weather documents for this location instance. Finally, your weather template can look like:
<template name="weather">
<h2>Temperature: {{temperature}}</h2>
</template>
Note we are now in a weather context so the #with is no longer needed.
Side note - using sort in your publisher has no affect in this case.

Related

Using if to have different elements on different pages in Meteor.js

I have a certain div class (landing), that I only want to have appear on certain routes. I am using a "main" template that has my header and footer (with a >yield included between for each pages unique code). However, there is an element in the header that I only want to appear on the landing page ('/' route).
I am using Iron Router.
Is there a way to do this easily with an #if in meteor? Thanks!
Here's a generic route name equality testing helper:
Template.registerHelper('routeEquals',function(name){
return Router.current().route.getName() === name;
});
Then in any template you can do (for example):
{{#if routeEquals '/'}}
in the / route
{{else}}
Not in the / route
{{/if}}
You want to create a helper on your header template that reactively checks the current route. I haven't used Iron Router in a while, but believe Router.current().route.getName() is reactive. Try the following:
Template.header.helpers({
onLanding() {
return Router.current().route.getName() === '<landingRouteName>';
},
});
<template name="header">
{{#if onLanding}}
<div>I should only show on the landing page!</div>
{{/if}}
</template>
Or, alternatively (and maybe optimally - it would keep the template loaded, rather than injecting it into the DOM when you hit landing), you can use the helper to apply a class like
<div class="{{#if onLanding}}isVisible{{/if}}>...</div>
You can put a session flag in the onBeforeAction of the landing route and then, on the header, check if that session is valid and show the div
Something like this
this.route("landing", {
path: "/landing",
onBeforeAction: function () {
Session.set('showDiv', true);
}
});
<template name='main'>
<header>{{#if helper_getShowDivFlag}}<div></div>{{/if}}</header>
</template>
helper_getShowDivFlag: function() {
return Session.get('showDiv');
}
Alternatively, checking the router name in the main template and show/hide the div
<template name='main'>
<header>{{#if helper_showDivFlag}}<div></div>{{/if}}</header>
</template>
helper_showDivFlag: function() {
return Router.current().route.getName() == 'landing'
}
Or if you don't want to write code, use this package and check the router :D
<template name='main'>
<header>{{#if isActiveRoute 'landing' }}<div></div>{{/if}}</header>
</template>
You can accomplish this using the data property of your route.
Router.route('/', {
name: 'home',
data: function() {
return {
showMySpecialHeaderWidget: true
};
}
});
Then you can access this data property in your template.
<template name="main">
<header>
{{#if showMySpecialHeaderWidget}}
<div>My special header widget</div>
{{/if}}
</header>
{{> yield}}
</template>
Using this method, you don't need to worry about updating your template if you ever change the names of your routes, and it's easy to show your widget on multiple routes by including this data property.

Meteor publish and subscribe not working

I have a fairly novice question and am hoping someone can help me.
I have the following code in which subscribe/publish is not working:
if (Meteor.isServer) {
Meteor.publish('items', function () {
return Items.find({});
});
}
if (Meteor.isClient) {
Meteor.subscribe("items");
}
In my template:
<template name="items">
<div class="ui segment">
<div class="ui relaxed list">
{{#each item in items}}
<div class="item">
<img class="ui avatar image" src="http://placehold.it/20x20">
<div class="content">
<a class="header">{{item.text}}
</div>
<button class="delete">×</button>
</div>
{{/each}}
</div>
</div>
</template>
But nothing gets output. However when I add:
Template.items.helpers({
items: function () {
return Items.find({});
}
});
I do see the list properly. Why is this so? Also I am clearly confused as to why someone would want to use subscribe/publish along with the template helpers.
I suggest you read Data flow from the database to the UI: Three layers of Meteor
You are creating a publication labeled: items. Then you are subscribing to a publication labeled deals. This will not work as the labels must match for the subscription to work.
If adding that template helper then shows the data in the UI, you must have the autopublish package in your app. It will be autopublish, not your pub/sub, that is sending the client the data the client puts in it's mini-mongo Items collection.
So pub/sub gets the data from the server to the client, but doesn't display it. That is why you need the template helper, to get the data from the client's mini-mongo collection into the format the templates require.
You must subscribe() using the same name as used in the publish(). In your case (pay attention to 'items'):
/server:
Meteor.publish('items', function () {
return Items.find({});
});
/client:
if (Meteor.isClient) {
Meteor.subscribe('items');
}
The publish/subscribe link is telling meteor to send all docs in the 'Items' collection to minimongo on the client. Now, the purpose of the template helper is to tell meteor that you want to reactively update the template any time a document changes in Items.
A good reference on the subject: https://www.discovermeteor.com/blog/understanding-meteor-publications-and-subscriptions/

How to create an Ember JS Master Detail where detail appears between rows of data

I'm trying to create a page that contains master/detail, which is obviously easy using a static {{ outlet }} in ember, however, I'd like the detail to slide down after the row that was selected. For example, on this page: http://jsbin.com/ijejap/1/edit, if the detail of the name was to appear after the currently selected name instead of at the bottom of the page.
The problem I'm trying to solve is that I can't have an outlet within a repeater so that when I click Row 1, I want the outlet to be positioned below Row 1, and when I click Row 2, I want the outlet after Row 2. In other words, I want to dynamically position an outlet, I think, unless there's another way to do it.
Ok, I finally made a conditionnal outlet depending on wether the user is selected or not, but I had to work with DS.Model and its FIXTURES to make this work.
To be brief, I added an action before the link to set a selected property on the user object and record which user is selected to prevent having two users at the same time.
The position of the {{outlet}} then depends on the current selected user.
Here's the template part:
{{#each model}}
<li {{action 'select' this}}>
{{#link-to "user" this}}{{first}}{{/link-to}}
</li>
{{#if selected}}
{{outlet}}
{{/if}}
{{/each}}
And the App.ApplicationController part:
App.ApplicationController = Ember.ArrayController.extend({
currentUser: null,
actions: {
select: function(newUser) {
var oldUser = this.get('currentUser');
if(newUser !== oldUser) {
if(oldUser)
oldUser.toggleProperty('selected');
this.set('currentUser', newUser);
newUser.toggleProperty('selected');
}
}
}
});
Finally, the model definition:
App.ApplicationRoute = Ember.Route.extend({
model: function() {
return this.store.find('user');
}
});
App.User = DS.Model.extend({
first: DS.attr(),
last: DS.attr(),
avatar: DS.attr()
});
Here's the link to the JSBin: http://jsbin.com/cilari/3/
You can make use of components power here.
I made it on JSBin : http://jsbin.com/zovos/2/
First, you declare your user details as a new component, say user-details :
<script type="text/x-handlebars" id="components/user-details">
<li>{{user.first}}</li>
<h2>
{{user.first}} {{user.last}}
<img {{bindAttr src="user.avatar"}} class="pull-right" width=50 />
</h2>
<dl>
<dt>First</dt>
<dd>{{user.first}}</dd>
<dt>Last</dt>
<dd>{{user.last}}</dd>
</dl>
</script>
Then you call it from your list, using the current user as the user in the component :
<script type="text/x-handlebars">
<div class="container">
<div class="row-fluid">
<ul class="nav nav-list span3 well">
{{#each model}}
{{user-details user=this}}
{{/each}}
</ul>
</div>
</div>
</script>
You add the selected property to your component and an action like expandDetails :
App.UserDetailsComponent = Ember.Component.extend({
selected: false,
actions: {
expandDetails: function() {
this.toggleProperty('selected');
}
}
});
Finally, you add the action to your component template and a conditional display on the selected property :
<script type="text/x-handlebars" id="components/user-details">
<li><a {{action "expandDetails"}}>{{user.first}}</a></li>
{{#if selected}}
<h2>
{{user.first}} {{user.last}}
...
</dl>
{{/if}}
</script>
And, of course, you get rid of your user route.
Here is a link to Ember's Guide that show this on a sample post example : http://emberjs.com/guides/components/handling-user-interaction-with-actions/
Hope this helps.
Are you looking for an accordion?
http://addepar.github.io/#/ember-widgets/accordion
I have an idea but unproved yet, maybe you can try it.
For detail, you only need one template. For master, you need write several named outlet (see: http://emberjs.com/guides/routing/rendering-a-template/) and place each below the corresponding item of master view.
When each master item has been clicked, you need pass a param to the detail route in order to let it know what kind of data should be populated.
The key part is the renderTemplate hook of route (see: http://emberjs.com/api/classes/Ember.Route.html#method_renderTemplate). You can take advantage of it because it allows you to specify which named outlet should be used to render the template.
Or you may load all details' data at once and cache it, then pass an identifier when master item got clicked, in this way you can filter the model in advance and get prepared for later renderTemplate use.
This is just an idea and currently don't have spare time to try it, but as your wish, you got the dedicated route, controller and model. I think it is highly doable, let me know it you make any progress.

How Do I Properly Make a Meteor Template Reactive?

My app displays a collection of items and I'd like to add an item drilldown view.
I've only been able to get half of the solution working; I find the appropriate document and use that document to render the template:
var item = Items.findOne('my_id');
$('#main').html(
Meteor.render(function () {
return Templates.item(item)
}));
This renders the individual item successfully and the appropriate events are bound.
Here's the rub, the template isn't reactive! If I change its data using the associated event handlers or from the console, the template isn't updated. However, a page refresh will reveal the updated data.
I'm a Meteor noobie, so it's probably something very simple. Any help would be greatly appreciated.
It seems to me that you aren't using the templates in they way they were really intended to be.
A meteor app starts with the main html markup which can only exist once in your app..
<head>
<title>My New Fancy App</title>
</head>
<body>
{{>templateName}}
</body>
Then you add a template..
<template name="templateName">
{{#each items}}
template or relevant html goes here..
{{/each}}
</template>
Now you need a template helper to give you data for your {{#each items}} block helper..
Template.templateName.helpers({
items: function(){ return Items.find({}) }
});
All this gets defined on the client side..
Then you'll need a collection and the collection should be defined on both the client and server.
Items = new Meteor.Collection('items');
This should now work as long as you have records in your collection.
Since you wish to only wish to render a single document you can change the helper and template just slightly..
first the helper becomes:
Template.templateName.helpers({
item: function(){ return Items.findOne() }
});
Then the template can reference the values of the returned document through document, so we change our template to:
<template name="templateName">
{{item.propertyName}}
</template>

Meteor: Re-use template with different data

I've got a few templates which list items (log lines, users) and style them accordingly in tables. Now I've added a "search" page which searches for a string in different item types and displays them on one page. In terms of layout, I want to reuse the original templates, but obviously with the data returned by the search.
How can I do that without duplicating the template itself in the HTML file?
Put another way: How can I use the actual data from the top-level template in the sub-templates.
Small example:
<template name="user_list">
{{#each user_list}}
</template>
<template name="users">
{{> user_list}}
</template>
<template name="search">
{{> user_list}}
{{> group_list}}
</template>
In .coffee:
Template.users.user_list = ->
[a,b,c,d]
Template.search.user_list = ->
[b,c]
Maybe this is an easy one, which would show how little I really understood about Meteor.
Just put the template you want to use inside the {{#each}} statement. Then each item in the array will be the context for that inner template.
HTML file
<template name="users">
<ul>
{{#each UserArr}}
{{> userItem}}
{{/each}}
</ul>
</template>
<template name="userItem">
<li>{{username}} {{profile.name}}</li>
</template>
JS file
Template.users.UserArr = function()
{
return Meteor.users.find({});
};
another solution is to put your request in a Session attribute and check for its existence when querying for the users. By default Session.get("userQuery") would return undefined but once you enter something in the search field you change that with Session.set("userQuery", {type: "something"})
Now in the coffeescript you could say:
Template.users.user_list = ->
if(Session.get("userQuery"))
[a,b,c,d]
else
[b,c]
or alternatively use a MiniMongo query because it is much nicer :-)
the nice thing is that your HTML will rerender because it is reactive to the Session's content.

Categories