I've been playing around with Meteor + Iron Router for a multi-page app I'm working on and I'm getting stuck on helper functions for named yields. Specifically, I've been trying to get the active class for my navbar tabs to update on each route change.
Below is the relevant code for my project:
router.js
Router.configure({
layoutTemplate: 'mothership',
yieldTemplates: {
'header' : {to: 'header'},
'footer': {to: 'footer'}
},
});
Router.map(function () {
// Home page
this.route('home', {
path: '/',
template: 'home',
});
this.route('about', {
path: '/about',
template: 'about',
});
this.route('emails', {
path: '/emails',
template: 'emails',
});
this.route('people', {
path: '/people',
template: 'people',
});
});
mothership.html
<template name="mothership">
Skip to content
<div id="wrap">
<!-- header -->
<div>{{yield 'header'}}</div>
<div id="content">{{yield}}</div>
</div>
<div id="push"></div>
<div id="footer">
{{yield 'footer'}}
</div>
</template>
header.html
...bootstrap stuff...
<a class="navbar-brand" href="{{pathFor 'home'}}">Mailchacho</a>
<li class="{{activeRoute 'about'}}">About</li>
<li class="{{activeRoute 'emails'}}">Sent Emails</li>
<li class="{{activeRoute 'people'}}">People</li>
...bootstrap stuff...
header.js
Handlebars.registerHelper('activeRoute', function(name) {
var active = location.pathname === Router.path(name);
return (active) ? 'active' : '';
});
// I know I can use Template.headers.helpers... to do this as well, I just found the registerHelper to be cleaner.
When I load a page from scratch, the proper active class is assigned, but when a route is changed on-page, the active class doesn't update. Using breakpoints, I can see the 'activeRoute' function isn't called on a change.
What's interesting is if I add a data dictionary to router.js, it does update. My guess is having the data dictionary indicates that something has changed between routes, forcing a refresh. What I'd like to do is have this refresh occur without needing to pass a data dictionary.
Since Iron Router is still fairly new, I haven't been able to able to find much online. The closest I've found is this issue on github (https://github.com/EventedMind/iron-router/issues/103), but the last comment was never resolved, which seems to be similar to mine.
With the above in mind, is there any way I can signal for a helper function to be rerun on a route change without passing a dummy data dictionary? I was thinking that something like Deps.autorun might be needed, but that doesn't feel right. I'm still pretty new to Meteor & Iron Router so any help here would be appreciated. Thanks!
Yours is a common problem so Mike Fitzgerald has built a package just for this purpose:
https://atmosphere.meteor.com/package/iron-router-active
The given example is like:
<nav>
<ul>
<li class="{{ isActive 'dashboard' }}">...</li>
<li class="{{ isActive 'dashboard|root' }}">...</li>
<li class="{{ isActive 'users' 'on' }}">...</li>
<li class="{{ isActivePath 'products' }}">...</li>
</ul>
</nav>
andd works through handlebars helpers which are called isActive, isActivePath, isNotActive and isNotActivePath.
I use meteor add zimme:active-route now. It works with iron:router, kadira:flow-router and meteorhacks:flow-router.
Just two examples: Output active class:
<li class="{{isActiveRoute 'home'}}">...</li>
Custom class:
<li class="{{isActiveRoute 'home' class='is-selected'}}">...</li>
https://atmospherejs.com/zimme/active-route
Related
The site I'm currently working on is built in Drupal 7. I have one form that requires user input so I'm attempting to build it with VueJS. The form is contained all within one template file (.tpl.php) and all the content is provided in this template file or via the VueJS Javascript (nothing is coming from the CMS).
The issue I have is that the Vue components are not rendering on the front-end, but when I copy the code into a JSFiddle they do, so I'm guessing it is an issue with the interaction between VueJS and Drupal. Here is a screenshot of my markup when inspecting...
Here is the code from the .tpl.php file...
<div id="app">
<form>
<div>
<label for="year">Per Year</label>
<input type="radio" name="frequency" id="year" value="year" v-model="frequency" checked>
<label for="month">Per Month</label>
<input type="radio" name="frequency" id="month" value="month" v-model="frequency">
</div>
</form>
<ul class="plans">
<template id="plan-component">
<h2 class="plan-name">{{ name }}</h2>
<h2 class="plan-cost">{{ price }}</h2>
<h2 class="plan-tagline">{{ tagline }}</h2>
Choose this plan
</template>
<li>
<plan-component :frequency="frequency"
name="Basic"
tagline="Basic tagline"
price-yearly="Free"
price-monthly="Free"
></plan-component>
</li>
<li>
<plan-component :frequency="frequency"
name="Rec"
tagline="Rec tagline"
price-yearly="3"
price-monthly="4"
></plan-component>
</li>
<li>
<plan-component :frequency="frequency"
name="Team"
tagline="Team tagline"
price-yearly="4"
price-monthly="5"
></plan-component>
</li>
<li>
<plan-component :frequency="frequency"
name="Club"
tagline="Club tagline"
price-yearly="5"
price-monthly="6"
></plan-component>
</li>
</ul>
</div>
..and the code from my JS file...
Vue.component('plan-component', {
template: '#plan-component',
props: ['frequency', 'name', 'tagline', 'priceYearly', 'priceMonthly'],
computed: {
'price': function() {
if (this.frequency === 'year') {
return this.priceYearly;
} else {
return this.priceMonthly;
}
}
},
methods: {
makeActivePlan() {
// We dispatch an event setting this to become the active plan
this.$dispatch('set-active-plan', this);
}
}
});
new Vue({
el: '#app',
data: {
frequency: 'year',
activePlan: {name: 'no', price: 'You must select a plan!' }
},
events: {
'set-active-plan': function(plan) {
this.activePlan = plan;
}
},
});
And here is the JSFiddle which outputs the components correctly - https://jsfiddle.net/2xgrpLm6/
What browser are you using? <template> tags are not supported in IE.
Another idea is to make sure you are never using fragment components (meaning wrap everything inside your template with a div like so:
<template id="foobar">
<div>
CONTENT HERE
</div>
</template>
Lastly, have you turned on Vue debug mode? Before you instantiate your Vue instance, set Vue.config.debug = true and see if you get console errors then.
Try moving the <template id="plan-component">...</template> code outside of the Vue instance. I.e., such that it is not contained within <div id="app">...</div>.
This has solved a similar problem for me in the past, though I'm not sure if it applies here.
For anyone having a similar issue, the solution was simple. After Jeff suggested turning on Vue debug mode (and downloading the Dev version of Vue JS instead of minified - https://vuejs.org/guide/installation.html) the console gave the error [Vue warn]: Cannot find element: #app.
The issue was that Drupal was loading my scripts in the <head>, before <div id="app"> was loaded in the DOM. As such #app couldn't be found. After outputting the scripts before the closing <body> tag all was sorted. See here for more information [Vue warn]: Cannot find element
I have a route that has a parameter in it and I need to access it from many different templates. Below is one example of the route, there are several routes that are very similar just after the _occasionnId parameter it changes:
For example:
Route 1: /occasions/:_occasionId/cards/
Router 2: /occasions/:_occasionId/tables/
Here is my full code for each route, the only thing that really changes is the route path and the template.
Router.route('/occasions/:_occasionId/cards/', {
template: 'cards',
data: function(){
//var currentoccasion = this.params._occasionId;
//console.log(currentoccasion);
},subscriptions : function(){
Meteor.subscribe('cards');
Meteor.subscribe('tables');
}
});
I need to get the _occasionId parameter into a template that has navigation which goes in on all of these pages. My goal is that from Route 1, you can go to Router 2. But I can't figure out how to add the correct URL in the template.
My template is:
<template name="occasionnav">
<nav>
<div class="nav-wrapper">
<ul class="right hide-on-med-and-down">
<li>cards</li>
<li>tables</li>
</ul>
</div>
</nav>
</template>
In the 'occasionnav' template by ":_occasionId" I need that to be the same parameter as the page currently being viewed be stuck into here.
If anyone has any insight or advice on the best way to approach this I would really appreciate it.
I recommend to use {{pathFor}} if you want to render an internal route in your Meteor application.
You just need to set the proper context and name your routes, for example:
<template name="occasionnav">
<nav>
<div class="nav-wrapper">
<ul class="right hide-on-med-and-down">
{{#with occasion}}
<li>cards</li>
<li>tables</li>
{{/with}}
</ul>
</div>
</nav>
</template>
Router.route('/occasions/:_id/cards/', {
template: 'cards',
name: 'occasions.cards',
data: function() {
return Cards.findOne({_id: this.params._id});
},
subscriptions: function() {
return Meteor.subscribe('cards', this.params._id);
}
});
Router.route('/occasions/:_id/tables/', {
template: 'tables',
name: 'occasions.tables',
data: function() {
return Tables.findOne({_id: this.params._id});
},
subscriptions: function() {
return Meteor.subscribe('tables', this.params._id);
}
});
However, you can also get the router parameters in your template via Router.current().params.
You can pass the _occasionId as a template helper and render it in jade like:
<li>cards</li>
You should try :
Router.route('/occasions/:_occasionId/cards/', function() {
this.layout("LayoutName");
this.render("cards", {
data: {
currentoccasion = this.params._occasionId;
}
});
});
When you use Router.map, it's not exactly the same syntax:
Router.map(function() {
this.route('<template name>', {
path: 'path/:_currentoccasion',
data: function () {
return {
currentoccasion: this.params._currentoccasion
}
}
});
And you can access just like that in your template :
Like an helper
{{ currentoccasion }}
Or on the onRendered and onCreated functions
Template.<template name>.onRendered({
this.data.currentoccasion
What is the "formal" way of handling multiple "pages" in meteor? I say "pages" I've seen people do it a couple of different ways. I've seen people create actual full pages, (index.html, about.html, contact.html) and then when links are clicked, you'd write a route to render those pages. But I've also seen people essentially put the code for each of those pages inside <template> tags and then do nifty show/hide type stuff based of what they've clicked, login credentials, etc.
There are several ways to handle multiple pages in meteor
1.iron-router
using iron router you can create a layout template and inject all other templates inside it using {{> yield}}. Check the iron-router guide to know more about layout template.
2.{{> Template.dynamic}}
if you do not want to add iron-router you can use {{> Template.dynamic}} to achieve the same.
<body>
<ul>
<li><a href="#" class="index">Home</li>
<li><a href="#" class="about">About</li>
<li><a href="#" class="contact">Contact</li>
</ul>
{{> Template.dynamic template=template_name }}
</body>
template_name can be changed reactively using a template helper
Meteor.startup(function () {
Session.setDefault("templateName", "index")
});
Template.body.helpers({
template_name: function(){
return Session.get("templateName")
}
});
Template.body.events({
"click .home": function() {
Session.set("templateName", "index");
},
"click .about": function() {
Session.set("templateName", "about");
}
// ..
});
If the Session returns "about" then,
{{> Template.dynamic template="about"}} which is equivalent to {{> about}}.
Ok, so I found this post: Meteor breadcrumb
But lets say I have the following:
<template name="somePage">
<h1>Page Title</h1>
{{> breadcrumb}}
</template>
<template name="breadcrumb">
<ul class="breadcrumb">
<li>
Home
</li>
{{#each path}}
<li>
{{this}}
</li>
</ul>
</template>
Helper:
Template.breadcrumb.helpers({
path: function() {
return Router.current().path.split( "/" );
}
});
Ok so the linked question at the top got me the basics. I'm trying to understand how to do a few more things here that should be obvious. I want the first to be for the home page, and the result returned from the path: function() includes an empty "", "page", "page", etc. in the beginning of it.
I'd like to be able to incorporate the proper paths. To be clear, I'd love to pull this off:
<template name="breadcrumb">
<ul class="breadcrumb">
<li>
Home
</li>
~ pseudo logic
{{#each path that isn't current page}}
<li>
{{this}}
</li>
{{/each}}
<li>
{{ currentPage }}
</li>
</ul>
</template>
Has anyone done this or found a reference that I haven't stumbled across yet?
I'll give you my own recipe for breadcrumbs using iron:router.
It works by supplying additional options to your routes in order to establish a hierarchy between them, with parent-children relations. Then we define a helper on the Router to give us a list of parent routes (up to home) for the current route. When you have this list of route names you can iterate over them to create your breadcrumbs.
First, we need to define our breadcrumbs template which is actually very similar to your pseudo-code. I'm using bootstrap and font-awesome, as well as some newly introduced iron:router#1.0.0-pre features.
<template name="breadcrumbs">
<ol class="breadcrumb">
<li>
{{#linkTo route="home"}}
<i class="fa fa-lg fa-fw fa-home"></i>
{{/linkTo}}
</li>
{{#each intermediateRoutes}}
<li>
{{#linkTo route=name}}
<strong>{{label}}</strong>
{{/linkTo}}
</li>
{{/each}}
<li class="active">
<strong>{{currentRouteLabel}}</strong>
</li>
</ol>
</template>
The {{#linkTo}} block helper is new in iron:router#1.0.0-pre, it simply outputs an anchor tag with an href attribute which value is {{pathFor "route"}}.
Let's define the helpers from our breadcrumbs template:
Template.breadcrumbs.helpers({
intermediateRoutes: function() {
if (!Router.current()) {
return;
}
// get rid of both the first item, which is always assumed to be "home",
// and the last item which we won't display as a link
var routes = Router.parentRoutes().slice(1, -1);
return _.map(routes, function(route) {
// extract name and label properties from the route
return {
name: route.getName(),
label: route.options.label
};
});
},
currentRouteLabel: function() {
// return the label property from the current route options
return Router.current() && Router.current().route.options.label;
}
});
Notice that we rely on the existence of a special option named 'label' which represents what we're going to put in our anchors, we could also have used the name for testing purpose.
The parentRoutes method is something we need to extend the Router with:
_.extend(Router, {
parentRoutes: function() {
if (!this.current()) {
return;
}
var routes = [];
for (var route = this.current().route; !_.isUndefined(route); route = this.routes[route.options.parent]) {
routes.push(route);
}
return routes.reverse();
}
});
Again, this function assumes that every route (except "home") has a parent property which contains the name of its parent route, we then iterate to traverse the route hierarchy (think of a tree, like a file system structure) from the current route up to the root route, collecting each intermediate route in an array, along with the current route.
Finally, don't forget to declare your routes with our two additional properties that our code relies on, along with a name which is now mandatory as routes are indexed by name in the Router.routes property:
Router.route("/", {
name: "home"
});
Router.route("/nested1", {
name: "nested1",
parent: "home"
});
Router.route("/nested1/nested2", {
name: "nested2",
parent: "nested1"
});
// etc...
This example is pretty basic and certainly doesn't cover every use case, but should give you a solid start in terms of design logic toward implementing your own breadcrumbs.
Inspired by #saimeunt I created a meteor breadcrumb plugin which can be found here: https://atmospherejs.com/monbro/iron-router-breadcrumb. You also specify a parent route and a title for the route itself.
I used saimeunt answer but had to make small changes to the template and the template helpers because I have parameters in some of my route paths. Here are my changes.
Template changes: add data=getParameter to #linkTo for intermediate routes
<template name="breadcrumbs">
<ol class="breadcrumb">
<li>
{{#linkTo route="dashboard"}}
<i class="fa fa-lg fa-fw fa-home"></i>
{{/linkTo}}
</li>
{{#each intermediateRoutes}}
<li>
{{#linkTo route=name data=getParameters}}
<strong>{{label}}</strong>
{{/linkTo}}
</li>
{{/each}}
<li class='active'>
<strong>{{currentRouteLabel}}</strong>
</li>
</ol>
</template>
Template helper changes: add helper function getParameters to get parameters from current route.
Template.breadcrumbs.helpers({
intermediateRoutes: function () {
if (!Router.current()) {
return;
}
var parentRoutes = Router.parentRoutes();
var routes = parentRoutes.slice(1, -1);
var intermediateRoutes = _.map(routes, function (route) {
return {
name: route.getName(),
label: route.options.label
};
});
return intermediateRoutes;
},
currentRouteLabel: function () {
var currentRouteLabel = Router.current() && Router.current().route.options.label;
return currentRouteLabel;
},
getParameters: function(){
var currentRoute = Router.current();
var parameters = currentRoute.params;
return parameters;
}
});
I've a tab bar on a responsive app I'm building:
<template name="tabNav">
<nav class="bar bar-tab">
<a class="tab-item" id="groups-nav" href="{{pathFor 'groupsList'}}">
<span class="icon icon-star-filled"></span>
<span class="tab-label">Groups</span>
</a>
<a class="tab-item active" id="games-nav" href="{{pathFor 'locationSet'}}">
<span class="icon icon-list"></span>
<span class="tab-label">Games</span>
</a>
<!-- more code -->
</template>
The pathFor 'groupsList' works on desktop, but not on mobile. You can try it out here: pp-groups.meteor.com.
This is a prototype only and doesn't use any real data. All of my views code is available here: https://github.com/stewartmccoy/pp-groups/tree/master/groups/client/views
These are my defined routes:
Router.map(function() {
this.route('layout', {
path: '/',
template: 'getLocation',
layoutTemplate: 'getLocation',
yieldTemplates: {
'tabNav': {to: 'footer'}
}
});
this.route('locationSet', {
path: '/locationSet',
template: 'locationSet',
layoutTemplate: 'locationSet'
});
this.route('groupsList', {
path: '/groupsList',
template: 'groupsList',
layoutTemplate: 'groupsList'
});
});
Why doesn't the pathFor work on mobile? (It at least doesn't work in Xcode iOS simulator or on iPhone Mobile Safari or Chrome).
The push.js component is causing the issue. You can still use Rachet with Iron Router by disabling push.js. Per rachet's documention you can disable push by adding a data-ignore tag to your HTML link.
<!-- Use data-ignore="push" to prevent the push.js interception -->
<a href="http://www.google.com" data-ignore="push">Google<a>
Routing issue:
Removing the ratchet package fixed it for me. Looks like ratchet uses it's own way of linking between templates which is incompatible with iron-router. Removing ratchet removes the UI elements, but the routing works on mobile: http://pp-groups-fixed.meteor.com. You could use a strictly UI library, like bootstrap to make the UI elements, or maybe even just the ratchet's UI components. If you want to fully use ratchet, you will most likely have to forgo IronRouter.
Other things to fix:
Layout Templates
When using meteor and iron-router, a layout template is a template with common elements, with a {{> yield}} placed where you want the regular templates to show up.
You actually only have one real layout template in your code, in groups.html there is a layout template named layout, and it is unused.
In your code, regular templates are being misused as layout templates, because they don't have {{> yield}} in them. Also, the tabNav template is being placed using iron-router, yet you have already included it in each template with {{> tabNav}}.
So, you can simply get rid of the layout template code in your iron router, and your app will still function:
Router.map(function() {
this.route('layout', {
path: '/',
template: 'getLocation',
// layoutTemplate: 'getLocation',
// yieldTemplates: {
// 'tabNav': {to: 'footer'}
// }
});
this.route('locationSet', {
path: '/locationSet',
template: 'locationSet',
// layoutTemplate: 'locationSet'
});
this.route('groupsList', {
path: '/groupsList',
template: 'groupsList',
// layoutTemplate: 'groupsList'
});
});
A better way is to take out all the common code, the header, general structure of the page, tab bar, and put it in a layout template. Add a {{> yield}} where you want the page template to render. Refer to this layout template in your router as layoutTemplate.
Another sidenote, iron-router automatically looks for the template with the same name as the route, if no template is defined. So if you are writing this.route('groupsList', ... you don't need to write template: 'groupsList' as well.
Data
Your past-game.js file should be named get-location.js. Yes, the name itself doesn't matter, but that is getLocation's complimentary code, not postGame's. Same with scheduled-games.js and locationSet. Look at the Template.templateName.helpers to see how the code corresponds.
Of course, ideally this data should be in a collection. For now, instead of creating the data as arrays with var, you could create a seperate file with your data as global variables. Simply define as PastGames = [...], then use the template helpers to return the data you need.