meteor and reactJS - how to make child components reactive? - meteor

I have a child component that depends on another widget to set what data it should subscribe to. So effectively a dynamic subscription.
getMeteorData() {
var topicName = this.context.topicName;
var handle = Meteor.subscribe('RebotTopicDetail', {name: topicName});
...
If topicName changes I want to re-subscribe to new data.
What is the best way to do this?
The topicName is being set by another component, and I am using context to pass it between objects.
https://facebook.github.io/react/docs/context.html
I can see the context.topicName is changing as I am displaying it.
But it's not triggering the reactive calculation inside meteor data to rerun.
Is there a way to declare a Deps on a variable within getMeteorData?
My understanding was that was a reactive block so changes would cause the getMeteorData block to re-run.
https://react-in-meteor.readthedocs.org/en/latest/meteor-data/
I also tried passing the topicName in via props, but that also doesn't trigger a reactive update.
for others coming here, related threads
https://github.com/meteor/react-packages/issues/19
https://github.com/meteor/react-packages/issues/66
https://github.com/facebook/react/issues/2517

edit: props seems to be reactive
```
getMeteorData() {
var topicName = this.props.topicName;
var handle = Meteor.subscribe('RebotTopicDetail', {name: topicName});
/// and in the container passing in a prop
<TriggerList topicName={this.state.topicName} />
```
works. not sure if using state of the container item had something to do with it.

Related

Meteor Blaze Data Context becomes undefined

I have a blaze template like this:
{{>jobsheetsTable companyId=companyId}}
In the JS for the template I have the onCreated function like this ...
Template.jobsheetsTable.onCreated(function(){
const instance = this;
instance.companyId = new ReactiveVar();
console.log(instance, instance.data, instance.data.companyId);
if(instance.data.companyId){
instance.companyId.set(instance.data.companyId);
}
}
The issue is that in the console.log statement I notice something odd ...
instance is correctly outputting the instance with the data object and a companyId
instance.data however returns {companyId: undefined}.
I am not changing instance.data anywhere and the function being passed into this template does not change the companyId.
Update: Using meteor 1.6.1.
The onCreated callback is only run once per template creation, so the data you get is the one that is provided to the initial creation (likely with you attribute set to undefined).
It is likely that the data context is changed after the initial rendering, and this does not trigger the function. as the template is not re-created.
If you are certain that you want to track the data context in the onCreated callback, you need to set a reactive dependency on it, using the Template.currentData() reactive data source. Since it needs to be inside a reactive context in order to be re-run when the data changes, you will need to create one, and a convenient method of doing so is via this.autorun(), which stops the computation for you when the template is destroyed.
Template.jobsheetsTable.onCreated(function(){
this.companyId = new ReactiveVar();
this.autorun(() => { // creates a reactive computation
const data = Template.currentData(); // creates a reactive dependency
console.log('data context', data);
if(data.companyId){
this.companyId.set(data.companyId);
}
})
});
The code above contains an autorun block that will re-run whenever the data context changes.

Facebook like load new posts in meteor

I'm in the process of learning meteor. I followed the tutorial to create microscope. If some one submits a post meteor will re render the template for all users. This could be very annoying if there are hundreds of posts then the user will come back to the top of the page and loose track of where he was. I want to implement something similar to what facebook has. When a new post is submitted template isn't rendered rather, a button or link will appear. Clicking it will cause the template to re-render and show the new posts.
I was thinking of using observeChanges on the collection to detect any changes and it does stop the page from showing new posts but only way to show them is to reload the page.
Meteor.publish('posts', function(options) {
var self = this, postHandle = null;
var initializing = true;
postHandle = Posts.find({}, options).observeChanges({
added: function(id, post) {
if (initializing){
self.added('posts', id, post);
}
},
changed: function(id, fields) {
self.changed('posts', id, fields);
}
});
self.ready();
initializing = false;
self.onStop(function() { postHandle.stop(); });
});
Is this the right path to take? If yes, how do I alert the user of new posts? Else, what would be a better way to implement this?
Thank you
This is a tricky question but also valuable as it pertains to a design pattern that is applicable in many instances. One of the key aspects is wanting to know that there is new data but not wanting to show it (yet) to the user. We can also assume that when the user does want to see the data, they probably don't want to wait for it to be loaded into the client (just like Facebook). This means that the client still needs to cache the data as it arrives, just not display it immediately.
Therefore, you probably don't want to restrict the data displayed in the publication - because this won't send the data to the client. Rather, you want to send all the (relevant) data to the client and cache it there until it is ready.
The easiest way involves having a timestamp in your data to work from. You can then couple this with a Reactive Variable to only add new documents to your displayed set when that Reactive Variable changes. Something like this (code will probably be in different files):
// Within the template where you want to show your data
Template.myTemplate.onCreated(function() {
var self = this;
var options = null; // Define non-time options
// Subscribe to the data so everything is loaded into the client
// Include relevant options to limit data but exclude timestamps
self.subscribe("posts", options);
// Create and initialise a reactive variable with the current date
self.loadedTime = new ReactiveVar(new Date());
// Create a reactive variable to see when new data is available
// Create an autorun for whenever the subscription changes ready() state
// Ignore the first run as ready() should be false
// Subsequent false values indicate new data is arriving
self.newData = new ReactiveVar(false);
self.autorun(function(computation) {
if(!computation.firstRun) {
if(!self.subscriptionsReady()) {
self.newData.set(true);
}
}
});
});
// Fetch the relevant data from that subscribed (cached) within the client
// Assume this will be within the template helper
// Use the value (get()) of the Reactive Variable
Template.myTemplate.helpers({
displayedPosts = function() {
return Posts.find({timestamp: {$lt: Template.instance().loadedTime.get()}});
},
// Second helper to determine whether or not new data is available
// Can be used in the template to notify the user
newData = function() {
return Template.instance().newData.get();
});
// Update the Reactive Variable to the current time
// Assume this takes place within the template helper
// Assume you have button (or similar) with a "reload" class
Template.myTemplate.events({
'click .reLoad' = function(event, template) {
template.loadedTime.set(new Date());
}
});
I think this is the simplest pattern to cover all of the points you raise. It gets more complicated if you don't have a timestamp, you have multiple subscriptions (then need to use the subscription handles) etc. Hope this helps!
As Duncan said in his answer, ReactiveVar is the way to go. I've actually implemented a simple facebook feed page with meteor where I display the public posts from a certain page. I use infinite scroll to keep adding posts to the bottom of the page and store them in a ReactiveVar. Check the sources on github here and the live demo here. Hope it helps!

Sessions vars not updating DOM fast enough in Meteor

I noticed this property of Session vars:
test.html
<input name='test' type='text' placeholder='something' value='{{testVal}}'>
test.js (template.test.helpers)
testVal: function(){
return Session.get("testVal");
}
template.test.onRendered
// Sets the initial value of the session var and the form shows "I like cats"
Session.set("testVal", "I like cats");
var displayValue = function(newInput){
Session.set("testVal", newInput);
console.log("session has been set to: ", newInput);
console.log("value of form entry using jQuery: ", $('input[name="test"]').val());
};
When I run displayValue("lalalalal") in Chrome Console I get:
"session has been set to: lalalalal"
"value of form entry using jQuery: I like cats."
The value of the text input does visually change reactively to "lalalalal" after setting the new value for the session variable, but it seems that the DOM update from the session variable isn't quick enough - jQuery still picks up the old value before the reactive change, which implies that reactive updates to the DOM for Session Vars is async.
So this means that if you use reactive variables to auto-update form fields and you're using jQuery to then grab the form values, you need some way to wait until the DOM has been fully updated before you can use jQuery.
Does anyone know any way to do this, ideally in the context of this example?
In this particular example, why not just using the value you're setting the Session var with, ie newInput ?
The problem with your code is that reactive programming on the client is asynchronous by nature : when you set the Session variable to a new value, you're invalidating the reactive computation within the helper responsible for setting the value of the DOM input inside the template markup.
You may use Tracker.afterFlush to let the reactive computation invalidation propagates and rerun the helper, thus really changing the input value that you'll be able to grab back using jQuery.
var displayValue = function(newInput){
Session.set("testVal", newInput);
console.log("session has been set to: ", newInput);
Tracker.afterFlush(function(){
var testValue = $('input[name="test"]').val();
console.log("value of form entry using jQuery: ", testValue);
});
};

What's the Difference between Template.Instance() vs template.data?

When creating a Meteor event handler, what's the difference between...
'click .something': function(e,t){
var data = t.data
}
vs
'click .something': function(e,t){
var data = Template.instance().data
}
They both seem to bring up the same data. Is there a reason why I should one or the other?
Similar question here:
Difference between Template.instance() and this
The thing to realize is that:
In the template life-cycle functions (onCreated, onRendered...) this is equal to Template.instance() so this.data is the same as Template.instance().data AT THAT TIME!
In a helper or event, this is the current data context.
So, note an important thing here: the Data context can change over time if your data changes upstream:
If you pass data to a template, the template will be re-rendered with the new data. New data = new data context.
So if you do something like:
Template.example.onCreated(function() {
this.data.myKey = "my example data set on template creation"; //WRONG!
// or equivalently:
Template.instance().data.myOtherKey = "another key"; //WRONG!
})
well, this data may be under this (i.e. the data context) in your helper (this.myKey) but only as long as the upstream data does not change.
As soon as the upstream data changes, this will be the new data context, and will NOT contain your added data.
So, in summary:
If you need to add context to your template in onCreated or onRendered, make sure you do NOT bind it to the current data context, but to the Template.instance()
you should do:
Template.example.onCreated(function() {
this.myKey = "my example data set on template creation";
// or equivalently:
Template.instance().myOtherKey = "another key";
})
and you can access this data in helper and events via
Template.instance().myKey
It's actually Template.instance() (with a lower i), and as this function returns the current template instance in scope (the one where the event originated), there's no difference with the second parameter of an event handler, which also holds the current template instance, this is why you can access the template data indifferently using Template.instance().data or t.data in an event handler.
There is however a simpler way to access the current data context inside an event handler : the this object is bound to the data context where the event was triggered.

Writing user entered text (input) back into it's document (in mongo)

I have a meteor template rendering a document comming out of mongo db.
Parts of the document are editable (they render as html input elements).
Now I need the data to flow back into the document (and into mongo),
What is the best way to do this ?
The answer is easy if I want to write back the value of doc.a :
doc = {a: "hello"}
it is less easy with : doc.a[0].z
doc = {a: [{z: "hello"}]}
because in order to do the update, the path must be remembered
in order to write the update statement.
Updating the whole document whenever a field changes looks simple,
but inefficient...
It is an extremely common use case, some frameworks (EmberJs) have
magical bindings that modifies the model whenever the widget's value
changes.
How is this done in meteor ?
As you point out, it would probably be inefficient to run a db update command whenever the input changes. This is especially true for draggable elements like sliders.
One thing you can do is separate the db query into a function, and then debounce it using underscore.js, like so (untested):
var debouncedUpdate = _.debounce(function(newObject) {
CollectionName.update({_id: newObject._id},newObject);
},300,true);
Template.objectInput.events({
'keydown #text-input': function(event) {
var newValue = $(this.find('#text-input')).val();
var self = this;
self.value = newValue;
debouncedUpdate(self);
},
});

Resources