JavaScript object that saves/loads its state - javascript

I'd like to create a Javascript object that can save and load its state (to local storage).
This is the basic pattern I'm using:
var obj = function () {
// private members
//
return {
// public members
load: function () {
this.state = JSON.parse(localStorage.getItem('obj'));
if (this.state === null) {
this.state = {
name: 'foo'
};
}
},
save: function () {
localStorage.setItem('obj', JSON.stringify(this.state));
}
};
}();
// load state
obj.load();
console.log(obj.state.name);
// save state
obj.state.name = 'bar';
obj.save();
But there's one thing that annoys me about this pattern: I have to access the object's persistent properties through the 'state' property.
How can I rewrite this so I can use the object in a more natural way, like:
// load state
obj.load();
console.log(obj.name);
// save state
obj.name = 'bar';
obj.save();
This is a very simple 'state', but the solution has to work for a complex state object with nested objects, arrays etc., so simply adding a 'name' property to my object is not what I'm after.

If you don't care which properties get loaded/saved then you can simply copy all from state into self. For example, after reading into var state (instead of this.state since you don't want state to be a part of this anymore): for(x in state) this[x] = state[x];
similarly, you'd save out: var state = {}; for(x in this) state[x] = this[x]
However, if you want to have a pre-defined list, then I'd recommend: var fields = ['name', 'zip', 'age'];
And then use for(x in fields) this[x] = state[x] to load and for(x in fields) state[x] = this[x]; to save.
Sorry it's a bit pieced together, but I hope you can follow what I mean :)
EDIT: Added full example per OPs request.
An example of a full solution using this technique is as follows:
var obj = function () {
// private members
//
return {
// public members
load: function () {
var state = JSON.parse(localStorage.getItem('obj'));
if(state == null) state = { name: 'foo' };
for(x in state) this[x] = state[x];
},
save: function ()
{
var state = {};
// check if it's a function. This version taken from underscorejs
var isFunction = function(obj) {
return !!(obj && obj.constructor && obj.call && obj.apply);
};
for(x in this)
{
if(isFunction(this[x])) continue; // skip functions
state[x] = this[x];
}
localStorage.setItem('obj', JSON.stringify(state));
}
};
};

You can also accomplish a direct save when a property changes,
by using ES5 getters/setters or by using Watch.js
Watch.js example:
var obj = (function () {
// private members
//
var self = {
// Some properties
name: '',
otherName: '',
// Try to load state or use "foo state"
state: JSON.parse(localStorage.getItem('obj')) || {
name: 'foo'
},
save: function () {
localStorage.setItem('obj', JSON.stringify(this.state));
}
};
// Watch the object and save it to local storage, when a property changes
// (Of course, you don't need to call the save method here...)
watch(self, function(property, value) {
console.log('saving state!');
self.state[property] = value;
self.save();
});
return self;
}());
// Update some properties and see that it is saved to local storage.
obj.name = "Some name";
obj.otherName = "Some other name";
console.log(JSON.parse(localStorage.getItem('obj')));
​
Example on JsFiddle.

You could make the state internal and surface getters and setters:
var obj = function () {
// private members
var state = {};
return {
// public members
load: function () {
var loadedState = JSON.parse(localStorage.getItem('obj'));
if (loadedState === null) {
state = {
name: 'foo'
};
} else {
state = loadedState;
}
},
save: function () {
localStorage.setItem('obj', JSON.stringify(state));
},
getState: function (key) {
return state[key];
},
setState: function (key, value) {
state[key] = value;
}
};
};

Using jQuery's extend():
var obj = (function () {
return {
load: function () {
var stored = localStorage.getItem("obj");
var state = stored ? JSON.parse(stored) : {
name: 'foo'
};
$.extend(this, state);
},
save: function () {
localStorage.setItem("obj", JSON.stringify(this));
}
};
})();
// load state
obj.load();
console.log(obj);
// save state
obj.name = 'bar';
obj.save();
jsfiddle
All credit to pimvdb.

Related

JS advantages of module pattern?

I know it's a duplicate but I don't understand the other posts: I'm doing an exercise on freeCodeCamp that I don't understand, it's about modules: What's the advantage of doing this:
const motionModule = (function() {
return {
isCuteMixin: function(obj) {
obj.isCute = function() {
return true;
};
},
singMixin: function(obj) {
obj.sing = function() {
console.log("Singing to an awesome tune");
};
}
};
})();
instead of this:
const motionModule = {
isCuteMixin: function(obj) {
obj.isCute = function() {
return true;
};
},
singMixin: function(obj) {
obj.sing = function() {
console.log("Singing to an awesome tune");
};
}
};
One advantage is you can emulate private variables and methods which are not accessible from outside the returned object. This helps keeping the data and functionality together and avoids corruption of global namespace.
const motionModule = (function() {
let song = 'My song'; // private variable
function singTheSong() {
// private method
}
return {
isCuteMixin: function(obj) {
obj.isCute = function() {
return true;
};
},
singMixin: function(obj) {
obj.sing = function() {
console.log("Singing to an awesome tune" + song);
singTheSong();
};
}
};
})();
// cannot call singTheSong from here
Within a module, you would often want the methods to be able to access each other and shared variables. To do this in your 2nd example, you need to attach them to the object and access them via the this keyword, and also (within the mixin creating functions) use arrow functions to ensure this refers to the right object.
const motionModule = {
song: "La La La",
sing: function() {
console.log(this.song);
},
singMixin: function(obj) {
obj.sing = () => {
console.log(`Singing ${this.song}`);
};
}
};
const a = {};
motionModule.sing();
motionModule.singMixin(a);
a.sing();
Modern ES6 class declarations also require you to work in this way.
class MotionModule {
song = "La La La";
sing() {
console.log(this.song);
}
singMixin(obj) {
obj.sing = () => {
console.log(`Singing ${this.song}`);
};
}
}
const motionModule = new MotionModule();
motionModule.sing();
const a = {};
motionModule.singMixin(a);
a.sing();
As shown in another answer, the first example (an immediately invoked function expression) allows you to access other variables and methods defined within the module without using this, and gives you greater control over which methods and variables are accessible from outside the module.

Can't update value into model generated by Constructor

I am trying to understand the OOP with the following example below. Can you please explain what am I doing wrong and why?
var shoppingcartModel = function() {
var _Cart = function() {
return {
totalPrice: {},
products: []
};
}
return {
cart: _Cart,
addProducts: function(product) {
return _Cart().products.push(product);
}
};
};
var shoppingCart = shoppingcartModel()
console.log(shoppingCart.cart())
shoppingCart.addProducts('product1')
shoppingCart.addProducts('product2')
console.log(shoppingCart.cart())
_Cart is a function that returns an object, not an object itself. Whenever you call Cart_(), including in addProducts, you create a new object, so whatever you push to one of the old objects is disregarded because no reference to the old object remains.
Try something like this instead:
var shoppingcartModel = function() {
const cart = {
totalPrice: {},
products: []
};
return {
cart,
addProducts: function(product) {
return cart.products.push(product);
}
};
};
var shoppingCart = shoppingcartModel()
console.log(shoppingCart.cart)
shoppingCart.addProducts('product1')
shoppingCart.addProducts('product2')
console.log(shoppingCart.cart)

Jasmine testing setting component variable

I'm using spyOn, in a jasmine test, to "listen" to a function call from a service,
that function returns an Observable.
I'm getting the error unexpected token U JSON;
The error is generated from the component line:
this.config = JSON.parse(localStorage.getItem('configuration'));
The localStorage item was JSON.stringified;
I understand that this error is usually thrown when JSON.parse = undefined,
So I tried to set the variable within my test i.e.
component.config = mockConfig;
So..
// Storage Mock
function storageMock() {
var storage = {};
return {
setItem: function(key, value) {
storage[key] = value || '';
},
getItem: function(key) {
return key in storage ? storage[key] : null;
},
removeItem: function(key) {
delete storage[key];
},
get length() {
return Object.keys(storage).length;
},
key: function(i) {
var keys = Object.keys(storage);
return keys[i] || null;
}
};
}
let mockConfig = JSON.stringify({
base_url:"http://image_url/",
poster_sizes:['w9', 'w100']
})
//Set storage
let m = storageMock()
m.setItem('configuration', mockConfig)
it('Should set items array with values from MoviesService', () => {
component.config = JSON.parse(m.getItem('configuration'));
let spy = spyOn(moviesService, 'getPreview').and.callFake(()=>{
return Observable.from([[{id1: 1, title: 'a'}, {id1: 2, title: 'b'}]])
})
component.ngAfterViewInit();
expect(component.items.length).toBeGreaterThan(0);
});
For anyone who come across this problem I got this working by by placing this code in the beforeEach wrapper
Object.defineProperty(window, 'localStorage', { value: m });
Basically it uses my mock localStorage variable instead of the one from the window object.

Using getter and setter on an object property that has his own properties

I'm fairly new to getters and setters and am looking for a way to listen for changes in an object to store the data immediately, without calling a Save() function everytime a value gets changed. This is how I do it right now:
var myObject = {
Data: {
enabled: true,
show: false
},
Save: function () {
//store myObject.Data to local storage
},
Load: function () {
//load data from local storage and assign it to myObject.Data
},
doSomething: function () {
myObject.Load();
if (myObject.Data.enabled) {
myObject.Data.show = true;
myObject.Save();
}
}
Now I would like to optimize this code so everytime a property in myObject.Data is changed, myObject.Save() is executed. The problem I'm experiencing is that it seems only possible to define a getter for a property that has just one value, but not for a property that is an object itself.
var myObj = {
_Data: {
a: 0,
b: 1,
c: 3
},
set Data (a) {
console.log(a);
}
};
myObj.Data.a = 2;
This obviously doesn't work since myObj.Data is not an object and doesn't have the same properties as myObj._Data.
Thanks in advance for any help.
You are likely interested in the Proxy object.
I used a very simple debounce function callHandler in order to avoid calling the onSet method dozens of times during array modifications. Otherwise, [1, 2, 3].splice(0, 1) would call the set handler once per item in the original array.
'use strict';
var myObject = {
Data: {
a: [1, 2, 3],
b: {c: ['test']}
},
Save: function() {
console.log('Save called');
},
}
function recursiveProxy(target, onSet) {
// For performance reasons, onSet will only be called one millesecond
// after the set handler has last been called.
var timeout;
function callHandler() {
clearTimeout(timeout);
timeout = setTimeout(onSet, 1);
}
var recursiveHandler = {
get: function(target, property) {
// If the property is something that could contain another object,
// we want to proxy it's properties as well.
if (typeof target[property] == 'object' && target[property] != null) {
return new Proxy(target[property], recursiveHandler);
}
return target[property];
},
set: function(target, property, value) {
console.log('Set called - queueing onSet');
callHandler();
target[property] = value;
return true;
}
}
return new Proxy(target, recursiveHandler);
}
myObject.Data = recursiveProxy(myObject.Data, myObject.Save);
myObject.Data.a.splice(0, 1);
myObject.Data.b.c[0] = 'test 2';
I believe you are looking for Defining a getter on existing objects using defineProperty
To append a getter to an existing object later at any time, use
Object.defineProperty().
var o = { a:0 }
Object.defineProperty(o, "b", { get: function () { return this.a + 1; } });
console.log(o.b) // Runs the getter, which yields a + 1 (which is 1)
For e.g:
var Data = {
enable: true,
show: false
};
Object.defineProperty(Data, 'doSomething', {
get: function() {
// get something;
},
set: function(something) {
// set something
}
});

Console logging "this" returns "null"

I am trying to create a flux store for a React app I am building. I am using an object-assign polyfill npm package and Facebook's Flux library.
Initially I was getting the error "Cannot read property '_data' of null' error in the console which was refering to var currIds = this._data.map(function(m){return m.id;});. That method is currently the only one being called directly. I then did console.log(this) which returned "null".
I find this strange. What is going on?
My code:
var Assign = require('object-assign');
var EventEmitterProto = require('events').EventEmitter.prototype;
var CHANGE_EVENT = 'CHANGE';
var StoreMethods = {
init: function() {},
set: function (arr) {
console.log(this);
var currIds = this._data.map(function(m){return m.id;});
arr.filter(function (item){
return currIds.indexOf(item.id) === -1;
}).forEach(this.add.bind(this));
},
add: function(item){
console.log(this);
this._data.push(item);
},
all: function() {
return this._data;
},
get: function(id){
return this._data.filter(function(item){
return item.cid === id;
})[0];
},
addChangeListener: function(fn) {
this.on(CHANGE_EVENT, fn);
},
removeChangeListener: function(fn) {
this.removeListener(CHANGE_EVENT, fn);
},
emitChange: function() {
this.emit(CHANGE_EVENT);
},
bind: function(actionType, actionFn) {
if(this.actions[actionType]){
this.actions[actionType].push(actionFn);
} else {
this.actions[actionType] = [actionFn];
}
}
};
exports.extend = function(methods) {
var store = {
_data: [],
actions: {}
};
Assign(store, EventEmitterProto, StoreMethods, methods);
store.init();
require('../dispatcher').register(function(action){
if(store.actions[action.actionType]){
store.actions[action.actionType].forEach(function(fn){
fn.call(null, action.data);
})
}
});
return store;
};
I can't see where set is called, however your this can be null if the function is invoked through call (see here) or apply, and your first argument is null.
This also happens in your require.register callback:
fn.call(null, action.data) //First parameter is your 'this'.

Categories