Dynamically showing templates in Meteor without re-rendering/re-creating template - javascript

Long time user of SO but first time poster.
I have a Meteor web application where a user is presented with a page with a single input. Once they action it, the following section appears below. This process repeats.
Journey.html
<head>
<title>Page Journey</title>
<script type="text/javascript">
function showValue(newValue)
{
document.getElementById("range").innerHTML=newValue;
}
</script>
</head>
<body>
{{> Page0}}
{{> Page1}}
{{> Page2}}
{{> Page3}}
</body>
<template name="Page0">
<h1>This is Page 0!</h1>
<p>Welcome to Journey App.</p>
<input type="range" min="1" max="10" value="1" step="1" onchange="showValue(this.value)" />
<span id="range">1</span>
</template>
<template name="Page1">
{{#if showpage1}}
<h1>This is Page 1!</h1>
<p>{{page1copy}}</p>
<input type="button" value="Show Page 2" />
{{/if}}
</template>
<template name="Page2">
{{#if showpage2}}
<h1>This is Page 2!</h1>
<p>{{page2copy}}</p>
<input type="button" value="Show Page 3" />
{{/if}}
</template>
<template name="Page3">
{{#if showpage3}}
<h1>This is Page 3!</h1>
<p>{{page3copy}}</p>
{{/if}}
</template>
Journey.js
//On initialization, set position to 0.
if (Meteor.isClient) {
Meteor.startup(function () {
Session.set("position",0);
});
}
if (Meteor.isClient) {
Template.Page0.events({
'click input': function () {
var value = getValue();
if (value>5) { Session.set("position",1); }
else { Session.set("position",0); }
}
});
Template.Page1.events({
'click input': function () {
//Increment position
Session.set("position",2);
}
});
Template.Page2.events({
'click input': function () {
//Increment position
Session.set("position",3);
}
});
}
if (Meteor.isClient) {
Template.Page1.showpage1 = function () {
return Session.get("position") > 0;
}
Template.Page2.showpage2 = function () {
return Session.get("position") > 1;
}
Template.Page3.showpage3 = function () {
return Session.get("position") > 2;
}
}
if (Meteor.isClient) {
Template.Page1.rendered = function(){
console.log ('Page 1 rendered');
};
Template.Page2.rendered = function(){
console.log ('Page 2 rendered');
};
Template.Page3.rendered = function(){
console.log ('Page 3 rendered');
};
}
function getValue()
{
var value = parseInt(document.getElementById("range").innerHTML);
return value;
}
Am I going about this all wrong?
I want to fire off a function (google analytics tracking code, for example) every time that a new page loads. The .rendered() function gets fired when the page loads up the first time around, so this isn't ideal.

One way is to listen to the change of your session variable like this:
if (Meteor.isClient) {
Deps.autorun(function(){
var position=Session.get("position");
console.log("POSITION HAS CHANGED TO "+position);
});
}
On another level, I think you could make your life easier by moving your conditions in the same template and using a helper. Like this:
<body>
{{>pages}}
</body>
<template name="pages">
{{> Page0}}
{{#if showpage 1}}
{{> Page1}}
{{/if}}
{{#if showpage 2}}
{{> Page2}}
{{/if}}
{{#if showpage 3}}
{{> Page3}}
{{/if}}
</template>
And in your .js file:
if (Meteor.isClient) {
Template.pages.showpage = function (p) {
return (Session.get("position")>=p);
};
}
It has the additionnal effect that your "rendered" callbacks will only be called if the template is actually on screen.
Though, when the 3 is rendered, 1 and 2 will also be called because the session variable (position) will trigger the whole pages template to refresh.
So the log output will look like this
POSITION HAS CHANGED TO 0
POSITION HAS CHANGED TO 1
Page 1 rendered
POSITION HAS CHANGED TO 2
Page 1 rendered
Page 2 rendered
POSITION HAS CHANGED TO 3
Page 1 rendered
Page 2 rendered
Page 3 rendered
If you want your callback to be fired once when the template appears on the page, but not when it is refreshed, you can use Template.pageX.created instead of Template.pageX.rendered.
POSITION HAS CHANGED TO 0
POSITION HAS CHANGED TO 1
Page 1 created
Page 1 rendered
POSITION HAS CHANGED TO 2
Page 2 created
Page 1 rendered
Page 2 rendered
POSITION HAS CHANGED TO 3
Page 3 created
Page 1 rendered
Page 2 rendered
Page 3 rendered
Because all your templates depends on the same session variable, everytime it changes, all your templates are re-rendered.
If you want to avoid that, The only solution I think of is to use different session variables for each subpage. Be prepared, it's gonna get a bit ugly, but I really don't know other ways :)
if (Meteor.isClient) {
function setPosition(a){
// only touch the following pages
for(var i=3; i>a; --i)
Session.set("position-"+i,false);
Session.set("position-"+a,true);
}
Meteor.startup(function () {
setPosition(0);
});
Template.Page0.events({
'click input': function () {
var value = getValue();
if (value>5) { setPosition(1); }
else { setPosition(0); }
}
});
Template.Page1.events({
'click input': function () {
//Increment position
setPosition(2);
}
});
Template.Page2.events({
'click input': function () {
//Increment position
setPosition(3);
}
});
Template.pages.showpage = function (p) {
return (Session.get("position-"+p));
};
}
If you do just this, it will not work yet, because the pages template will now depends on the three session variables, so it will be entirely re-rendered upon modification of any of those variables.
You need to split your templates so that they depends on only one session variable each. But we want to be able to keep the helper that we made before, right ?
So we can use the {{#isolate}} helper that will virtually separate some parts of our template.
{{#isolate}}
{{> Page0}}
{{/isolate}}
{{#isolate}}
{{#if showpage 1}}
{{> Page1}}
{{/if}}
{{/isolate}}
{{#isolate}}
{{#if showpage 2}}
{{> Page2}}
{{/if}}
{{/isolate}}
{{#isolate}}
{{#if showpage 3}}
{{> Page3}}
{{/if}}
{{/isolate}}
Now the log is like this:
Page 1 created
Page 1 rendered
Page 2 created
Page 2 rendered
Page 3 created
Page 3 rendered

Related

Template content not updating on consecutive calls Angular

Very new to Angular and after searching all over the show I simply cannot find a solution to my problem.
I have the following function in a directive/controller:
ModalIssueController.prototype.openModal = function (e, issue) {
this._dataService.getMain().then(function (model) {
this._$scope.modalIssue.open = true;
this._$scope.modalIssue.issue = model.getIssueById(issue);
this._windowService.setModalOpen(true);
}.bind(this));
};
The above function is called each time the user clicks on a different issue from a list. This opens a modal and shows the content related to issue.
When the modal is closed via a close button, the following is called:
ModalIssueController.prototype.closeModal = function () {
this._$scope.modalIssue.open = false;
this._windowService.setModalOpen(false);
this._$timeout(function () {
this._$location.url('/');
}.bind(this));
};
The problem is, even though I can see that the value of this._$scope.modalIssue.issue changes to reflect the new issue that was clicked, the content in the modal never changes, but instead, continues to show the data from the first selected issue ;(
Am I missing something here? Is there an additional step I need to add to ensure that the data in the template is updated?
Here is the directive 'set-up':
var ModalIssueDirective = function () {
return {
restrict: 'A',
replace: true,
scope: true,
controller: ModalIssueController,
templateUrl: '/app/lorax/directives/modal-issue.tpl.html'
};
};
And here is the template I am populating:
<section class="modal modal--fade-show modal--issue" ng-show="modalIssue.open" >
Close
<h1 class="detail-header-title">{{::modalIssue.issue.getTitle()}}</h1>
<div class="detail-main__copy">{{::modalIssue.issue.getNarrative()}}</div>
<header class="detail-link__header">
<h1>{{::modalIssue.issue.getMiscLocale().mozDoingLabel}}</h1>
</header>
<p class="detail-link__copy">{{::modalIssue.issue.getMozActionCopy()}}</p>
<a ng-if="::modalIssue.issue.getMozActionLink().length === 1" href="{{::modalIssue.issue.getMozActionLink()[0].url}}" class="btn detail-link__btn">{{::modalIssue.issue.getMozActionLink()[0].copy}}</a>
<a ng-if="::modalIssue.issue.getMozActionLink().length > 1" ng-repeat="link in ::modalIssue.issue.getMozActionLink()" href="{{link.url}}" class="detail-link__multiple">{{link.copy}}<span class="icon-arrow-right"></span></a>
<header class="detail-link__header">
<h1>{{::modalIssue.issue.getMiscLocale().yourDoingLabel}}</h1>
</header>
<p class="detail-link__copy">{{::modalIssue.issue.getYourActionCopy()}}</p>
<a ng-if="::modalIssue.issue.getYourActionLink().length === 1" href="{{::modalIssue.issue.getYourActionLink()[0].url}}" class="btn detail-link__btn">{{::modalIssue.issue.getYourActionLink()[0].copy}}</a>
<a ng-if="::modalIssue.issue.getYourActionLink().length > 1" ng-repeat="link in ::modalIssue.issue.getYourActionLink()" href="{{link.url}}" class="detail-link__multiple">{{link.copy}}<span class="icon-arrow-right"></span></a>
</section>
Thank you in advance for any assistance that can be provided here.
So, turns out :: in Angular templates defines a one-time binding. This essentially means that as soon as, for example, the following expression has been run:
{{::modalIssue.issue.getTitle()}}
and it returned a value that is not undefined, it is considered stable and the expression will never be run again. So, removing :: from each of the relevant lines in the template resolved the issue.
Docs: https://docs.angularjs.org/guide/expression (#see One-Time Binding)

ParentContext in childTemplate - Meteor

I tried to detect which template includes another, in order to have different css classes for specific template inclusions. I already asked this question here.
The suggested solution is like this:
app.html:
<body>
{{> parentTemplate parentContext}}
</body>
<template name="parentTemplate">
{{> childTemplate specialContext}}
{{> childTemplate}}
</template>
<template name="childTemplate">
<div class="{{isSpecialClass}}">
<p>parent name: {{name}}</p>
</div>
</template>
app.js
if (Meteor.isClient) {
Template.body.helpers({
// add some context to the parent do demo how it can be modified
parentContext: {name: 'dave'}
});
Template.parentTemplate.helpers({
specialContext: function () {
// make a copy of the parent data context
var data = _.clone(Template.instance().data || {});
// modify the context to indicate the child is special
data.isSpecial = true;
return data;
}
});
Template.childTemplate.helpers({
isSpecialClass: function () {
// grab the context for this child (note it can be undefined)
var data = Template.instance().data;
if (data && data.isSpecial)
// add the 'awesome' class if this child is special
return 'awesome';
}
});
}
Now the problem is that my childTemplate has the context of parentTemplate. I checked the data of parentTemplate and it has the field isSpecial, it just has the wrong context. Any idea why this happens? For instance, if I use {{title}} in my childTemplate I will get the title of the parent context object, but I want the context of the childTemplate.
I misunderstood the original question. My answer was overly complex because I thought the parent context had to be preserved. It's actually a bit easier if you just need to modify the child context. Here's a working example:
app.html
<body>
{{> parentTemplate}}
</body>
<template name="parentTemplate">
{{#each children}}
{{> childTemplate}}
{{/each}}
</template>
<template name="childTemplate">
<div class="{{isSpecialClass}}">
<p>name: {{name}}</p>
</div>
</template>
app.js
if (Meteor.isClient) {
Children = new Mongo.Collection(null);
Meteor.startup(function () {
Children.insert({name: 'joe'});
Children.insert({name: 'bob'});
Children.insert({name: 'sam'});
});
Template.parentTemplate.helpers({
children: function () {
// find all of the children and modify the context as needed
return Children.find().map(function(child, index) {
// modify the child context based on some aspect of the child or index
if ((index == 0) || (child.name == 'bob'))
child.isSpecial = true;
return child;
});
}
});
Template.childTemplate.helpers({
isSpecialClass: function () {
// add the 'awesome' class if this child is special
if (this.isSpecial)
return 'awesome';
}
});
}
In this version, the parent finds all of the children and modifies each by adding isSpecial to the child context only if the child is either first in the list or if the child has the name 'bob'. Now, the child only needs to check this.isSpecial in its class helper. Please let me know if you have any questions.

Specifying a default flatiron-director route (inside element) via polymer core-pages component

This question is directly related to: flatiron-director / core-pages SPA with route specific js functions & default route . I'm sure that solution works, but I'm a little too inexperienced with polymer (and js) to determine the correct event listener in my circumstance:
How/where would you specify an appropriate event listener to set the default route if the flatiron-director is used inside a polymer element, particularly when the element's template itself does not use is="auto-binding". In this case, and to be clear, the index.html page which imports the element shown below does in fact specify a template using is="auto-binding".
Here is the element code to show what I am attempting to communicate / achieve. The flatiron routing is working (if I manually enter #itemsList or #itemOpen into the URL and use browsers previous or next buttons), but it does not add the default #itemsList to the URL automatically when hitting index.html on its own:
<polymer-element name="my-app" attributes="user items connected">
<template>
<flatiron-director id="page-director" route="{{route}}" autoHash on-director-route="{{ routeChanged }}"></flatiron-director>
<!-- HIGH LEVEL APP LAYOUT ELEMENT -->
<core-header-panel id="appHeader" mode="standard">
<!-- OUTER APP TOOLBAR ELEMENT -->
<core-toolbar id="appToolbar">
<paper-icon-button id="navicon" icon="arrow-back" on-tap="{{ showItems }}"></paper-icon-button>
<span flex>App Name</span>
<paper-icon-button id="searchbutton" icon="search"></paper-icon-button>
</core-toolbar>
<!-- MAIN CONTENT ELEMENTS -->
<!-- ATTEMPT FLATIRON ROUTING -->
<core-pages id="mainPages" selected="{{route}}" valueattr="name">
<my-items-element name="itemsList" on-core-activate="{{ itemSelect }}" user="{{user}}" items="{{items}}" item="{{item}}"></my-items-element>
<item-open-scaffold-element name="itemOpen" user="{{user}}" item="{{item}}" hidden></item-open-scaffold-element>
</core-pages>
</core-header-panel>
</template>
<script>
Polymer('my-app', {
route: "itemsList",
itemSelect: function(e, detail, sender) {
if (sender.shadowRoot.activeElement == null || sender.shadowRoot.activeElement.nodeName != "PAPER-MENU-BUTTON"){
// Ensure the user hasn't clicked on the item menu dropdown to perform alternative actions (or another element with actions for that matter)
// (i.e. make sure the user intends to open the item)
this.openItem();
}
},
openItem: function() {
this.$.mainPages.children.itemOpen.hidden = false;
this.$.mainPages.selected = "itemOpen";
//this.route = "scaffoldPage";
},
showItems: function() {
this.$.mainPages.children.itemOpen.hidden = true;
this.$.mainPages.selected = "itemsList";
}
});
</script>
<script>
var template = document.querySelector('template');
template.addEventListener('template-bound', function() {
this.route = this.route || "itemsList";
});
</script>
As noted by Jeff, use ready() lifecycle method as intra-element equivalent to template-bound event outside of element. So...based on the example above, its as simple as including the following line within polymer element's ready():
this.route = this.route || "itemsList"

Meteor : wait until all templates are rendered

I have the following template code
<template name="home">
<div class="mainBox">
<ul class="itemList">
{{#each this}}
{{> listItem}}
{{/each}}
</ul>
</div>
</template>
<template name="listItem">
<li class="item">
{{username}}
</li>
</template>
And I'd like to execute a code once ALL of the "listItem" are rendered. There are about 100 of them. I tried the following
Template.home.rendered = function() {
// is this called once all of its 'subviews' are rendered?
};
But it doesn't wait until all views are loaded.
What's the best way of knowing when all sub-view templates are loaded?
This is how I proceed :
client/views/home/home.html
<template name="home">
{{#if itemsReady}}
{{> itemsList}}
{{/if}}
</template>
<template name="itemsList">
<ul>
{{#each items}}
{{> item}}
{{/each}}
</ul>
</template>
<template name="item">
<li>{{value}}</li>
</template>
client/views/home/home.js
Template.home.helpers({
itemsReady:function(){
return Meteor.subscribe("items").ready();
}
});
Template.itemsList.helpers({
items:function(){
return Items.find();
}
});
Template.itemsList.rendered=function(){
// will output 100, once
console.log(this.$("li").length);
};
lib/collections/items.js
Items=new Mongo.Collection("items");
server/collections/items.js
insertItems=function(){
var range=_.range(100);
_.each(range,function(index){
Items.insert({value:"Item "+index});
});
};
Meteor.publish("items",function(){
return Items.find();
});
server/startup.js
Meteor.startup(function(){
Items.remove({});
if(Items.find().count()===0){
insertItems();
}
});
We specify that we want to render our list of items only when the publication is ready, so by that time data is available and the correct number of li elements will get displayed in the list rendered callback.
Now the same using iron:router waitOn feature :
client/views/home/controller.js
HomeController=RouteController.extend({
template:"home",
waitOn:function(){
return Meteor.subscribe("items");
}
});
client/lib/router.js
Router.configure({
loadingTemplate:"loading"
});
Router.onBeforeAction("loading");
Router.map(function(){
this.route("home",{
path:"/",
controller:"HomeController"
});
});
client/views/loading/loading.html
<template name="loading">
<p>LOADING...</p>
</template>
Using iron:router is probably better because it solves a common pattern elegantly : we don't need the itemsReady helper anymore, the home template will get rendered only when the WaitList returned by waitOn will be globally ready.
One must not forget to add both a loading template and setup the default "loading" hook otherwise it won't work.
I had this same problem with needing to wait on all my subtemplates to load before calling a slick JavaScript carousel plugin (or any cool JavaScript plugin like charts or graphs that need your whole data set loaded in the DOM before calling it).
I solved it by simply comparing the rank of the subtemplate to the overall count that should be returned for whatever query I was doing. Once the rank is equal to the count, you can call your plugin from the subtemplate.rendered helper because all the subtemplates have been inserted into the DOM. So in your example:
Template.listItem.rendered = function() {
if(this.data.rank === ListItems.find({/* whatever query */}).count()) {
console.log("Last item has been inserted into DOM!");
// Call your plugin
$("#carousel").owlCarousel({
// plugin options, etc.
});
}
}
Then you just need your helper for listItems to return a rank, which is easy enough:
Template.home.helpers({
listItems: function() {
return ListItems.find({/* whatever query */}).map(function(listItem, index) {
listItem.rank = index + 1; // Starts at 1 versus 0, just a preference
});
}
}
the method rendered works of this way
This callback is called once when an instance of Template.myTemplate is rendered into DOM nodes and put into the document for the first time.
so, when is rendered you doesn't have variable reactive in this case.
// this would sufficient
Template.listItem.helpers = function() {
username:function(){
return ...
}
};
I'd suggest something like:
var unrendered = [];
Template.listItem.created = function () {
var newId = Random.id();
this._id = newId;
unrendered.push(newId);
};
Template.listItem.rendered = function () {
unrendered = _.without(unrendered, this._id);
if (!unrendered.length) {
// WHATEVER NEEDS DOING WHEN THEY'VE ALL RENDERED
}
};
CAVEAT
This works on the assumption that essentially all template instances will be created before they first ones have been rendered, otherwise your code will run before it should. I think this should be the case, but you'll have to try it out as I don't really have time to run a 100+ sub-template test. If it's not the case, then I can't see how you can achieve this behavior without knowing in advance exactly how many sub-templates will be created.
If you do know how many there will be then the code above can be simplified to a counter that decrements every time rendered runs, and it's easy.
unrendered = [number of listitems];
Template.listItem.rendered = function () {
unrendered--;
if (!unrendered) {
// WHATEVER NEEDS DOING WHEN THEY'VE ALL RENDERED
}
};
Also, you may need to meteor add random, but I think this package is now included in core.
Apparently there are various ways to handle your situation. You could easily use template subscriptions.
Template.myView.onCreated(function() {
var self = this;
self.autorun(function(){
self.mySub = self.subscribe('mySubscription');
});
if(self.mySub.ready()) {
// my sweet fancy code...
}
});
<template name="myTemplate">
<ul>
{{#if Template.subscriptionsReady}}
{{#each items}}
<li>{{item}}</li>
{{/each}}
{{else}}
<div class="loading">Loading...</div>
{{/if}}
</ul>
</template>

Meteor Framework - how handle order change

It's list sorted by likes.
Meteor template and reactivity system automatically rerender html template whenever data defined by cursor(barvy.find({}, {sort:{likes: -1}})) changes. So now on screen list is always sorted by 'likes'. If first item has 50 likes, second item 50 likes and i add one like to second item, then it moves to first position on the screen, because cursor returns it like first item.
My question is: how i can show arrow up on items that moves up in ordered list and arrow down on items that moves down? Because creating of DOM element is handled by Meteor, i don't know how to get info about what elements changes their positions.
Template.poradi.barvy = function () {
return barvy.find({}, {sort:{likes: -1}});
};
Html template:
<body>
{{> poradi}}
</body>
<template name="poradi">
<h2>Poradi</h2>
<ul>
{{#each barvy}}
<li>{{barva}}, {{likes}} <input type="button" id="button_{{barva}}" value="like" /></li>
{{/each}}
</ul>
</template>
I would like to try something to store the old position with an unique id and compare it against the new position. You could write a extra function by extending your template e.g. (not tested code, but logic should work):
Template.HelloWorld.getArrow = function(uniqueId, currentPosition) {
if(typeof array[uniqueId] == 'undefined') { // If there is no old data
array[uniqueId] = currentPosition;
return "same.png";
}
oldPosition = array[uniqueId];
if(oldPosition < currentPosition) {
arrow = "up.png";
}
else if(oldPosition > currentPosition) {
arrow = "down.png";
}
else {
arrow = "same.png";
}
array[uniqueId] = currentPosition;
return arrow;
};
And thats how to call it in your template "HelloWorld":
<img src="{{getArrow "itemId" "positionNumber"}}">
Every time the data in your collection is changing, the template is redrawn and so the function getArrow would be recalled for every item.
If I understand the question correctly, you want to know how each click corresponds to a rendered item. There are a couple of ways to do this, but the easiest is just to render each list item in a separate template. Here is a complete working example:
likes.html
<body>
<h1>Items to like</h1>
{{> itemsList}}
</body>
<template name="itemsList">
<ul>
{{#each items}}
{{> item}}
{{/each}}
</ul>
</template>
<template name="item">
<li>
{{text}} ({{likes}})
<button class='up'>Up</button>
<button class='down'>Down</button>
</li>
</template>
likes.js
if (Meteor.isClient) {
Items = new Meteor.Collection(null);
Meteor.startup(function () {
Items.insert({text: 'apples', likes: 10});
Items.insert({text: 'grapes', likes: 8});
Items.insert({text: 'pears', likes: 6});
Items.insert({text: 'oranges', likes: 4});
});
Template.itemsList.items = function () {
return Items.find({}, {sort: {likes: -1}});
};
Template.item.events({
'click .up' : function () {
Items.update(this._id, {$inc: {likes: 1}});
return false;
},
'click .down' : function () {
Items.update(this._id, {$inc: {likes: -1}});
return false;
}
});
}
I've used a collection local to the client for easy initialization, and so it will reset every time you refresh the page. The key insight is that if you render each item in its own template, the click events refer only to that item, so you have access to this._id.

Categories

Resources