I have a model "User" calling an API with an ID returning information such as desired pagination, language, role etc.
When I perform a fetch on the model's instance, it works fine. I know I need to wait for the fetch to be finished, but how do I keep the variables I'd potentially define on success callback visible to the rest of application?
var user = new App.Models.User();
user.fetch().done(function() {
//when I define fruit_list here, it's not visible to the outside world
});
// fruit_list view uses the information retrieved from the user model (pagination), which falls into the default because fetch wasn't performed yet
var fruit_list = new App.Views.FruitList();
Any thoughts? What is the best practice for storing global user information?
Many thanks!
If visibility to outside world is your concern, this can be one option.
You already have a global variable App declared for your application. You can create a separate namespace for model and view instances as well. For e.g.
App.models = {
user : new App.Models.User()
};
App.views = {
fruit_list : new App.Views.FruitList()
};
App.models.user.fetch().done(function() {
// do your stuff with 'user' model and 'fruit_list' view
// App.views.fruit_list
});
Related
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 have an app model and apps have an id and name.
this.resource("apps", function() {
this.route('show', { path: ':app_id' });
});
I'd like to make the show view show metrics about the app, but the query is pretty intense, so I don't want to include it in the call to the index view (let's say this is a table).
I'm not sure if this is possible with ember-data because the app would already be in the store with the simplified payload and not be re-requested for the show view to get the metrics.
Then my head went to making metrics a completely different model accessible from apps/1/metrics and then making it another model and everything.
But if I sideload the data, i have to provide ID references to the metrics for a particular app. And it's hasOne so there's not really IDs as there would be for a database backed model.
What's the best way to load in additional data about a model or expand the information supplied in the show view?
The backend is Rails and this is an ember-cli project.
The easiest way is to retrieve the data in the Route's afterModel handler:
var ShowRoute = Ember.Route.extend({
model: function(params) {
// Load the model and return it.
// This will only fire if the model isn't passed.
},
afterModel: function(model, transition) {
// Load the rest of the data based on the model and return it.
// This fires every time the route re-loads (wether passed in or via model method).
}
});
For example I have the following server routes set up for my user entity:
GET /users/ // gets collection of users
GET /users/:id // gets user :id
GET /users/me // gets the current user
At the beginning of my app I want to get the current user from the server and store it... Something along the lines of:
App.addInitializer(function () {
$.get('/users/me')
.done(function processCurrentUser (userJson) {
App.user = new User(userJson);
});
});
My question is where this API call should actually reside. Would it be better to have something along the lines of:
App.addInitializer(function () {
App.user = new User();
App.user.fetchMe(); // performs the api call above
});
Or should I be doing something inside of a controller?
Thanks for the help!
When doing a fetch, I always worry about how its asyn behavior is going to affect the components that depend on that data. If there are no downriver components that will need the data before it can be reasonably expected to return, then there's technically nothing wrong with your approach.
There is, however, another possible way of loading your globals. What I often do (and for a user's list, too, it so happens) is bootstrap the data to the initial load page. I generally load it on the window variable. So for your example, in your backend template,
<script>
window.globals = {};
window.globals.currentUser = #Html.Raw(Json.Encode(ViewBag.User))
</script>
Of course, you can replace #Html.Raw(Json.Encode(ViewBag.User)) (we use C#) with your favorite backend model.
Then in your app start you're guaranteed to have the models:
App.addInitializer(function () {
App.user = new User(window.globals.currentUser);
});
First off, some background
My client has a kind of a "split-view", meaning- a side-panel displaying a list of objects and a main view displaying the selected object's details. Every time the user clicks on an Object in the list, a Backbone's route is called to navigate to the id which updates a "selected" property on the Session, what causes the main view to update- pretty standard stuff.
The problem
I want the client to be as responsive as possible, therefore i'm trying to utilize Meteor's abillity to update the client immediately without waiting for a server confirmation.
My goal is that every time an Object is created, the list and the main view will be instantly updated to reflect the newly added Object. To achieve this I created a Meteor.method, create(), that uses Collection.insert and returns the id so I can use it with my Route. The method is shared across the client and server and is being called from within a template's event handler.
My first try was to store the returned id in a variable in the event handler and update the Route in the next line; For some reason, that didn't work because the method returned an undefined value. So I tried a different approach, instead of returning the id, I used it within the method to update the Route directly (if Meteor.isClient of course). That didn't work either because the id returned by Collection.insert in the client's version of the method was different from the one in the server's version.
First approach
Template.createDialog.events({
'click #btn-dialog-create': function (event, template) {
var objectId = Meteor.call('create');
appRouter.navigate("object/id/" + objectId, {trigger:true});
}
});
Second approach
Meteor.methods({
create: function () {
var ObjectId = Objects.insert({name:'test'});
if(Meteor.isClient){
appRouter.navigate("object/id/" + objectId, {trigger:true});
}
}
});
If anyone knows what's going on and can give me some directions that would be great.
Any different approaches to the problem or suggestions would be much appreciated as well.
Thanks
Update
So I tried #Pent's suggestion and I got the same result as with my second approach. For some odd reason Meteor decides to ignore my id (created with Random.id()) and inserts the object with a different one.
So I tried another approach, I used just a simple string value instead of Random.id() and voila - it worked. Riddle me that.
Answer updated:
This will be both a client and server method:
Meteor.methods({
create: function () {
var id = Random.id();
Objects.insert({_id: id, name:'test'});
if(this.isSimulation) {
appRouter.navigate("object/id/" + id, {trigger:true});
}
}
});
You can view a similar pattern from Meteor's party example: https://github.com/meteor/meteor/blob/b28c81724101f84547c6c6b9c203353f2e05fbb7/examples/parties/model.js#L56
Your problem is coused by the fact that remote methods, i.e. those which will be called on the server, don't simply return any value. Instead, they accept a callback that will be used to process the returned value (see docs). So in your first example you should probably do something like this:
Template.createDialog.events({
'click #btn-dialog-create': function (event, template) {
Meteor.call('create', function (error, result) {
if (!error)
appRouter.navigate("object/id/" + result, {trigger:true});
});
}
});
You also said:
I want the client to be as responsive as possible, therefore i'm trying to utilize Meteor's abillity to update the client immediately without waiting for a server confirmation.
I think that in this case you should definitely wait for server response. Note, that there is no chance you get the correct object id unless this is given to you by the server.
One possible way to get around this issue is to create a local (client-side) collection:
// only on client
var temporary = new Meteor.Collection(null); // null name
in which you could store your "temporary" newly created objects, and then save them to the "real" collection after the user clicks the save button. You could implement your router to respond to urls like object/new/* to get access to these objects before they're saved to your database.
The correct answer for this question is defining a client side method that's responsible for creating the unique id (preferably using Random.id() ) and calling the Meteor.methods' create(). That way, you can have the id available immediately without waiting for the server to generate one. The trick here is to generate the id outside of the Meteor.method so that the id generation happens only once for both the stub and the actual server method.
create = function(){
var id = Random.id();
Meteor.call('create', id);
return id;
}
Meteor.methods({
create: function (id) {
Objects.insert({_id: id, name:'test'});
//more code...
}
});
//and in the Template...
Template.createDialog.events({
'click #btn-dialog-create': function (event, template) {
var objectId = create();
appRouter.navigate("object/id/" + objectId, {trigger:true});
}
});
I have a collection that can potentially contain thousands of models. I have a view that displays a table with 50 rows for each page.
Now I want to be able to cache my data so that when a user loads page 1 of the table and then clicks page 2, the data for page 1 (rows #01-50) will be cached so that when the user clicks page 1 again, backbone won't have to fetch it again.
Also, I want my collection to be able to refresh updated data from the server without performing a RESET, since RESET will delete all the models in a collection, including references of existing model that may exist in my app. Is it possible to fetch data from the server and only update or add new models if necessary by comparing the existing data and the new arriving data?
In my app, I addressed the reset question by adding a new method called fetchNew:
app.Collection = Backbone.Collection.extend({
// fetch list without overwriting existing objects (copied from fetch())
fetchNew: function(options) {
options = options || {};
var collection = this,
success = options.success;
options.success = function(resp, status, xhr) {
_(collection.parse(resp, xhr)).each(function(item) {
// added this conditional block
if (!collection.get(item.id)) {
collection.add(item, {silent:true});
}
});
if (!options.silent) {
collection.trigger('reset', collection, options);
}
if (success) success(collection, resp);
};
return (this.sync || Backbone.sync).call(this, 'read', this, options);
}
});
This is pretty much identical to the standard fetch() method, except for the conditional statement checking for item existence, and using add() by default, rather than reset. Unlike simply passing {add: true} in the options argument, it allows you to retrieve sets of models that may overlap with what you already have loaded - using {add: true} will throw an error if you try to add the same model twice.
This should solve your caching problem, assuming your collection is set up so that you can pass some kind of page parameter in options to tell the server what page of options to send back. You'll probably want to add some sort of data structure within your collection to track which pages you've loaded, to avoid doing unnecessary requests, e.g.:
app.BigCollection = app.Collection.extend({
initialize: function() {
this.loadedPages = {};
},
loadPage: function(pageNumber) {
if (!this.loadedPages[pageNumber]) {
this.fetchNew({
page: pageNumber,
success: function(collection) {
collection.loadedPages[pageNumber] = true;
}
})
}
}
});
Backbone.Collection.fetch has an option {add:true} which will add models into a collection instead of replacing the contents.
myCollection.fetch({add:true})
So, in your first scenario, the items from page2 will get added to the collection.
As far as your 2nd scenario, there's currently no built in way to do that.
According to Jeremy that's something you're supposed to do in your App, and not part of Backbone.
Backbone seems to have a number of issues when being used for collaborative apps where another user might be updating models which you have client side. I get the feeling that Jeremy seems to focus on single-user applications, and the above ticket discussion exemplifies that.
In your case, the simplest way to handle your second scenario is to iterate over your collection and call fetch() on each model. But, that's not very good for performance.
For a better way to do it, I think you're going to have to override collection._add, and go down the line dalyons did on this pull request.
I managed to get update in Backbone 0.9.9 core. Check it out as it's exactly what you need http://backbonejs.org/#Collection-update.