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
Related
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).
I have a back-end server with a REST api. On the front-end, I use Angular.js. To handle the real-time part, I would like to use a third-party service such as Pusher.
I'm looking for a simple library that could handle the M of the mvc part of the front-end. More specifically, I would like a Model interface that would abstract away the complexity of the offline and real-time aspects.
For instance, from the Angular side, I would like to subscribe to my Model and get notified when it changes. I would also like to have a .save() method that would handle the syncing with the server and other clients.
That library should:
Work offline: it will save the data in the local_storage and sync back with the server when it gets back online.
Listen to real-time changes, update its model and propagate the changes to the listeners.
Work well with a standard REST interface.
So, just as a quick pseudocode example, in Angular I would like to do:
my_controller = function($scope) {
User.find_all(function(users) {
$scope.users = users;
});
}
User is the model abstraction.. when it gets a real-time update, my $scope.users should change accordingly.
$scope.users[0].set('name', 'testing)
This should save the model to the server. Or, if offline, should save it locally and sync it later on when it's back online.
I know there are online services trying to accomplish that, such as Firebase and kinvey. The problem with these tools is that it only offers a hosted solution. I need to controller the REST server and the database. So, basically, I'm looking for a "Firebase" library - without all the authentications and authorizations - that could work for with a REST server and pubsub third party.
Thanks!
this is a bit long for an answer, but i don't have it published yet.
function monitor(obj, callBack){
var api={
patch: patchObjectWithDiff,
init: init,
resolve: resolve,
snapshot: snapshot,
diff: diff,
update: changeMonitor
};
function merge2(o, ob) {
for (var z in ob) {
if (ob.hasOwnProperty(z)) {
if(typeof ob[z]=="object"){
if(ob[z]==null){
delete o[z];
}else{
merge2( o[z] || {}, ob[z]);
}
}else{
o[z] = ob[z];
}
}
}
return o;
}
function snapshot(obj) {
var out = [];
function merge3(ob, path) {
path = path || [];
var tp;
for(var z in ob) {
if(ob.hasOwnProperty(z)) {
if(ob[z] && typeof ob[z] == "object" && [Date, RegExp].indexOf(ob[z].constructor) == -1) {
tp=path.concat(z);
out.push({
path: tp.join("`"),
path2: tp,
dt: "set",
date: +new Date,
v: Array.isArray(ob[z]) ? "[]" : "{}"
});
merge3(ob[z], path.concat(z));
} else {
tp=path.concat(z);
out.push({
path: tp.join("`"),
path2: tp,
type: "set",
dt: +new Date,
v: JSON.stringify(ob[z])
});
}
}
}
}
merge3(obj);
return out;
};
function diff(d1, d2){
var out=d2.filter(function(a,b,c){
var ov=JSON.stringify(a.v);
return d1.some(function(aa,bb){ return aa.path==a.path && JSON.stringify(aa.v) != ov; });
}),
// find deletions
dels=d1.filter(function(a,b,c){
return !d2.some(function(aa,bb){ if(aa.path==a.path ){ return true; }; });
}),
allPaths=dels.map(function(a){return a.path}).sort(),
dels2=dels.filter(function eliminateUnneededSubBranches(a){
var pos=allPaths.indexOf( a.path2.slice(0,-1).join("`") );
return pos==-1 || pos >= allPaths.indexOf(a.path);
}).map(function(a){a.type="del"; delete a.v; return a;});
[].push.apply(out, dels2);
//find inserts
var outNew=d2.filter(function(a,b,c){
var ov=JSON.stringify(a.v);
return !d1.some(function(aa,bb){ return aa.path==a.path });
});
[].push.apply(out, outNew);
return out.map(function(a){
var x= {
dt: a.dt,
k: a.path2
};
if(a.hasOwnProperty("v")){ x.v=a.v; }
return x;
a.k=a.path2;
delete a.path;
delete a.path2;
delete a.type;
return a;
});
}
function resolve(path, object){
var tob=object;
path.map(function(a){ return (tob=tob[a])||tob; })
return tob;
}
function patchObjectWithDiff(diff, object){
diff.forEach(function(a,b,c){
var p= resolve(a.k.slice(0,-1), object),
k= a.k.slice(-1)[0];
if(a.hasOwnProperty("v")){ //set:
p[k]=JSON.parse(a.v);
if(String(p[k]).match(/Z$/)){ p[k]=new Date(''+p[k]) || p[k]; }
}else{ // del:
if(Array.isArray(p)){ p.splice(k,1); }else{ delete p[k]; }
}
});
return object;
}
var init=snapshot(JSON.parse(JSON.stringify(obj))),
id=Math.random()+ Number(new Date());
var init=snapshot(obj);
function changeMonitor(){
var thisTime=snapshot(obj),
diffs=diff(init, thisTime);
if(diffs.length){
api.diffs=diffs;
(callBack||console.log.bind(console))("objectUpdate", diffs );
init=thisTime;
}//end if change?
}
setInterval(changeMonitor, 2500);
return api;
}
demo / example usage:
var obj={a:1, b:[1,2,3], c: false}; // a model object
var dupe=JSON.parse(JSON.stringify(obj)); // a cheap clone of the data for demo use
//subscribe this object to updates
var mon=monitor(obj, function(type, changes){console.log(type, changes); });
// make some changes to the object:
obj.e="cool!";
obj.b.push(5);
obj.a=7;
// manually call update instead of waiting for the bundler:
// (this is needed for this demo so we can reconcile the changes in sync and view the output)
mon.update();
// now apply stored changes to the clone of the orig data:
var updatedDupe= mon.patch(mon.diffs, dupe);
// use a cheap and easy but not production-reliable to compare the objects:
JSON.stringify(updatedDupe)==JSON.stringify(obj); // should be true
tested in chrome and firefox.
be aware that this particular demo's use of JSON depends on some luck, and consistent key ordering, which is not guaranteed by the JS spec. Key order doesn't really matter, but it might cause the JSON.stringify() == comparison to fail, even though the object's properties are indeed sync'd. This is just for demonstration's sake to get a true/false answer if it works, don't beat me up...
you can give it a custom callback to send("diff", {diffs:mon.diffs}) the changes as they happen and then use a subscribed event from pusher et al like on("diff", function(e){mon.patch(e.diffs, obj);}); to apply your changes and trigger the view update in your MVC.
I'll leave it to you to work localStorage and online/offline in there as you need, it should be really easy after getting this far.
All diffs in the change list come with three keys:
{"dt":1392348959730,"k":["b","3"],"v":"5"}
dt: a timestamp of when the change was discovered
k: the key path where the change was detected
v: what the discovered changed value is as of dt
This script is hot off the press and i haven't had time to write proper documentation, but i figure it might help or at least inspire a solution that works for you.
I think you should start researching HTML5 WebSockets: http://www.websocket.org/
It allows bi-directional communication between server and client, client pull and server push.
Then look at SignalR, the asp.net implementation of HTML5 WebSockets: http://www.asp.net/signalr
I've created a game based on HTML5+JS using melonJS for Windows8.
But, how do I save player's persisting game data into the disk file (preferably localFolder).
I've read the sample given in the MSDN forum, but there's no mention about saving it to JSON format file... and plus I'm a little new to Win8 app programming.
Anyways, this is what I tried (this function is called when player choose to save):
function meSave() {
//create a var to store the the persisting data during the play
var dataSaved = {
data: {
progress: game.data.progress,
HP: game.data.HP,
MP: game.data.MP,
Level: game.data.Level,
maxHP: game.data.maxHP,
maxMP: game.data.maxMP,
Money: game.data.Money,
currPos: {
x: me.game.getEntityByName('mainPlayer')[0].pos.x,
y: me.game.getEntityByName('mainPlayer')[0].pos.y,
},
currStage: me.levelDirector.getCurrentLevelId(),
}
};
var applicationData = Windows.Storage.ApplicationData.current;
var localFolder = applicationData.localFolder;
var filename = "dataFile.json";
function writeTimestamp() {
localFolder.createFileAsync("dataFile.json",
Windows.Storage.CreationCollisionOption.replaceExisting)
.then(function (file) {
return Windows.Storage.FileIO.writeTextAsync(file,
JSON.stringify(dataSaved));
}).done(function () {
game.savingDone = true;
});
}
}
The most obvious issue seems to be that you're not calling the function writeTimestamp in your code. There are also a handful of other things that you might consider doing:
meSave()
.done(function () {
console.log('saved!');
});
function meSave() {
//create a var to store the the persisting data during the play
var dataSaved = {
/* put or fetch your game data here */
};
var applicationData = Windows.Storage.ApplicationData.current;
var localFolder = applicationData.localFolder;
var filename = "dataFile.json";
return localFolder.createFileAsync(filename,
Windows.Storage.CreationCollisionOption.replaceExisting)
.then(function (file) {
return Windows.Storage.FileIO.writeTextAsync(file,
JSON.stringify(dataSaved));
}).then(function() {
// dataSaved.done ... although this would be lost if dataSaved is local
});
}
I changed a few things:
There wasn't a need for an inner named function inside of the meSave function. I've replaced the function with another Promise. In this way, your calling code can rely on the callbacks of the Promise to be aware that the file has been saved to storage successfully.
You had extra variables (unused), and the done for the file creation was setting a variable that didn't exist locally (maybe it's somewhere else in your code).
My application has three things that are "global": a user location with lat and lng values, if the user is logged in or not, and the activity that they have chosen (think of it like a category).
What I'd like to happen is for these values to be used across the application. Each screen will use some set of this date, so loading it per page isn't the right answer. The information also will not change unless the user creates an event: different location, activity, or signs in/out.
How should I set this up?
My thinking is that there should be an initial load upon startup, in my main.js file I load each and then app.start like so:
siteService.js
define([...], function (...) {
return {
init: function () {
return $.when(
activityService.getStoredActivity(),
locationService.getStoredLocation(),
userService.getUsername()
);
}
}
});
main.js
define([..., 'modules/siteService'], function (..., site) {
...
site.init().then(function () {
app.start().then(function () {
...
app.setRoot('viewmodels/shell', 'entrance');
});
});
});
This does make sure that the data is loaded. From there in each of my services I store the information into local storage so I don't have to keep firing off AJAX calls. If local storage isn't there it will.
The problem comes later when I need this data in other pages. I'm ending up calling my site.init().then() within each page's activate method. Which is pretty sloppy.
shell.js
self.activate = function () {
...
siteService.init().then(function (activity, location, username) {
self.activity(activity);
self.location(location);
setUsername(username);
});
return router.activate();
};
and then again in:
welcome.js
this.activate = function () {
site.init().then(function(activity, location) {
loc = location;
act = activity;
load();
});
});
I'd like to set the values initially, each page initially loading from those values, and then use the pub/sub of Durandal to react to any changes after the fact.
I was thinking of solely using the pub/sub, but then felt that I would run into chicken and the egg issues when the data is loaded in relation to the page loads (IE If my AJAX loaded first, and my page wasn't loaded, the event fires, but the page never gets the update).
Is it possible to setup something like a static class in javascript to pull the data initially and then share it throughout the viewmodels? Or, is there a way to guarantee that the value will be there when I initially load a page, like a app.on('value', { initialize: true })?
When a require.js module returns an object, that object is a singleton, so you can have something like
settings.js
define(function() {
return {
location: {
lat: 0,
long: 0
},
loggedin: false,
activity: ""
}
});
Then any time you name settings as a dependency, you'll always get a reference to the same object and you can get/set its fields as you wish.
Changed siteService.js to:
define([...], function (...) {
var module = function () { };
module.prototype.activity = {};
module.prototype.location = {};
module.prototype.username = {};
module.prototype.init = function () {
return $.when(
activityService.getStoredActivity(),
locationService.getStoredLocation(),
userService.getUsername()
).then(function (activity, location, username) {
module.prototype.activity = activity;
module.prototype.location = location;
module.prototype.username = username;
});
};
return module;
});
main.js to
define([..., 'modules/siteService'], function(..., Site) {
site = new Site();
site.init().then(function () {
app.start().then(function () {
...
app.setRoot('viewmodels/shell', 'entrance');
});
});
and shell.js and welcome.js now just use:
var site = new Site();
self.activity(site.activity);
self.location(site.location);
setUsername(site.username);
and
var site = new Site();
loc = site.location;
act = site.activity;
I'm trying to add a list of models to a collection to be stored locally. I don't fully understand backbone yet which is really the cause of this problem.
I basically pull in an RSS feed, assign each item in the feed to a Model and try place the list of Models into a collection so I can iterate over them later.
I am getting an error saying that I need to specify a Url for the collection.
It would be brilliant if someone could explain to me the correct process I need to follow to achieve my goal.
Currently I have:
var DetailIndividual = Backbone.Model.extend();
var DetailsIndividual = Backbone.Collection.extend({
model: DetailIndividual
});
var Search = Backbone.View.extend({
events: {
'click a.individualCast' : 'pullIndividual'
},
initialize: function() {
this.detailsIndividual = new DetailsIndividual();
_this = this;
this.detailsIndividual.bind('reset', function(collection) {
collection.each(function(item) {
//code to handle update
});
});
},
pullIndividual: function(e){
e.preventDefault();
//Logic to pull in RSS feed
for (var i = 0; i < result.feed.entries.length; i++) {
entry[i] = new DetailIndividual({ title: result.feed.entries[i].title, link: result.feed.entries[i].link, });
}
this.detailsIndividual.add(entry);
}
});
The error is reported out from here,because model must have url attribute:
http://backbonejs.org/docs/backbone.html#section-167
do you model have url attribute?
The reason why you're getting the error is because you're binding the 'reset' event. 'reset' is only fired on a collection.fetch or an explicit call to collection.reset, and in your case you're never fetching from the server with your collection - I'm assuming from your code you already have the feed in memory - so unless you're explicitly resetting, there's no need to listen for the reset.
In your code, you're not really extending Collection and Model, so it's actually not necessary to make extended objects - just use Backbone.Collection. You don't even need to create a Model extension because by default, when you add a JSON, a Backbone.Model is automatically created. It's only necessary to assign the collection.model if you're creating a truly custom model (with method overrides and additions).
Here's a way you could load your collection:
var search = Backbone.View.extend({
events: {
'click a.individualCast' : 'pullIndividual'
},
initialize: function() {
this.detailsIndividual = new Backbone.Collection();
},
pullIndividual: function(e) {
e.preventDefault();
//Logic to pull in RSS feed
for (var i = 0; i < result.feed.entries.length; i++) {
this.detailsIndividual.add({
title: result.feed.entries[i].title,
link: result.feed.entries[i].link
});
}
}
});
You didn't provide any code of how you wanted to parse collection (except in the 'reset'), but essentially you'd load the collection from the feed as shown.