I'm new to meteor.js. Still getting used to it.
I get how templates update reactively according to the cursor updates on the server, like this:
{{#if waitingforsomething.length}} Something Happened! {{/if}}
This is good to display elements on the page, updating lists and content. Now, my question is: what if I want to call some javascript or fire some event when something gets updated reactively? What would be the right way to do it with meteor.js?
Anything inside Tracker.autorun or template instance this.autorun runs with changes in reactive data sources inside these autoruns.
Reactive data sources are ReactiveVar instances, db queries, Session variables, etc.
Template.myTemplate.onCreated(function() {
// Let's define some reactive data source
this.reactive = new ReactiveVar(0);
// And put it inside this.autorun
this.autorun(() => console.log(this.reactive.get()));
});
Template.myTemplate.events({
// Now whenever you click we assign new value
// to our reactive var and this fires
// our console.log
'click'(event, template) {
let inc = template.reactive.get() + 1;
template.reactive.set(inc);
}
});
It is a little bit outdated, but Sacha Greif's Reactivity Basics is a very quick and concise introduction to meteor's reactivity model.
Basically, you have what's called reactive computations, code that observes special data objects (sessions, subscriptions, cursors, etc.) and gets executed whenever any of these reactive sources changes.
This is exposed via the Tracker API
Computation works pretty well for me:
Template.myTemplate.onRendered(function() {
this.computation = Deps.autorun(function () {
if (something) {
$(".reactive").html("Something Happened!");
}
});
});
Template.myTemplate.destroyed = function(){
if (this.computation){
this.computation.stop()
}
};
I Hope this helps.
Related
I've tried to prepare data from an OData source to show it in a bar graph in my fiori app. For this, I setup the OData model in the manifest.json. A test with a list, simply using
items="{path : 'modelname>/dataset'}
works fine and shows the content.
To prepare data for a diagram (VizFrame), I used the onInit() function in the controller of the view (mvc:XMLView). The data preparation is similar to the one discussed in question.
At first I obtain the ODataModel:
var oODataModel = this.getOwnerComponent().getModel("modelname");
Next I do the binding:
var oBindings = oODataModel.bindList("/dataset");
Unfortunately, the oBindings().getContexts() array is always empty, and also oBindings.getLength() is zero. As a consequence, the VizFrame shows only "No Data".
May it be that the data model is not fully loaded during the onInit() function, or do I misunderstand the way to access data?
Thanks in advance
Update
I temporary solved the problem by using the automatically created bind from the view displaying the data as list. I grep the "dataReceived" event from the binding getView().byId("myList").getBindings("items") and do my calculation there. The model for the diagram (since it is used in a different view) is created in the Component.js, and registered in the Core sap.ui.getCore().setModel("graphModel").
I think this solution is dirty, because the graph data depends on the list data from a different view, which causes problems, e.g. when you use a growing list (because the data in the binding gets updated and a different range is selected from the odata model).
Any suggestions, how I can get the odata model entries without depending on a different list?
The following image outlines the lifecycle of your UI5 application.
Important are the steps which are highlighted with a red circle. Basically, in your onInit you don't have full access to your model via this.getView().getModel().
That's probably why you tried using this.getOwnerComponent().getModel(). This gives you access to the model, but it's not bound to the view yet so you don't get any contexts.
Similarly metadataLoaded() returns a Promise that is fullfilled a little too early: Right after the metadata has been loaded, which might be before any view binding has been done.
What I usually do is
use onBeforeRendering
This is the lifecycle hook that gets called right after onInit. The view and its models exist, but they are not yet shown to the user. Good possibility to do stuff with your model.
use onRouteMatched
This is not really a lifecycle hook but an event handler which can be bound to the router object of your app. Since you define the event handler in your onInit it will be called later (but not too late) and you can then do your desired stuff. This obviously works only if you've set up routing.
You'll have to wait until the models metadata has been loaded. Try this:
onInit: function() {
var oBindings;
var oODataModel = this.getComponent().getModel("modelname");
oODataModel.metadataLoaded().then(function() {
oBindings = oODataModel.bindList("/dataset");
}.bind(this));
},
May it be that the data model is not fully loaded during the onInit()
function, or do I misunderstand the way to access data?
You could test if your model is fully loaded by console log it before you do the list binding
console.log(oODataModel);
var oBindings = oODataModel.bindList("/dataset");
If your model contains no data, then that's the problem.
My basic misunderstanding was to force the use of the bindings. This seems to work only with UI elements, which organize the data handling. I switched to
oODataModel.read("/dataset", {success: function(oEvent) {
// do all my calculations on the oEvent.results array
// write result into graphModel
}
});
This whole calculation is in a function attached to the requestSent event of the graphModel, which is set as model for the VizFrame in the onBeforeRendering part of the view/controller.
I have some ko.pureComputed properties that usually hold a big amount of data inside themselves.
When those ko.pureComputed properties go to sleeping state (noone is subscribe to them) I don't need that data anymore until they go back to listening state (someone is subscribe to them).
During that time while they are in the sleeping state I'd like the ko.pureComputed properties to clear their values so that the garbage collector can remove that computed data from memory, then when I need the computed data again, that is, when the ko.pureComputed go back into listening state, I'd like to reevalute the computed data.
Is that possible?
Further details about my use-case scenario:
My site is a Single Page Application, meaning a Javascript framework (Durandal) switches pages (HTML and JS) in display for the user.
Some pages have a need for computed properties which would store large amount of data. I'd like to use ko.pureComputed for that purpose, because it will stop updating itself once the user goes off its page, i.e. once the ko.pureComputed goes into sleep state because it has no more listeners.
(Durandal deattaches and reattaches the page's JS viewmodel from and into the HTML view when the user goes away or visits the page)
The problem is that the ko.pureComputed keeps its latest value cached.
In my case those values are large arrays of large objects, which take up a noticeable amount of memory. I'd like to dispose of that data once it's not needed anymore.
Is there a way to clear the cached value from the ko.pureComputed once it goes into the sleeping state (when the user leaves the page), and then later reinitialize it when the ko.pureComputed goes back to listening state (when the user revisits the page)?
Using a pure computed's state change events, we can tell the computed to clear its value while it's sleeping. Here's a wrapper function that sets it all up:
function computedValueOnlyWhenActive(readFunction) {
var isAwake = ko.observable(false),
theComputed = ko.pureComputed(function () {
if (isAwake()) {
return readFunction();
}
});
theComputed.subscribe(function() {
isAwake(true);
}, undefined, "awake");
theComputed.subscribe(function() {
isAwake(false);
theComputed.peek(); // force reevaluation
}, undefined, "asleep");
return theComputed;
}
Demo: https://jsfiddle.net/mbest/gttosLzc/
This isn't an answer to the specific question you asked, but it might be a more helpful answer depending on your situation.
In Durandal the router plugin navigates by asynchronously loading the specified module with a requireJS call. Once it retrieves the module it checks if the result is either an object or a function, and if it's a function it will instantiate a new object from the function. If it is an object it just uses the object.
RequireJS automatically caches the modules it retrieves in that it doesn't bother re-fetching a module from the server if it's already downloaded it. So if your module definition is a plain object then that same object will get displayed each time.
This module definition will save its state between navigations:
define(['durandal/app'], function (app) {
var title = 'myView';
var vm = {
title: title;
};
return vm;
});
This module definition will create a new object and will re-bind all knockout bindings resulting in a freshly loaded screen on each navigation.
define(['durandal/app'], function (app) {
var title = 'myView';
var vm = function(){
this.title = title;
};
return vm;
});
EDIT:
For a more granular durandal solution that also works with older versions of knockout (i.e. before pureComputed) you can combine the concept in michael best's answer of using an isAwake observable with durandal's view activation and deactivation lifecycle hooks.
function viewModel(){
var self = this;
this.isAwake = ko.observable(true);
this.theComputed = ko.computed(function () {
if (isAwake()) {
return myValue();
}
return "";
});
this.activate = function(){
self.isAwake(true);
}
this.deactivate = function(){
self.isAwake(false);
}
}
var vm = new viewModel();
return vm; //return the instance not the function
http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks.html
I'm creating a location-based chat app in Meteor. Now I want to render only the chat messages which are in the users region. The TheRegion.region variable gets filled with an HTML5 geolocation request.
Template.locationchat.helpers({
messages: function () {
return Messages.find({location: TheRegion.region});
}
});
The problem of this code is that the TheRegion.region variable is still null when this helper is called. Is there a way to run the helper in a callback of the geolocation function? Or run the template helper when the variable has a value?
I often find in Meteor that if you are waiting on a variable all you need is an if clause to protect yourself.
Try this:
Template.locationchat.helpers({
messages: function () {
if(TheRegion.region)
return Messages.find({location: TheRegion.region});
}
});
It doesn't feel natural, but usually it works. Give it a try.
That's because your variable isn't reactive.
In your onCreated:
TheRegion = new ReactiveDict();
TheRegion.set('region',undefined);
Now, region is always going to exist by the time it reaches the helper & when the value changes, your helper will rerun.
I'm not sure if i have completely wrapped my head around this idea - but I'll try my best to clearly describe what I am trying to do here.
I have a factory that changes and parses a URL for me, so I can pass params into a controller for use (that were stored in the url). This is sort of so I can save a state for the user and they can share it via copy'ing of a URL (send it to their friends or bookmark it or w/e).
I am trying to set up a factory (or service) that listens for locationChangeSuccess - so that if the user mofies the url and presses enter, it will refresh the scopes in the controllers. So here is what I have:
.factory("urlFactory", function($location, freshUrl, StateString){
//request to change new url
requestObj.requestState = function(moduleName, stateName, startVar){
}
//request item from url, via your module and state name
requestObj.parseState = function(moduleName, stateName){
}
I dropped the center out (if it is needed im happy to link), but those just get and set the url for me.
So in the controllers I do something like
$scope.mod2m3 = urlFactory.parseState("module2", "mod3");
$scope.mod2m4 = urlFactory.parseState("module2", "mod4");
So when they land on the page, they pull their state. This works great. However, now i'm trying to solve some edge case scenarios where maybe the user modifies the url.
So I can latch onto that even pretty easily with
.factory("urlWatcher", function($location, $scope){
var urlWatcher = {};
$scope.$on('$locationChangeSuccess', function(event) {
console.log("Asdsa");
});
return urlWatcher
});
However, where I am struggling is trying to determine a way where when this fires, it would connect the new value to the scope in the controller. It was suggested to me that a callback of some sort in the parse (set) function, but I am struggling with how to approach that. It would be super cool if I could set a way for this factory/service to re send the new value when it changes to the right place. Callback sounds good, however I don't know how to config this correct.
The easiest route would be to just do an
$scope.$on('$locationChangeSuccess', function(event) {
console.log("Asdsa");
});
In each controller and manually bind to each scope, but I am trying to make this as modular as possible (and thats also a ton of watchers on the locationchangesuccess). would be fantastic if I could figuire out a clean way to set the service/factory to listen once, and on change find the right module/controller and change the value.
I can't seem to think a clear route, so I would be very greatful for any insight to this issue. Thank you very much for reading!
If what you want is a publish/subscribe architecture, where publications are global and subscriptions have the same lifecycles as Angular scopes... then Angular events are what you're looking for. There's no point setting up an ad hoc communication system with callbacks and whatnut, that would just be partially reinventing events.
However, if you want to make the semantics more obvious / add flexibility, you can listen once to $locationChangeSuccess in a service and broadcast a custom event.
$rootScope.$on("$locationChangeSuccess", function (event) {
$rootScope.$broadcast('myCustomeEvent', {message: "Guys, time to refresh!"});
});
Then listen to this event in each of the scopes where it is relevant.
$scope.$on('myCustomeEvent', function (event) {
console.log("Asdsa");
});
If setting up the listening gets repetitive, by all means, factor it out in a function, which you can for example put in a service:
myApp.factory('someFactory', [function () {
return {
listenToLogAsdsa: function (scope) {
scope.$on('myCustomeEvent', function (event) {
console.log("Asdsa");
});
}
};
}]);
Then all you have to write in your controller is:
someFactory.listenToLogAsdsa($scope);
You can assign a variable in the scope to an object in the factory, that way it's bound to a reference instead of a value. Then, in your HTML you bind the reference to the DOM. urlFactory.parseState() should then save the result to said object, and return the key where it was saved.
For example:
In urlFactory:
requestObj.parseState = function(moduleName, stateName){
var key = moduleName+stateName;
this.urlContainer[key] = "www.example.com";
return key;
}
In the controller:
$scope.urls = urlFactory.urlContainer;
$scope.mod2m3 = urlFactory.parseState("module2", "mod3");
In your HTML:
{{urls[mod2m3]}}
This way, "urls" is bound to a reference, which angular watches for changes, and whenever you change urls[mod2m3], it will affect the DOM.
You can also just react to changes in the scope variables by watching them:
$scope.$watch('urls', function() {
//do something
});
NOTE: Since this is an object, you might need to use $watchCollection instead of $watch.
My publications:
Meteor.publish('items', function() {
return Items.find({}, {skip: randomNumber, limit: 100});
});
My subscription code happening when a button is clicked (in templates.event)
Meteor.subscribe('items');
Items.find().fetch();
However, the problem is that items isn't refreshed with the new data, but new data is appended to the old list instead. What can I do to unsubscribe the old data?
When you call Meteor.subscribe it will return a subscription handle.
You call stop on the handle to cancel it.
eg, in your event helper
if (SomeGlobalVar){
SomeGlobalVar.stop();
}
SomeGlobalVar = Meteor.subscribe('items');
The other way is to run the subscription inside Deps.autorun; it will automatically clean up old subscriptions.
eg, in you event handler
Session.set('subscribe', true);
Elsewhere in your code:
Deps.autorun(function(){
if (Session.get('subscribe')){
Meteor.subscribe('items');
}
});
If you call Session.set('subsribe', false); Meteor will automatically cancel/clean-up that subscription to items.
Seems to me like there is a bit of a confusion here, when you are connecting to a reactive data source you are not subscribing to specific data, so when new records are added ofcourse they are appended to your data through that connection.
If I understood your question and you want the new data to completely replace the old data I advise you not to build your template around the Collection but rather build it around a cached version of the data (which will kept in an object and made reactive using Deps), that way you will have total control over the data displayed.