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.
Related
While this is not the exact code, it represents the issue. I have an object that has a method that returns an array. This array is then updated asynchronously. In the demo it is a timeout but in reality it would be an HTTP call:
function () {
let a = ['apple'];
setTimeout(() => {
a.push['banana'];
}, 3000);
return a;
}
Then it is included in a Vue component via a computed property and displayed, e.g.
<ul>
<li v-for="(row, index) in testObjectArr" v-bind:key="index">
{{row}}
</li>
</ul>
However this only displays the first entry (apple) and not the second (banana). If an event does cause the template to refresh then it does appear.
Using things like watch also doesn't work. The only way to get it working is to pass a handler in to the object that manually triggers a redraw which is oviously less than ideal.
Is there any way to get responsivity to actually work with the above situation?
In order to make variables reactive, you should declare them as such by using ref or reactive. So your code should be:
<script setup>
import { ref } from 'vue';
const items = ref(['apple']);
setTimeout(() => {
items.value.push('banana');
}, 1000);
</script>
<template>
<ul>
<li v-for="(item, index) of items" :key="index">
{{ item }}
</li>
</ul>
</template>
See it live
I have a simple popup component that get's called on a click event in some places in my app. The popups get created on click and are pushed in an array, that is later forEach-ed and displayed so that when the user clicks on a button multiple times multiple popups are displayed. The success ones disappear and the red ones disappear once someone clicks on the X, I achieve that by splicing them from the array, but if a user generated 5 popups and clicks the X the next popup gets closed, not the one that the user has clicked on, so it seems that I am not targeting the specific popup order in the array but the next, how can I properly target the exact clicked modal so that it gets closed and not the one that is next in line, what am I doing wrong here? How should I splice the array properly so that I target the exact pushed modal and not the rest?
Here is my code:
<template>
<div class="main">
<div
v-for="item in array"
:key="item.id"
:class="['popUp', `popUp--type--${item.popUpTypeType}`]"
>
<div class="popUp-side">
<p class="exclamation-mark">!</p>
</div>
<h5 class="popUp-message">{{item.message}}</h5>
<div class="popUp-side">
<p class="closing-x" #click="closeMessage" v-if="item.popUpTypeType === 'danger'">X</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'popUp',
data: () => ({
array:[],
}),
methods: {
removepopUpType() {
var self = this;
var id = Math.round( Math.random()*1e13 );
var index = self.array.map(x => {
return x.id;
}).indexOf(id);
self.array.splice(index, 1);
},
callpopUpType(obj){
var newpopUpType = {
popUpTypeType: obj.type,
message: obj.message,
id: obj.id
};
if(obj.type === 'success'){
setTimeout(this.removepopUpType, 2000)
}
this.array.push(newpopUpType);
},
closeMessage() {
this.removepopUpType()
},
},
created() {
this.$root.$on('call-popUp', this.callpopUpType);
},
destroyed(){
this.$root.$off('call-popUp', this.callpopUpType);
}
}
</script>
There is quite a bit of unusual code in the example, so I may be missing something, but couldn't you just pass the item to the method.
<p
v-if="item.popUpTypeType === 'danger'"
class="closing-x"
#click="closeMessage(item)"
>X</p>
and then
closeMessage(item) {
const index = this.array.indexOf(item);
if (index >= 0) this.array.splice(index, 1)
},
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.
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>
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