After a clear, localStorage remembers previous values - javascript

So, this is a bit strange. I'm using Backbone and Backbone.localStorage to save remote data to local storage for caching. Pretty standard stuff.
When I run a localStorage.clear() on the entire store, all values are cleared out permanently except for the key that has a string array of values. It gets cleared out on first inspection, but then when storage saves again with Backbone.LocalStorage.sync('create', model); the previous values are back in there.
Of course, if I manually delete the key within Chrome Developer Tools, then it stays gone and isn't repopulated. It's as if the localStorage.clear() call still caches keys with a string array. I've confirmed it is initially cleared out on app start.
I'll post some code and screenshots here on edit, but really it's pretty standard except for the fact those values remain after the key is repopulated. Any ideas here?
EDIT: Lots of fun code to look at:
Collection:
app.DiagnosisCollection = Backbone.Collection.extend({
model: DiagnosisModel,
localStorage: new Backbone.LocalStorage('diagnosis'),
save: function(model) {
Backbone.LocalStorage.sync('create', model);
}
});
Model:
app.DiagnosisModel = Backbone.Model.extend({
urlRoot: app.ApiRoot,
url: app.DiagnosisApi,
initialize: function(options) {
if(!options.query) {
throw 'query must be passed to diagnosisModel';
}
this.local = false;
this._query = options.query;
},
sync: function(method, model, options) {
var diagnosisKey = localStorage.getItem('diagnosis');
var index, diagnosisStore;
if(diagnosisKey) {
diagnosisKey = diagnosisKey.split(',');
}
index = _.indexOf(diagnosisKey, this._query);
if(index !== -1) {
diagnosisStore = localStorage.getItem('diagnosis' + '-' + diagnosisKey[index]);
}
if(diagnosisStore) {
this.local = true;
options.success(JSON.parse(diagnosisStore));
}
else {
this.local = false;
var fetchUrl = this.urlRoot + this.url + this._query;
$.getJSON(fetchUrl, function(data, textStatus) {
if(textStatus !== 'success') {
options.error();
}
options.success(data);
});
}
}
});
return app.DiagnosisModel;
Controller function that does the work:
var self = this;
// Create a new collection if we don't already have one
// The save function puts it in local storage
if(!this.diagnosisCollection) {
this.diagnosisCollection = new DiagnosisCollection();
this.diagnosisCollection.on('add', function(diagModel) {
this.save(diagModel);
});
}
var diagModel = new DiagnosisModel({
query: diagId
});
diagModel.fetch({
success: function() {
var diagView = new DiagnosisView({
model: diagModel
});
if(!diagModel.local) {
self.diagnosisCollection.add(diagModel);
}
self._switchPage(diagView);
},
error: function() {
console.error("Diag Model Fetch Failed");
Backbone.history.navigate('503', { trigger: true });
}
});
By the way, localStorage.clear() call is in app start. It does an API call to see if the version on the server has changed. If the version has changed, then we nuke localStorage.

Instead of localStorage.clear(), use :
localStorage.removeItem('userInfo');
I have faced the same issue in my backbone application and i used this way to clear out the values.
P.s : Yes you need to specify all of your localstorage variables one by one..:(.. But this works well.

Related

Meteor - how do I make this "reactive" using Deps?

On my client side, I display a list of users and a small chart for each user's points stored in the DB (using jQuery plugin called sparklines).
Drawing the chart is done on Template.rendered method
// client/main.js
Template.listItem.rendered = function() {
var arr = this.data.userPoints // user points is an array of integers
$(this.find(".chart")).sparkline(arr);
}
Now I have a Meteor method on the server side, that is called on a regular basis to update the the user points.
Meteor.methods({
"getUserPoints" : function getUserPoints(id) {
// access some API and fetch the latest user points
}
});
Now I would like the chart to be automatically updated whenever Meteor method is called. I have a method on the template that goes and calls this Meteor method.
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id);
}
});
How do I turn this code into a "reactive" one?
You need to use reactive data source ( Session, ReactiveVar ) together with Tracker.
Using ReactiveVar:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
var instance = Template.instance();
Meteor.call("getUserPoints", this._id, function(error, result) {
instance.userPoints.set(result)
});
}
});
Template.listItem.created = function() {
this.userPoints = new ReactiveVar([]);
};
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = self.userPoints.get();
$(self.find(".chart")).sparkline(arr);
})
}
}
Using Session:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id, function(error, result) {
Session.set("userPoints", result);
});
}
});
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = Session.get("userPoints");
$(self.find(".chart")).sparkline(arr);
})
}
}
Difference between those implementation :
A ReactiveVar is similar to a Session variable, with a few
differences:
ReactiveVars don't have global names, like the "foo" in
Session.get("foo"). Instead, they may be created and used locally, for
example attached to a template instance, as in: this.foo.get().
ReactiveVars are not automatically migrated across hot code pushes,
whereas Session state is.
ReactiveVars can hold any value, while Session variables are limited
to JSON or EJSON.
Source
Deps is deprecated, but still can be used.
The most easily scalable solution is to store the data in a local collection - by passing a null name, the collection will be both local and sessional and so you can put what you want in it and still achieve all the benefits of reactivity. If you upsert the results of getUserPoints into this collection, you can just write a helper to get the appropriate value for each user and it will update automatically.
userData = new Meteor.Collection(null);
// whenever you need to call "getUserPoints" use:
Meteor.call("getUserPoints", this._id, function(err, res) {
userData.upsert({userId: this._id}, {$set: {userId: this._id, points: res}});
});
Template.listItem.helpers({
userPoints: function() {
var pointsDoc = userData.findOne({userId: this._id});
return pointsDoc && pointsDoc.points;
}
});
There is an alternative way using the Tracker package (formerly Deps), which would be quick to implement here, but fiddly to scale. Essentially, you could set up a new Tracker.Dependency to track changes in user points:
var pointsDep = new Tracker.Dependency();
// whenever you call "getUserPoints":
Meteor.call("getUserPoints", this._id, function(err, res) {
...
pointsDep.changed();
});
Then just add a dummy helper to your listItem template (i.e. a helper that doesn't return anything by design):
<template name="listItem">
...
{{pointsCheck}}
</template>
Template.listItem.helpers({
pointsCheck: function() {
pointsDep.depend();
}
});
Whilst that won't return anything, it will force the template to rerender when pointsDep.changed() is called (which will be when new user points data is received).

After successfully deleting an item from my database table, my view does not update. (KnockoutJS w/Mapping and jQuery)

I've tried several different approaches, but I've been unsuccessful in having my view update its contents. My view contains a list of customers and a list of employees.
This represents what I've tried so far omitting employees as its essentially duplicated code.
function dbControl() {
var self=this;
self.customers = function() { $.ajax(...) }; // returns customers object
self.addCustomer = function() { $.ajax(...) };
self.delCustomer = function() { $.ajax(...) };
}
var DB = new dbControl();
var VM = {};
// Populate the VM
VM.selCustomer = ko.observable();
VM.customers = ko.mapping.fromJS(DB.customers);
VM.addCustomer = function() {
DB.addCustomer() // successfully adds to Database
VM.customers.push(); // push VM.selCustomer() into VM.customers
}
VM.delCustomer = function() {
DB.delCustomer() // succcessfully removes from Database
VM.customers.remove(); // removes VM.selCustomer() from VM.customers
}
// Knockout Binding
ko.applyBindings(VM);
The data-bind="foreach: customers" binding works on the webpage just fine and lists all the entries.
All the AJAX calls successfully LIST/ADD/DELETE from the Database properly, but the View does not update after a successful DELETE.
I've tried adding ko.mapping.fromJS(DB.customers, VM) to the end of the delete function in the VM as instructed in the DOCS:Then, every time you receive new data from the server, you can update all the properties on viewModel in one step by calling the ko.mapping.fromJS function again, but no luck.
I've tried adding VM.customers.valueHasMutated() to the VM and it still doesn't update.
I've tried creating an interval to run both previous attempts:
setInterval(function() {
VM.customers = [];
VM.customers = ko.mapping.fromJS(DB.customers);
VM.customers.valueHasMutated();
}, 1000);
Am I structuring this whole project wrong?
VM.addCustomer = function() {
DB.addCustomer() // successfully adds to Database
VM.customers.push(); // push VM.selCustomer() into VM.customers
}
VM.delCustomer = function() {
DB.delCustomer() // succcessfully removes from Database
VM.customers.remove(); // removes VM.selCustomer() from VM.customers
}
You're not passing anything to push or remove; those two methods of observableArray definitely want arguments. Depending on how the methods of DB are written, you may have the same problem with them.
Ok, so after more copy/pasting, I think I've solved my own problem:
The reason the View wasn't updating was because the data coming from the database was never being "re-called".
To fix the problem, within the add/delete functions in the VM, I had to call first the function that pulls the data from the server DB.getCustomers(), then make the ko.mapping.fromJS(DB.customers, {}, VM.customers) call.
function dbControl() {
var self = this;
getCustomers = function() {
$.ajax({
/* URL/TYPE/DATA/CACHE/ASYNC */
success: function() {
self.customers = data; // Now, DB.customers == Updated Data!
}
});
}
}
var DB = new dbControl();
VM = {};
VM.addCustomer = function() {
DB.addCustomer() // successfully adds to Database
//VM.customers.push(); <------ Remove the redundancy
DB.getCustomers(); // get the new list from the server
ko.mapping.fromJS(DB.customers, {}, VM.customers);
}
VM.delCustomer = function() {
DB.delCustomer() // succcessfully removes from Database
//VM.customers.remove(); <------ Remove the redundancy
DB.getCustomers(); // get the new list from the server
ko.mapping.fromJS(DB.customers, {}, VM.customers);
}

Load data into local storage once?

I am using backbone.js I need a very simple way to render a local json file into the users local storage only one time. I am building a cordova app and I just want to work with local storage data.
I have hard coded a decent size .json file (list of players) into my collection, and I just want to load the .json file into the local storage if local storage on that device is empty which will only be once, upon initialization of the app.
I could use ajax, but I don't know how to write it to only inject data one time as "starter" data. So if you know how to do this I can upload the json file to my server and somehow fetch it.
I can inject the data if I go through a series of tasks, I have to disable the fetch method and render this code below in an each statement, plus the json has to be hardcoded into the collection, with a certain format.
playersCollection.create({
name: player.get('name'),
team: player.get('team'),
team_id: player.get('team_id'),
number: player.get('number'),
points: player.get('points')
})
I am trying to finish this lol I need to use it tonight to keep stats, I am almost there the structure works, when data is loaded I can add stats etc, but I need to get that data loaded, I pray someone can help!
Edit: I was able to put together some sloppy code last minuet that at least worked, thanks to #VLS I will have a much better solution, but Ill post the bad code I used.
// I fire renderData method on click
events: {
'click .renderData':'renderData'
},
// Inside my render method I check if "players-backbone" is in local storage
render: function() {
var self = this;
if (localStorage.getItem("players-backbone") === null) {
alert('yup null');
//playersCollection.fetch();
this.$el.append('<button class="renderData">Dont click</button>')
} else {
alert('isnt null');
this.$el.find('.renderData').remove();
playersCollection.fetch();
}
this.teams.each(function(team) {
var teamView = new TeamView({ model: team });
var teamHtml = teamView.render().el;
console.log($(''))
var teamPlayers = this.players.where({team_id: team.get('id')})
_.each(teamPlayers, function(player) {
var playerView = new PlayerView({ model: player });
var playerHtml = playerView.render().el;
$(teamHtml).append(playerHtml);
}, this);
this.$el.append(teamHtml);
}, this);
return this;
},
// method that populates local storage and fires when you click a button with the class .renderData
renderData: function() {
var self = this;
this.teams.each(function(team) {
var teamPlayers = this.players.where({team_id: team.get('id')})
_.each(teamPlayers, function(player) {
playersCollection.create({
name: player.get('name'),
team: player.get('team'),
team_id: player.get('team_id'),
number: player.get('number'),
points: player.get('points')
})
}, this);
}, this);
playersCollection.fetch();
return this;
}
This is obviously not the best way to go about it, but it worked and I was in such a hurry. The caveats are you have to click a button that populates the data, the collection is hard coded in, it's just overall not very elegant (but it works) the app did what it needed.
So big thanks to #VLS, I appreciate the effort to explain your code, and create a fiddle. Sorry I was so late!
You can extend your collection's fetch method and use it in conjunction with Backbone.localStorage, so inside your collection you'd have something like:
localStorage: new Backbone.LocalStorage("TestCollection"),
fetch: function(options) {
// check if localStorage for this collection exists
if(!localStorage.getItem("TestCollection")) {
var self = this;
// fetch from server once
$.ajax({
url: 'collection.json'
}).done(function(response) {
$.each(response.items, function(i, item) {
self.create(item); // saves model to local storage
});
});
} else {
// call original fetch method
return Backbone.Collection.prototype.fetch.call(this, options);
}
}
Demo: http://jsfiddle.net/5nz8p/
More on Backbone.localStorage: https://github.com/jeromegn/Backbone.localStorage

Ember get not getting certain attribute

When running the following from the UserController on Google Chrome, with ember-couchdb-kit-0.9, Ember Data v1.0.0-beta.3-56-g8367aa5, Ember v1.0.0, and this couchdb adapter:
customerSignUp: function () {
var model = this.get('model');
var customer = this.get('store').createRecord('customer', {
description: 'Why hello sir',
user: model
});
customer.save().then(function() {
model.set('customer', customer);
model.save();
});
}
with these models:
App.User = App.Person.extend({
name: DS.attr('string'),
customer: DS.belongsTo('customer', {async: true })
App.Customer = DS.Model.extend({
user: DS.belongsTo('user', {async: true}),
description: DS.attr('string')
});
neither the user nor the customer has their relationship set properly (in the Ember Debugger the user has null and the customer has <computed>, rather than some sort of <EmberPromiseObject> which is what they have when it works).
This only happens when the object in question is persisted. If the save() calls are omitted, both have correctly set relationships, but of course the database hasn't been updated with this information. Whenever the saves happen, the relationships are overwritten with empty entries.
I found that the problem was in the adapter's serializeBelongsTo function, which I've now changed my copy to the following:
serializeBelongsTo: function(record, json, relationship) {
console.log("serializeBelongsTo");
console.log(record.get('user'));
console.log(json);
console.log(relationship);
var attribute, belongsTo, key;
attribute = relationship.options.attribute || "id";
console.log(attribute);
key = relationship.key;
console.log(key);
belongsTo = Ember.get(record, key);
console.log(belongsTo);
if (Ember.isNone(belongsTo)) {
return;
}
json[key] = Ember.get(belongsTo, attribute);
console.log(Ember.get(belongsTo, attribute));
console.log(json);
if (relationship.options.polymorphic) {
return json[key + "_type"] = belongsTo.constructor.typeKey;
}
else {
return json;
}
}
attribute, belongsTo, and key all log as correct, but
console.log(Ember.get(belongsTo, attribute)); returns undefined,
which I've tried to change to
console.log(Ember.get(Ember.get(belongsTo, 'content'), attribute));
since console.log(belongsTo); told me the id attribute was hidden inside a content object. Attached is a screenshot showing what I mean.
The change doesn't fix the problem though, and I keep getting undefined. No matter what method I use to try to get the id out of the belongsTo object, I always get either null or undefined. Here are some examples of things I've tried to get content out of the object:
var content = belongsTo.content;
var content = Ember.get(belongsTo, 'content');
var content = belongsTo.get('content');
console.log(json); returns Object {description: "Why hello sir", user: undefined}
Here's a pastebin showing relevant output: http://pastebin.com/v4mb3PJ2
Update
A very confusing update!
When I save the model from a different function:
saveModel: function() {
this.get('model').save().then(
function( data, textStatus, jqXHR ) {
console.log('Saved successfully.');
},
function( jqXHR, textStatus, errorThrown ) {
console.log(jqXHR);
console.log(errorThrown);
console.log(textStatus);
}
);
}
The model is correctly saved. Everything in serializeBelongsto works exactly as expected.
Here's a different pastebin showing output for this case: http://pastebin.com/Vawur8Q0
I figured out the problem. Basically the belongsTo object in serializeBelongsTo wasn't really resolved by the time it was being referenced, which I found out by querying isFulfilled. So I implemented by saving side this way:
function saveOn (target, attribute) {
target.addObserver(attribute, function () {
if (target.get(attribute)) {
console.log("Inside with %#".fmt(attribute));
target.removeObserver(attribute);
Ember.run.once(target, function() {
target.save();
});
}
});
};
customerSignUp: function () {
var model = this.get('model');
var customer = this.get('store').createRecord('customer', {
description: 'Why hello sir'
});
customer.save().then(function () {
model.set('customer', customer);
customer.set('user', model);
saveOn(customer, 'user.isFulfilled');
saveOn(model, 'customer.isFulfilled');
});
}
Now everything works like a charm. It might be a good idea for serializeBelongsTo to take this into account though. This line: console.log(Ember.get(belongsTo, 'isFulfilled')); was coming up false in my case. There was just a race condition of some sort between the creation of the record and it's serialization!
I'd like to make my saveOn function return a promise though, which I could then use to chain multiple saveOns together. That way I wouldn't have to do a customer.save() to make sure the id's were populated.

delegateEvents not working as expect when using back button

Below is my backbone view.
define([
'app',
'backbone',
'twig',
'templates/report',
'data/reportViewCollection',
'data/reportViewModel'
], function (app, Backbone, Twig, template, Collection, Model) {
var collection = new Collection();
var fetching;
return Backbone.View.extend({
setParams: function (rlId, viewId, categoryId, depth) {
// reset any possible pending previous repests.
fetching = false;
var model = collection.getModel(rlId, viewId, categoryId, depth);
if (!model) {
model = new Model({
rlId: rlId,
viewId: viewId,
categoryId: categoryId,
depth: depth
});
fetching = model.fetch({
success: function (model) {
collection.add(model);
},
error: function (model) {
alert('error getting report view');
}
});
}
this.model = model;
},
render: function () {
var that = this;
var done = function() {
app.vent.trigger('domchange:title', that.model.get('title'));
that.$el.html(Twig.render(template, that.model.toJSON()));
that.delegateEvents(that.events);
fetching = false;
};
if (fetching) {
app.loading(this);
fetching.done(done);
} else {
done();
}
return this;
},
events: {
'change select.view-select': 'viewSelect',
'click #dothing': function (e) {e.preventDefault(); alert('hi');}
},
viewSelect: function(e) {
var selectedView = $(e.target).val();
var rlId = this.model.get('rlId');
if (!rlId) rlId = 0;
var url = 'report/' + rlId + '/' + selectedView;
console.log(this, e, url);
Backbone.history.navigate(url, {trigger: true});
}
});
});
Description of functionality:
What happens is when a specific url is navigated to, the setParams() function is called to fetch the model from the server. When the render method is called, it checks if we are currently fetching the model and if so, uses sets a deferred callback to render the template when it gets done fetching. When the model is fetch-ed and we are ready to render, renders the template and fills in the view by that.$el.html().
Problem:
What happens is that my events work perfectly the first time I navigate to a url, but when I hit the back button, my events don't get attached.
I've stepped through the code and can't see any differences. The only real difference is that I'm loading the model from the cached collection immediately instead of doing an ajax request to fetch it.
Any clues what is going on?
try to change:
that.$el.html(Twig.render(template, that.model.toJSON()));
to
that.$el.html("");
that.$el.append(Twig.render(template, that.model.toJSON()));
had kind the same problem and this fixed it.
I resolved my issue. The comment by #mu set me in the right direction.
I am using Marionette and my view is contained in a region. In the Region's open function, it is doing this.$el.html(view.el); which wipes out the events in certain circumstances. I'm still not sure why it does in some but on in others.
The Solution proved to be to add an onShow function to my view that call's this.delegateEvents(). Everything is working smoothly now!
I eventually figured it out by stepping through the code and watching the events registered on the view's div.

Categories