After trying to authenticate Google API calls through Meteor's accounts-google
package, I decided to try Analytics' Embed API to cut down on some complexity. I've been able to get individual dashboards working by loading the client library, authenticating, etc. on each template, but of course that's inefficient and filled with repetition.
What's the best way to load the Embed API library, authentication, and view selector once while allowing templates to detect them as they're loaded with Iron Router? Or am I simply forcing Meteor into an application it's not well-suited for?
I've added some sample code to give an idea of how things are structured now:
<template name="layout">
<header>
<h1><a href="{{pathFor 'basic'}}">DDDashboard</a></h1>
</header>
<section id="admin">
<div id="auth-button"></div> <!-- Outlet for Embed API's auth -->
{{> view}} <!-- Outlet for the View template -->
</section>
<section id="main">
{{> yield}} <!-- Outlet for each dashboard -->
</section>
</template>
This template is used in a Template.foo.rendered
to prevent the dashboard from loading until the Embed API view selector is fully instantiated.
<template name="view">
<div id="view-selector"></div>
</template>
Generic example of a dashboard—the Embed API relies on predefined elements to render into. More complex dashboards would have more <div>
to contain other charts/info.
<template name="basic">
<div id="data-container"></div> <!-- Outlet for the Embed API chart -->
</template>
Router.configure({
layoutTemplate: 'layout'
});
Router.route('/', {
name: 'basic'
});
Loads the Google Embed API library at launch.
(function(w,d,s,g,js,fs){
g=w.gapi||(w.gapi={});g.analytics={q:[],ready:function(f){this.q.push(f);}};
js=d.createElement(s);fs=d.getElementsByTagName(s)[0];
js.src='https://apis.google.com/js/platform.js';
fs.parentNode.insertBefore(js,fs);js.onload=function(){g.load('analytics');};
}(window,document,'script'));
gapi.analytics.ready(function(){
gapi.analytics.auth.authorize({
container: 'auth-button',
clientid: 'INSERT-CLIENTID'
});
});
Once the Embed API library is fully loaded, the view selector is created as a global variable so any chart or data objects can access it.
gapi.analytics.ready(function(){
viewSelector = new gapi.analytics.ViewSelector({
container: 'view-selector'
});
viewSelector.execute();
});
Uses Template.foo.rendered
to wait for the view selector to load; if it doesn't, the chart div
remains empty. This dashboard contains one chart; others would contain many but more or less the same pattern.
Template.view.rendered = function(){
gapi.analytics.ready(function(){
var dataChart = new gapi.analytics.googleCharts.DataChart({
query: {
metrics: 'ga:sessions',
dimensions: 'ga:date',
'start-date': '30daysAgo',
'end-date': 'yesterday'
},
chart: {
container: 'data-container',
type: 'LINE',
options: {
width: '100%'
}
}
});
viewSelector.on('change', function(ids){
dataChart.set({query: {ids: ids}}).execute();
});
});
};
Turns out to be pretty simple—just needed to add some Meteor sugar to get things behaving properly. Simple explanation: on a change from the viewSelector, set the current view as a Session variable; then add Tracker.autorun()
blocks to grab updates and have the chart update itself. Also, be aggressive using Template.view.created
callbacks to ensure objects load in the right order and aren't being rendered multiple times.
Template.view.created = function(){
gapi.analytics.ready(function(){
var viewSelector = new gapi.analytics.ViewSelector({
container: 'view-selector'
});
viewSelector.execute();
viewSelector.on('change', function(ids){
Session.set('currentView', ids);
});
});
};
Template.users.created = function(){
gapi.analytics.ready(function(){
dataChart = new gapi.analytics.googleCharts.DataChart({
query: {
metrics: 'ga:users',
dimensions: 'ga:date',
'start-date': '30daysAgo',
'end-date': 'yesterday'
},
chart: {
container: 'data-container',
type: 'LINE',
options: {
width: '100%'
}
}
});
Tracker.autorun(function(){
if(Session.get('currentView')){
dataChart.set({query: {ids: Session.get('currentView')}}).execute();
}
});
});
};
I'm sure there's room to improve with this pattern—wrapping each session variable into a Tracker.autorun()
block on every dashboard seems a little superfluous, but keeping all the selectors and data objects locally-scoped is probably a but more secure and resistant to user tampering. Also works with date selection.