Have been using https://github.com/acreeger/meteor-moment in meteor and it works well, however is there a way to make the output of moment reactive so that it counts up "3 seconds ago", "4 seconds ago", etc?
Rather than using a new Session variable for each individual timer, I would create a single Tracker.Dependency which is flagged for changes every second (or perhaps every 10 seconds), then depend on this whenever you want to depend on the current time.
var timeTick = new Tracker.Dependency();
Meteor.setInterval(function () {
timeTick.changed();
}, 1000);
fromNowReactive = function (mmt) {
timeTick.depend();
return mmt.fromNow();
}
Template.example.helpers({
example: function () {
return fromNowReactive(moment().startOf('hour'));
}
});
This is the approach taken by mizzao:timesync, which is a useful package you can use if those fromNows are based on server-side timestamps. One reason to not use client-generate timestamps is that these may be out of sync, resulting in strings like 5 seconds from now for a post that was just made. mizzao:timesync allows server-generated timestamps to be used everywhere, and also groups different reactivity intervals together efficiently.
You can now use the package copleykj:livestamp. (github | atmosphere)
Install it like this:
meteor add copleykj:livestamp
It has a dependency on momentjs:moment so it will bring that along automatically. It installs a universal helper that is available anywhere and can be passed a date object.
You can use it in a template like this:
<li>Regular: {{date}} </li>
<li>Livestamp: {{livestamp date}}</li>
Here's a working demo in MeteorPad
Thanks for the replies everyone, I found a mrt package which does the job atmospherejs.com/package/livestamp
Use setTimeout and Session to store your variable.
Something like this (in your lib file):
var updateTime = function () {
var time = moment().startOf('hour').fromNow(); // 22 minutes ago
Session.set('timeFromNow', time);
setTimeout(updateTime, 60 * 1000); // 60 seconds
});
updateTime();
Then, in your template:
Template.t.timeFromNow = function () {
return Session.get('timeFromNow');
}
When setTimeout triggered to update Session variable, the template is updated.
Related
I use Cloud Functions for Firebase for some server-side-tasks. Using the database-trigger onWrite() I experience some unexpected behaviour.
exports.doStuff = functions.database.ref('/topic/{topicId}/new').onWrite((event) => {
// If data, then continue...
if (event.data.val()){
// doStuff
//
} else {
console.log("started, but no content!");
}
When new data is added to the specified folder the function is started at least once without any new content ("started, but no content!" is logged to the console). Sometimes even two, three or four times. Then it's run again, automatically (a couple of seconds later) and this time everything works as expected.
EDIT:
The code that writes to the specified node is as follows:
functionA(topicId){
return this.db.object('/topic/'+topicId+'/new').update({
timestamp: firebase.database.ServerValue.TIMESTAMP
});
}
The timestamp is only set once. So before that operation is called, the node new does not exist. So there is no edit or delete. However, this means, by calling the above function first the node new is created, some miliseconds later timestamp and then the value for timestamp. Does Firebase call the onWrite() function for each of these events?
Does this make sense to anybody? Any idea how to make sure, that the function is only executed, when there is really new data available?
onWrite() has a new format, since version 1.0 of Firebase SDK
Your original code was based on tutorials or help from an earlier, beta, version of Firebase.
exports.doStuff = functions.database.ref('/topic/{topicId}/new')
.onWrite((event) => {
if (event.data.val()){
// do stuff
}
}
However since version 1.0, the first parameter in the onWrite function is a "change" object which has two properties: before and after. You want the after. You can get it as follows:
exports.doStuff = functions.database.ref('/topic/{topicId}/new')
.onWrite((change) => {
if (change.after.val()){
// do stuff
}
}
Source: https://firebase.google.com/docs/reference/functions/functions.Change
I don't know how else to word this, but basically I'm implementing a datepicker, so the user can choose the range for which the data is displayed. The user picks a start data and ending date and with that, I re-run a gigantic function that is located in the lib folder to re-run and change all the data that is displayed via Meteor helpers on the main page.
The dates that the user picks are stored in Session variables, which are accessed in the function that I intended. The function runs, but no changes are displayed on client (but these changes are true in the console and I can see the changes being made via console.log statements I have throughout the function).
This is what the datepicker's onRendered function looks like:
Template.dashboard.onRendered(function(){
// Date picker
$(function() {
function cb(start, end) {
$('#reportrange span').html(start.format('MMMM D, YYYY') + ' - ' + end.format('MMMM D, YYYY'));
var startDate = start.format('MMMM D, YYYY');
Session.set("startingDate", startDate)
var endDate = end.format('MMMM D, YYYY');
Session.set("endingDate", endDate);
}
var firstDate = dates[0];
var lastItem = dates.length-1;
var lastDate = dates[lastItem]
cb(moment(firstDate), moment(lastDate));
$('#reportrange').daterangepicker({
ranges: {
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
'This Month': [moment().startOf('month'), moment().endOf('month')]
}
}, cb);
});
});
The Tracker.autorun:
Tracker.autorun(function(){
libFxn();
});
libFxn() is the rather large function in the lib folder that I call in the Tracker. So, whenever one of the Session variable changes due to user input, the Tracker.autorun fires and function is run and values are being changed, which I am able to see via console. However, on client, I don't see the changes.
That leaves me in a dilemma: I need to show the user the resulting data changes based on the input, but:
1) Changes are not seen in client, even though function in lib folder is being executed.
2) I can't use document.location.reload(true); or refresh the page in any way because when the page refreshes, the session variables are restored to default values (which is first date and last date of the dates array that I have on hand).
So I need to figure out a way to send the user's date input data to the function in the lib folder in a way that will show the changes in the client/template that doesn't involve Sessions if the page has to be refreshed.
If anyone can give me hints or tips, I would be grateful.
Here is an example of one helper, which is basically identical to all others minus the different variables it calls (all these variables are in the libFxn() function and are populated there and called via these helper functions):
WinRate: function(){
if(Number((((wins/gamesPlayed))))){
return numeral((wins/gamesPlayed)).format('0%');
} else if(Number((((wins/gamesPlayed)))) === 0){
return "0%"
} else{
return "n/a";
}
}
From comments above you are not making the variables themselves reactive. You can do this using the Tracker.Dependency.
In your lib file you will want to use globalsDep = new Tracker.Dependency; or similar, you will probably want to have one for each type of outcome from your function i.e. if you can modify 10 variables independently then you will want 10, a new dependency for each one otherwise you will re-run every helper that depends on them each time any value changes. if you want everything to re-run of course just use one:
globalsDep = new Tracker.Dependency;
Each place you modify the relevant variable (or at the end of your function if you only want one dependency) you need to tell the dependency that it has become invalid and needs to recompute
globalsDep.changed();
And then in each of the helpers you want to rerun you call the depends function:
globalsDep.depends()
And you should see them running straight away in the view. Simple example below:
/****************
** In Lib File **
****************/
globalsDep = new Tracker.Dependency;
xDep = new Tracker.Dependency;
x = 15;
y = 10;
t = 0;
myBigLongFunction = function(){
x = x + 5;
y = y + 1;
t = x + y;
console.log('Changing Values', x, y, t);
globalsDep.changed();
if (x > 20)
xDep.changed();
}
/****************
** In JS File **
****************/
Template.main.helpers({
testGlobalReactive: function(){
globalsDep.depend();
console.log('All vars rerun');
return {t:t, x:x, y:y};
},
testXReactive: function(){
xDep.depend();
console.log('X rerun');
return x;
}
});
/****************
** In HTML File **
****************/
<template name="main">
<div style="min-height:200px;width:100%;background-color:lightgrey;">
{{#with testGlobalReactive}}
X:{{x}}<br><br>
Y:{{y}}<br><br>
T:{{t}}<br><br>
{{/with}}
X Individual: {{testXReactive}}
</div>
</template>
Although I would caution against having client state in this way, you would be better leveraging the reactivity of collections and ensuring everything is synched with the server through them, having this sort of data stored on the client will not be persistent anywhere and cannot be trusted in any manner as client can modify global variables at will. If you are already setting this data from collections in the function ignore the last but you may still want to consider accessing the data either in iron router data field or at a template level direct from collection as it will be reactive by default without need for the Tracker.dependency :D
I thought it'd be easy but, yeah... it wasn't. I already posted a question that went in the same direction, but formulated another question.
What I want to do
I have the collection songs, that has a time attribute (the playing-time of the song). This attribute should be handled different in the form-validation and the backend-validation!
! I'd like to do it with what autoform (and simple-schema / collection2) offers me. If that's possible...
in the form the time should be entered and validated as a string that fits the regex /^\d{1,2}:?[0-5][0-9]$/ (so either format "mm:ss" or mmss).
in the database it should be stored as a Number
What I tried to do
1. The "formToDoc-way"
This is my javascript
// schema for collection
var schema = {
time: {
label: "Time (MM:SS)",
type: Number // !!!
},
// ...
};
SongsSchema = new SimpleSchema(schema);
Songs.attachSchema(SongsSchema);
// schema for form validation
schema.time.type = String // changing from Number to String!
schema.time.regEx = /^\d{1,2}:?[0-5][0-9]$/;
SongsSchemaForm = new SimpleSchema(schema);
And this is my template:
{{>quickForm
id="..."
type="insert"
collection="Songs"
schema="SongsSchemaForm"
}}
My desired workflow would be:
time is validated as a String using the schema
time is being converted to seconds (Number)
time is validated as a Number in the backend
song is stored
And the way back.
I first tried to use the hook formToDoc and converted the string into seconds (Number).
The Problem:
I found out, that the form validation via the given schema (for the form) takes place AFTER the conversion in `formToDoc, so it is a Number already and validation as a String fails.
That is why I looked for another hook that fires after the form is validated. That's why I tried...
2. The "before.insert-way"
I used the hook before.insert and the way to the database worked!
AutoForm.hooks({
formCreateSong: {
before: {
insert: function (doc) {
// converting the doc.time to Number (seconds)
// ...
return doc;
}
},
docToForm: function (doc) {
// convert the doc.time (Number) back to a string (MM:SS)
// ...
return doc;
}
}
});
The Problem:
When I implemented an update-form, the docToForm was not called so in the update-form was the numerical value (in seconds).
Questions:
How can I do the way back from the database to the form, so the conversion from seconds to a string MM:SS?
Is there a better way how to cope with this usecase (different data types in the form-validation and backend-validation)?
I am looking for a "meteor autoform" way of solving this.
Thank you alot for reading and hopefully a good answer ;-)
I feel like the time should really be formatted inside the view and not inside the model. So here's the Schema for time I'd use:
...
function convertTimeToSeconds (timeString) {
var timeSplit = timeString.split(':')
return (parseInt(timeSplit[0]) * 60 + parseInt(timeSplit[1]))
}
time: {
type: Number,
autoValue: function () {
if(!/^\d{1,2}:?[0-5][0-9]$/.test(this.value)) return false
return convertTimeToSeconds(this.value)
}
}
...
This has a small disadvantage of course. You can't use the quickForm-helper anymore, but will have to use autoForm.
To then display the value I'd simply find the songs and then write a helper:
Template.registerHelper('formateTime', function (seconds) {
var secondsMod = seconds % 60
return [(seconds - secondsMod) / 60, secondsMod].join(':')
})
In your template:
{{ formatTime time }}
The easy answer is don't validate the string, validate the number that the string is converted into.
With simpleschema, all you do is create a custom validation. That custom validation is going to grab the string, turn it into a number, and then validate that number.
Then, when you pull it from the database, you'll have to take that number & convert it into a string. Now, simpleschema doesn't do this natively, but it's easy enough to do in your form.
Now, if you wanted to get fancy, here's what I'd recommend:
Add new schema fields:
SimpleSchema.extendOptions({
userValue: Match.Optional(Function),
dbValue: Match.Optional(Function),
});
Then, add a function to your time field (stored as Date field):
userValue: function () {
return moment(this.value).format('mm:ss');
},
dbValue: function () {
return timeToNumber(this.value);
}
Then, make a function that converts a timeString to a number (quick and dirty example, you'll have to add error checking):
function timeToNumber(str) {
str.replace(':',''); //remove colon
var mins = +str.substr(0,2);
var secs = +str.substr(2,2);
return mins * 60 + secs;
}
Then, for real-time validation you can use schema.namedContext().validateOne. To update the db, just send timeToNumber(input.value).
In my application I have posts, and in my post page I'm showing posted date as a minute ago using moment package.
I'm displaying dates using the following code
Template.registerHelper("postedTime",function(date){
return moment(date).fromNow();
});
and in my HTML
{{#with post}}
.............
............
{{postedTime date}}
............
............
{{/with}}
I know these dates are not reactive. In my post I have hundreds of comments also with the same date format.
What is the best way to update all those timings without much load to the client browser?
Dates are not reactive by themselves, so you need to include a reactive data source in your helper to force it to rerun.
In this example, we'll update a session variable that will force all instances of postedTime to be reevaluated every 60 seconds:
Template.registerHelper('postedTime', function(date) {
Session.get('timeToRecompute');
return moment(date).fromNow();
});
setInterval(function() {
Session.set('timeToRecompute' new Date);
}, 60 * 1000);
Is it possible yet, to preserve insertion order or set reliable timestamps in Meteor given that MongoDB doesn't guarantee to return items in insertion order if no sort is specified, a document's _id is randomly generated and setting a timestamp manually on insertion would depend upon the client's clock?
I suggest a method.
Meteor.methods({
addItem: function (doc) {
doc.when = new Date;
return Items.insert(doc);
}
});
While the client will run this locally and set when to its own current time, the server's timestamp takes priority and propagates to all subscribed clients, including the original client. You can sort on doc.when.
We'll probably add hooks for setting timestamps automatically as part of document validations and permissions.
If you're willing to use something like these collection hooks (https://gist.github.com/matb33/5258260), along with this fancy Date.unow function (which you can safely sort on even if many documents were inserted with the same timestamp):
if (!Date.unow) {
(function () {
var uniq = 0;
Date.unow = function () {
uniq++;
return Date.now() + (uniq % 5000);
};
})();
}
if (Meteor.isServer) {
// NOTE: this isn't vanilla Meteor, and sometime in the future there may be
// a better way of doing this, but at the time of writing this is it:
Items.before("insert", function (userId, doc) {
doc.created = Date.unow();
});
}