I'm trying to understand options for using a stateful / non-reactive DOM component in a Meteor template, in a way that allows the component to retain its state as Meteor updates the DOM.
One specific example involves Leaflet.js: I have an application that includes a Leaftlet map, and I want the user to be able to switch between a display of the map, and some other content. The map is interactive --- the user can pan and zoom in the map --- and I'd like the current zoom/pan state of the map to be retained if/when the user switches away from the map to other content, and then back to the map.
My first attempt at doing this is to put the map in one template, and the other content in another template, and use conditional logic in the containing template to determine which template is rendered:
HTML:
<body>
<div>
<input type="submit" id="mapbutton" value="Display Map">
<input type="submit" id="otherbutton" value="Display Other Stuff">
</div>
{{#if showmap}}
{{> map}}
{{else}}
{{> otherstuff}}
{{/if}}
</body>
<template name="map">
<div id="map"></div>
</template>
<template name="otherstuff">
<p>Here is some other stuff</p>
</template>
JS:
Template.map.rendered = function() {
var map = L.map('map', {
doubleClickZoom: false
}).setView([38.0, -98.0], 5);
L.tileLayer('https://{s}.tiles.mapbox.com/v3/{id}/{z}/{x}/{y}.png', {
maxZoom: 18,
id: 'examples.map-i875mjb7'
}).addTo(map);
};
Session.setDefault("showmap", true);
Template.body.helpers({
"showmap" : function() {
return Session.get("showmap");
}
});
Template.body.events({
"click input#mapbutton": function() {
Session.set("showmap", true);
},
"click input#otherbutton": function() {
Session.set("showmap", false);
}
});
The problem with this approach is that every time the user switches to the map display, Meteor re-renders the map template, creating a new Leaflet map (and associated DOM component), which is initialized from scratch. This means that whatever pan and/or zoom settings the user had previously made in the map are lost. It also involves a short delay in the display while the Leaflet map is constructed. I'd like the Leaflet map to get created one time only, the first time it is displayed, and then saved somewhere off-screen when the user swiches to other content, so that it can be immediately swapped back in later, without incurring the construction delay, and retaining its previous pan/zoom state.
I know that one way to accomplish this would be to design my HTML templates to keep the map div in the DOM when switching displays,
and to use CSS to hide it when necessary. Something like the following:
HTML:
<body>
<div>
<input type="submit" id="mapbutton" value="Map">
<input type="submit" id="otherbutton" value="Other Stuff">
</div>
<div id="map" class="{{#if showmap}}visible{{else}}hidden{{/if}}"></div>
{{#if showother}}
{{> otherstuff}}
{{/if}}
</body>
<template name="otherstuff">
<p>Here is some other stuff</p>
</template>
JS:
Template.body.rendered = function() {
var map = L.map('map', {
doubleClickZoom: false
}).setView([38.0, -98.0], 5);
L.tileLayer('https://{s}.tiles.mapbox.com/v3/{id}/{z}/{x}/{y}.png', {
maxZoom: 18,
id: 'examples.map-i875mjb7'
}).addTo(map);
};
Session.setDefault("showmap", true);
Template.body.helpers({
"showmap" : function() {
return Session.get("showmap");
},
"showother" : function() {
return !Session.get("showmap");
}
});
Template.body.events({
"click input#mapbutton": function() {
Session.set("showmap", true);
},
"click input#otherbutton": function() {
Session.set("showmap", false);
}
});
CSS:
#map.visible {
display: block;
}
#map.hidden {
display: none;
}
This works fine for this simple example, but in reality my application (and the associated templates and resulting DOM) is much more complex.
What I REALLY want is to be able to move the map component around arbitrarily in the DOM. For example, depending on the context, the
map might appear inside a table, or full-screen, or not at all, and I'd like to retain the map's internal state between all of these contexts. Using a Meteor template for the map with conditional logic that determines where it is included seems like a natural way to structure this kind of thing, but that returns to the above problem that every time the map template is rendered, the map is rebuilt from
scratch and reset to its initial state.
Is there a way to tell Meteor to "cache" its rendering of a particular template, and to hang on to the associated DOM element, so that subsequent times when that template is used in the rendering of other content, the previously constructed DOM element is used? I realize this goes against the grain of the reactive approach, but this is a situation where I'm trying to use a complex non-reactive component, and it seems like support for such things could be useful in many contexts.
This issue isn't specific to Leaftlet.js, by the way. I have other non-reactive, stateful components that I would like to use in my Meteor application, and I'd love to find a graceful way to solve this problem for all of them.
Does anyone know if there is a way to do this, or have ideas for a better approach?
Thanks!
I don't think you can keep a rendered item ready for hiding/displaying without any re-rendering, except if you use CSS.
Blaze (the component taking care of rendering templates) can't do that (yet). Have a look at this topic where they basically say the same, but it comes from a meteor dev: https://github.com/meteor/meteor/issues/4351
Either you rely on CSS, either you keep the values you need in for example a reactive dictionary and use them when you render your map template.
Thanks #Billybobbonnet. Your comment to keep the values you need and re-use them when rendering the template gave me the idea to try this:
HTML:
<body>
<div>
<input type="submit" id="mapbutton" value="Map">
<input type="submit" id="otherbutton" value="Other Stuff">
</div>
{{#if showmap}}
{{> map}}
{{else}}
{{> otherstuff}}
{{/if}}
</body>
<template name="map">
<div id="mapcontainer">
<div id="map"></div>
</div>
</template>
<template name="otherstuff">
<p>Here is some other stuff</p>
</template>
JS:
var $mapdiv = undefined;
Template.map.rendered = function() {
if ($mapdiv === undefined) {
// if this is the first time the map has been rendered, create it
var map = L.map('map', {
doubleClickZoom: false
}).setView([38.0, -98.0], 5);
L.tileLayer('https://{s}.tiles.mapbox.com/v3/{id}/{z}/{x}/{y}.png', {
maxZoom: 18,
id: 'examples.map-i875mjb7'
}).addTo(map);
// and hang on to the map's div element for re-use later
$mapdiv = $("#map");
} else {
// map has already been created, so just empty out the container
// and re-insert it
$("#mapcontainer").empty();
$("#mapcontainer").append($mapdiv);
}
};
Session.setDefault("showmap", true);
Template.body.helpers({
"showmap" : function() {
return Session.get("showmap");
}
});
Template.body.events({
"click input#mapbutton": function() {
Session.set("showmap", true);
},
"click input#otherbutton": function() {
Session.set("showmap", false);
}
});
This seems to be working well. It feels a little kludgy, but I like the fact that it lets me put the map in a template which I can use anywhere, just like any other template, and yet the map is only created once.
Related
I have some troubles wihth gridster & meteor. At first, I loaded the whole widgets into my template and then recalculate the grid with a method below.
I have a template named dashboard, in this template I do a loop through my widgets and call a second template called widgetTmpl that contains all the formatted html
<template name="dashboard">
<div id="dashboardBody">
<button id="configMode" class="btn btn-primary"> <i class="fa fa-pencil"></i>Configuration </button>
<div class="gridster">
<ul id="widgetItemList" class="widget_item">
{{#each activeWidgets}}
{{> widgetTmpl}}
{{/each}}
</ul>
</div>
</div>
</template>
I execute this code with the callback onRendered
Template.dashboard.onRendered(function(){
var gridsterUl = jQuery(".gridster ul");
gridsterUl.gridster({
widget_margins: [5, 5],
widget_base_dimensions: [25, 25],
resize : {
enabled : true
},
draggable: {
start: overlay_fix_start,
stop: overlay_fix_stop
},
serialize_params : function($w, wgd){
return {
id : $w.prop('id'),
col : wgd.col,
row : wgd.row,
size_x : wgd.size_x,
size_y : wgd.size_y
};
}
});
});
This works fine, but when I add or reload a widget, I have to refresh the page due to the onRedered callback.
I heard about a gridster.add_widget methods, that does the perfect job but I don't know how to implement it to my code.
Where should I use the gridster.add_widget ?
There is methods like Blaze.insert and Blaze.renderWithData but I have no idea how to use it
I've never tried using Gridster, however I have integrated d3 into quite a few of my meteor apps. When I want the d3 context to reactivity change without a page refresh, I use a Tracker.autorun in my onRendered callback with a reactive datasource. For example my simplified code would look like this:
Template.d3Plot.onRendered( function () {
Tracker.autorun(function() {
// Function That Draws and ReDraws due to Tracker and Reactive Data
drawPlot(Plots.find());
});
});
drawPlot = function (plotItems) {
.....
}
Where Plots is my mongo collection so that whenever a new item is inserted/updated in the collection, the drawPlot function re-fires.
I'm aware that you can turn off reactivity with reactive: false while fetching collections. How to accomplish the same with collection fields within, say, a contenteditable area? Example:
Template.documentPage.events({
'input .document-input': (function(e) {
e.preventDefault();
content = $(e.target).html();
Documents.update(this._id, {
$set: {
content: content
}
});
}
});
<template name="documentPage">
<div class='document-input' contenteditable='true'>{{{content}}}</div>
</template>
to turn of reactivity of e.g. helpers or other reactive functions wrap your function with Tracker.nonreactive(fn). See: http://docs.meteor.com/#/full/tracker_nonreactive
you could set the reactive data to an attribute and move it to the html in onRendered
<template name="documentPage">
<div class='document-input' contenteditable='true' data-content='{{content}}'></div>
</template>
Template.documentPage.onRendered(function () {
var doc = this.find(".document-input");
doc.innerHTML = doc.dataset.content;
});
That way the data-content attribute will be updated by blaze on input not the text of the element.
SECURITY NOTE: I changed the template to use {{content}} instead of {{{content}}} so it can be escaped by meteor but used doc.innerHTML so the content will be "unescaped" in the element. Any time you are rendering html from user input you should make sure you are sanitizing it so you don't open an xss vulnerability in your site.
The alternative would be to use a library like https://github.com/UziTech/jquery.toTextarea.js to change the editable content of a div to text and use doc.textContent in place of doc.innerHTML to remove any chance of xss
Is it possible for a parent Meteor template to access a subtemplate directly? Ideally, I'd like to use templates a widgets with an API. I was hoping for something like this:
mypage.html
<template name="myPage">
<div class="widgetContainer"></div>
<button>submit</button>
</template>
mypage.js
Template.myPage.rendered = function(){
this.myWidgetInstance = UI.render(Template.myWidget)
UI.insert(this.myWidgetInstance, $('.widgetContainer')[0]);
}
Template.myPage.events({
'click button': function(e, template){
// I don't want this logic to live inside of mywidget.js.
// I also don't want this template to know about the <input> field directly.
var val = template.data.myWidgetInstance.getMyValue();
}
});
mywidget.html
<template name="myWidget">
<input></input>
</template>
mywidget.js
Template.myWidget.getValue = function(){
return this.$('input').val();
}
The above doesn't work because myWidgetInstance.getMyValue() doesn't exist. There doesn't appear to be a way for external code to access template helper functions on an instance.
Is anyone using Meteor templates in the way I'm trying to use them above? Or is this better suited for a separate jQuery widget? If so, it'd be a shame because I still want my widget template to benefit from the features Meteor provides.
It is possible to access subtemplate helper function.
Your example will work once you apply few fixes:
fix 1 : getValue() instead of getMyValue()
Template.myPage.events({
'click button': function(e, template){
// I don't want this logic to live inside of mywidget.js.
// I also don't want this template to know about the <input> field directly.
var val = template.myWidgetInstance.getValue();
console.log(val);
}
});
fix 2 : $('input').val(); instead this.$('input').val();
Template.myWidget.getValue = function(){
return $('input').val();
}
fix 3 : <input> should have no close tag.
<template name="myWidget">
<input type="text" value="sample value">
</template>
How do I access one 'sibling' variable in a meteor template helper, when I am in the context of another? I want to determine whether the user that is logged in and viewing the page is the same user that posted the ride offering, so that I can hide or show the "bid" button accordingly.
For example, here is my template (html) file:
<!-- client/views/ride_offers.html -->
<template name="RideOfferPage">
<p>UserIsOwner:{{UserIsOwner}}</p>
{{#with CurrentRideOffer}}
{{> RideOffer}}
{{/with}}
</template>
<template name="RideOffer">
<div class="post">
<div class="post-content">
<p>Details, Author: {{author}}, From: {{origin}}, To: {{destination}}, between {{earliest}} and {{latest}} for {{nseats}} person(s). Asking ${{price}}.
<button type="button" class="btn btn-primary" >Bid</button><p>
<p>UserIsOwner:{{UserIsOwner}}</p>
</div>
</div>
</template>
And here is my JavaScript file:
Template.RideOfferPage.helpers({
CurrentRideOffer: function() {
return RideOffers.findOne(Session.get('CurrentOfferId'));
},
UserIsOwner: function() {
return RideOffers.find({_id: Session.get('CurrentOfferId'), userId: Meteor.userId()}).count() > 0;
}
});
In the "RideOffer" template, I am able access the variables author, origin, ..., price. But I am unable to access the boolean UserIsOwner. I am, however, able to access the boolean UserIsOwner in the "RideOfferPage" template.
Does anyone know how I can access the boolean UserIsOwner in the "RideOffer" template?
Cheers,
Put the userIsOwner function outside the helper as an anonymous function and then call it from both templates.
Template.RideOfferPage.helpers({
CurrentRideOffer: function() {
return RideOffers.findOne(Session.get('CurrentOfferId'));
},
UserIsOwner: checkUserIsOwner()
});
Template.RideOffer.helpers({
UserIsOwner: checkUserIsOwner()
});
checkUserIsOwner= function() {
return RideOffers.find({_id: Session.get('CurrentOfferId'), userId: Meteor.userId()}).count() > 0;
}
There are several ways to do what you're asking.
In your particular example you are not asking about siblings, but about parents, since the RideOfferPage template renders the RideOffer template. You can access variables in the parent data context (but not helpers) like so:
<template name="RideOffer">
<div class="post">
<div class="post-content">
<!--
other stuff
-->
<p>UserIsOwner:{{../UserIsOwner}}</p>
</div>
</div>
</template>
In other cases, you may have a template being rendered as a sibling of this one. In that case, you can't actually know what the sibling is until the template is actually on the page; however, you can find it in the rendered callback:
Template.foo.rendered = function() {
var current = this.firstNode;
var next = $(currentItem).next(); // or .prev()
if (next.length) {
nextData = Spark.getDataContext(next[0]);
}
// Do something with nextData
};
Finally, you can get the parent context of any rendered DOM element by repeatedly iterating through its parents. This isn't super efficient but I've used it in places where there is extensive drag and drop with DOMFragments moving around on the page:
Template.bar.events = {
"click .something": function(e) {
var target = e.target;
var context = Spark.getDataContext(target);
var parentContext = context;
while (parentContext === context) {
parentContext = Spark.getDataContext(target = target.parentNode);
}
// Do something with parentContext
}
};
I'm curious to know if there is a better way to do the last thing, which may potentially have to iterate through many DOM elements. In any case, you may want to check out my meteor-autocomplete package for this and other cool tricks.
Meteor promises reactive updates, so that views are auto-updated when data changes. The included leaderboard example demonstrates this. It runs fine when I test it: data is updated across several browsertabs in different browsers, as expected.
All set and go, I started coding with meteor and progress was being made, but when I tested for reactive updates across browertabs, I noticed that only after a short while the updates across tabs stopped.
I boiled down the problem to the following code, based on a new empty meteor project:
updatebug.html
<head>
<title>updatebug</title>
</head>
<body>
{{> form}}
</body>
<template name="form">
<form onsubmit="return false;">
{{#each items}}
{{> form_item }}
{{/each}}
</form>
</template>
<template name="form_item">
<div>
<label>{{name}}
<input type="text" name="{{name}}" value="{{value}}">
</label>
</div>
</template>
updatebug.js:
Items = new Meteor.Collection("items");
if (Meteor.is_client) {
Template.form.items = function () {
return Items.find();
};
Template.form_item.events = {
'blur input': function(e) {
var newValue = $(e.target).val();
console.log('update', this.name, this.value, newValue);
Items.update({_id: this._id}, {$set: {value: newValue}});
},
};
}
if (Meteor.is_server) {
Meteor.startup(function () {
if (Items.find().count() === 0) {
Items.insert({name: 'item1', value: 'something'});
}
});
}
Run in multiple browsertabs, start changing the value of the input in one tab. The other tabs will reflect the change. Goto the next tab and change the value. Repeat a couple of times.
After a while, no more updates are received by any other tabs. It seems that once a tab has changed the value, it does not receive/show any more updates.
Differences compared to the leaderboard example (since it's very similar):
The leaderboard uses no form controls
The leaderboard example does an increment operation on update, not a set
I am about to file a bug report, but want to be sure I am not doing anything stupid here, or missing an essential part of the Meteor Collection mechanics (yes, autopublish package is installed).
The issue here is input element preservation. Meteor will preserve the input state of any form field with an id or name attribute across a template redraw. The redraw is preserving the old text in your form element, because you wouldn't want to interrupt another user typing in the same field. If you remove the name attribute from the text box, each tab will update on blur.
In fact, I'm not sure why the first update works in your example. That may actually be the bug!
You can see it's not a data problem by opening the console in each browser. On each blur event you will get an updated document in every open tab. (Type Items.find().fetch())