I am making a card game. Players take turn. The player can not throw any card, but is limited by the previous card thrown. If previous player throws 7 of clubs, current player can only throw clubs. (more complicated than this, but...)
I solved this by having two css classes, cardRegular and cardThrowable where cardThrowable moves the card up onmousehover, indicating to the user that the card can be thrown.
I have defined these events:
Template.card.events({
"click .cardThrowable": function () {
console.log("click .cardThrowable " + this);
},
"click .cardRegular": function () {
console.log("click .cardRegular " + this);
}
});
When I open the page, both events work just fine in both browsers (for two player), however, once one player throws his card I have a problem.
The second players cards are properly updated and only allowed cards have the cardThrowable class, but neither event is fired.
If I simply refresh the page, then the events are properly fired, but then the other user has the same issue. Until he refreshes the page...
I am totally lost as to what may be causing this bug.
EDIT:
Cards template
<template name="cards">
<div class="cardsDIV">
{{#each playerCards}}
{{> card}}
{{/each}}
</div>
</template>
<template name="card">
{{{card this}}}
</template>
...
Template.card.helpers({
card: function () {
return '<img src="' + cardImage(this.cardName) +
'" style="position:absolute;bottom:0;left:' + this.left + 'px" class="' + this.css + '" />';
}
});
I think the event handlers are not attached because the way you are adding the cards.
I don't quite understand how you are getting the values for css and left but I assume by the way you use them that they are values in the card document
Try something like this HTML:
<template name="card">
<img class="{{this.css}} js-playCard" src="{{this.cardName}}" style="position:absolute;bottom:0;left:{{this.left}}px" />
</template>
And this JS:
Template.card.events({
'click .js-playCard'(event, instance) {
console.log("clicked " + this.css);
}
});
Related
I have two separate form input's (both text type), one in template A, and one in template B. Template A invokes template B. All the specific names/properties of these two input form's are unique. I have event handlers for both, within their own properly named Template.name.events().
When I build a very simple test case of this, no problems, everything works fine. But in my larger and more complex actual app, when I enter text into the template B form, the correct template B submit event handler gets invoked. And then...the template A submit event handler gets invoked! This happens even when I do nothing but an event.PreventDefault call in handler B (side question: are event handlers ever invoked for reactive reasons, or strictly "event occurred" reasons?). I am able to work around this odd behavior for the moment by checking in the A event handler for an undefined name property and just exiting if that's the case, but that's just a kludge for something wrong somewhere. Any suggestions as to a likely culrprit for this odd behavior in my code? Thanks!
Here's the code for the two templates in the failing case; the first (entryHall, with the "new-room" form input) is the "A" template, the second (knock, with the "knock-room" form input) the "B" template. Underneath the event handling code for those two templates is the html+handlebars code for the template definitions and invocations. Sorry for the verbosity and lack of a simpler failing case!
Template.entryHall.events({
// NEW ROOM REQUEST PROCESSING
"submit.new-room": function (event) {
event.preventDefault();
if (event.target.roomName === undefined) {
console.log ("in submit new room and roomname in event is undefined");
return;
}
var rName = event.target.roomName.value;
// Is there already a room name of this same name in the Rooms collection?
var roomsCursor = Rooms.findOne({ roomName: rName });
if (roomsCursor != null) {
// It's a dup; don't allow it
event.target.roomName.value = "Duplicate room name, try again";
return(null);
}
var uName = Session.get ('userName');
// It's a unique name, put it in the Rooms collection.
Rooms.insert({
roomName : rName,
owner : uName,
members: [], // an array of user names
knockRequests: [], // an array of user names
chat : null,
files : null
});
// We have the room document added to the Rooms collection, now we have to
// add the room to the owned list for the user
var userEntry = PZUsers.find({ userName : uName }).fetch();
PZUsers.update({ _id : userEntry[0]._id},
{ $push: { ownedRooms: rName }});
ownedRoomCount++;
roomReactor.changed();
event.target.roomName.value = "";
}
});
Template.knock.events({
// Knock on a room request processing
"submit.knock-room": function (event) {
// Prevent default browser form submit
event.preventDefault();
var knockName = event.target.knockName.value;
event.target.knockName.value = "";
console.log("in knock room submit!");
// Can only knock on a room that exists!
var knockRoomCursor = Rooms.findOne({ roomName: knockName });
if (knockRoomCursor == null) {
console.log ("no such room found to knock on");
return;
}
// Add a knock request to this room, and add this room the the user's list of "open knocks" rooms
var roomEntry = Rooms.find({ roomName : knockName }).fetch();
console.log ("_id of room: " + knockName + " is: " + roomEntry[0]._id);
Rooms.update({ _id : roomEntry[0]._id },
{ $push: { knockRequests: Session.get('userName') }});
roomReactor.changed();
}
});
And here's the invoking html:
<template name="entryHall">
<h2>Welcome {{userName}}</h2>
<h3>Create a new room:</h3>
<div class="roomName">
<form class="new-room">
<input type="text" name="roomName" id="roomName" placeholder="Select a room name" />
</form>
</div>
{{markNoOwnedRooms}}
{{#each ownedRooms}}
{{#if firstOwnedRoom}}
<h3>Enter one of your own rooms:</h3>
{{/if}}
{{ > room }}
{{/each}}
{{markNoMemberRooms}}
{{#each memberRooms}}
{{#if firstMemberRoom}}
<h3>Enter one of your member rooms:</h3>
{{/if}}
{{ > room }}
{{/each}}
<h3>Knock to request entry:</h3>
{{ > knock }}
</template>
<template name="room">
<li>{{this}}</li>
</template>
<template name="knock">
<div class="knockName">
<form class="knock-room">
<input type="text" name="knockName" id="knockName" placeholder="Enter room name" />
</form>
</div>
</template>
I have chat app, and if you use Slack you know that when you enter to the room, you will automatically find yourself at the bottom of your chat room.
So I decided to do this and what I have
Template.room.onCreated(function() {
console.log($(document).height(),$(window).height());
$('html, body').scrollTop($(document).height()-$(window).height());
});
it output 437 , 437
BUT when I do this in console:
$('html, body').animate({ scrollTop: $(document).height()-$(window).height() + 64}, "fast");
it outputs 2000,437 , and that means that my messages is not loaded fully in my template. (If someone want more code, just ask.)
Any idea how to build this ?
EDIT:
This part of template.html
<div class="messages-wrap">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
{{#if haseMoreMessages}}
<div class="loadmore text-center" id="incLimit">Load More</div>
{{/if}}
{{#if Template.subscriptionsReady }}
{{#each messages}}
{{> message}}
{{/each}}
{{/if}}
</div>
</div>
</div>
</div>
And template.js (part of it)
Template.room.onRendered(function() {
Session.setDefault('messageLimit', 200);
});
Template.room.onCreated(function() {
var self = this;
self.autorun(function() {
if (self.subscriptionsReady()) {
Tracker.afterFlush(function () {
$('html, body').scrollTop($(document).height() - $(window).height());
});
}
});
});
Template.room.events({
'click #incLimit' : function(e,template){
Session.set('messageLimit',Session.get('messageLimit') + 100);
}
});
Template.room.helpers({
messages: function(){
return Messages.find({},{sort:{createdAt:1}});
},
haseMoreMessages:function(){
if (Session.get('messageLimit') > Messages.find().count()) return false;
return true;
},
});
This is one very frustrating aspect of Blaze. Try this, though:
Template.room.onRendered(function () {
var template = this;
this.autorun(function () {
if (template.subscriptionsReady()) {
Tracker.afterFlush(function () {
$('html, body').scrollTop($(document).height() - $(window).height());
});
}
});
});
This waits till all the template subscriptions are ready first, and then waits till any computations are fully complete (Tracker.afterFlush), and then executes the scroll. Tracker.afterFlush is usually necessary if your template has {{#if}} blocks that depend on other things before they get evaluated and rendered.
UPDATE:
Without seeing all your code and knowing why or when you want to scroll to the top, it's hard to say what you're aiming for. But see the Meteorpad link below for a working version of what you were trying to do with the message limits (I'm only incrementing the limit by 1 since there are 3 messages).
Meteorpad Link
A few things you should note:
Set default variables and subscribe to things in Template.x.onCreated, not Template.x.onRendered.
You forgot to actually subscribe to your collection.
Limit messages on the server side, in the Meteor.publish callback.
Different Approach via tracking of Template helpers:
I took advantage of the Template helper, as it already tracks reactively all changes (and new) messages. Hence, there you can place your scroll-down command.
Template.room.helpers({
messages: function(){
//put your scroll-up command here
//as this helper is keeping track on changes already
$('html, body').scrollTop($(document).height() - $(window).height());
return Messages.find({},{sort:{createdAt:1}});
},
haseMoreMessages:function(){
if (Session.get('messageLimit') > Messages.find().count()) return false;
return true;
},
});
Hence, you save the resources to have an additional tracker observing the collection (you would be double-tracking the same collection).
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.
I am trying to implement searching in my Meteor app. I don't exactly understand how it ties together. At this point, I have this following code:
html:
<form class="navbar-search pull-left">
<input type="text" class="search-query" placeholder="Search">
</form>
js:
Template.menubar.events({
'keyup input.search-query': function (evt) {
console.log("Keyup value: " + evt.which);
if (evt.which === 13) {
console.log("Got an Enter keyup");
Session.set("searchQuery", "justATestVar");
}
}
});
I can see the values of keyup as I press different keys into the search box, so I know the event is being hit. Capturing the "enter" keyup also works, but pressing enter causes the enter site to reload and when I do:
Session.get("searchQuery")
it returns undefined.
I don't know if I'm handling this properly. Essentially, I just want to get the value from the search box and then use that value for making a search on my collection. Any help would be appreciated! Thank you.
You should really use a submit button for your search form to avoid ruining accessibility.
Using a submit button will also enable by default the behavior you're looking for : form submission on enter key pressed.
If you really want to get rid of the submit button, keep it in the DOM but use CSS to hide it.
It's very important to call preventDefault on the event you'll receive from "submit form" handler, if you forget to do so, the page will refresh ruining the meteor "Single Page App" experience (and by the way, page refresh will clear your Session variables, which is why you get an undefined value in the first place).
"submit form":function(event,template){
event.preventDefault();
Session.set("searchQuery",template.find(".search-query").value);
}
What is probably happening is your form is being submitted when you hit enter. Try an preventDefault(). Probably something like this would work:
Template.menubar.events({
'keyup input.search-query': function (evt) {
console.log("Keyup value: " + evt.which);
if (evt.which === 13) {
console.log("Got an Enter keyup");
Session.set("searchQuery", "justATestVar");
}
},
'submit form': function (evt) {
evt.preventDefault();
}
...
You could also try adding evt.preventDefault(); in your keyup but I think it's the form submission that's doing it.
In case anyone got here trying to implement the search function as well, I recommend the following:
meteor add matteodem:easy-search
On client and server:
Players = new Meteor.Collection('players');
// name is the field of the documents to search over
Players.initEasySearch('name');
On the client, make a template.html:
<template name="searchBox">
{{> esInput index="players" placeholder="Search..." }}
<ul>
{{#esEach index="players"}}
<li>Name of the player: {{name}}</li>
{{/esEach}}
</ul>
</template>
Reference
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())