This question is a follow-up to my last question: JavaScript Serialization and Methods. While related, I believe this is different which is why I started this thread. Regardless...
I have a complex tree of objects that I need to pass around between pages. Because of this, I'm attempting to serialize and deserialize my objects. One of my objects in particular has several collections of child objects. Each of those child object types has a function on it, that I'm trying to call. Unfortunately, I am not having any luck. I setup a test project in an attempt to isolate the problem and get to the bottom of it. Currently, I have my JavaScript Objects defined in a seperate file called objects.js. That file looks like this:
objects.js
function MyChild() { this.init(); }
MyChild.prototype = {
data: {
id: 0,
fullName: "",
},
init: function () {
},
save: function (key) {
},
load: function (key) {
},
test: function () {
alert("Testing Child functions");
}
}
function MyParent() { this.init(); }
MyParent.prototype = {
data: {
id: "",
children: null
},
init: function () {
this.data.children = [];
},
save: function (key) {
window.localStorage.setItem(key, JSON.stringify(this.data));
},
load: function (key) {
var temp = window.localStorage.getItem(key);
if (temp != null) {
this.data = JSON.parse(temp);
$.each(this.data.children, function (i, r) {
});
}
},
test: function () {
alert("Testing Parent functions");
}
}
I am creating, serializing, deserializing, and attempting to interact with these objects in an .html file. That .html file is shown here:
test.html
<div>
<input id="button1" type="button" value="Create Object Tree" onclick="button1Click();" /><br />
<input id="button2" type="button" value="Retrieve and Execute" onclick="button2Click();" /><br />
</div>
<script type="text/javascript">
function button1Click() {
var child = new MyChild();
child.data.id = 1;
var p = new MyParent();
p.data.id = "A";
p.data.children.push(child);
p.save("A");
}
function button2Click() {
var storedParent = new MyParent();
storedParent.load("A");
storedParent.test(); // This works
alert(storedParent.data.children.length); // This displays "1" like I would expect
alert(storedParent.data.children[0].data.id); // This does NOT work.
storedParent.data.children[0].test(); // This does NOT work.
}
</script>
I'm not sure what I'm doing wrong. Can someone please help me understand this? Can somone please help me fix my example. I have a hunch that I'm not serializing MyChild objects properly. But I don't understand how I should be serializing / deserializing them in relation to MyParent.
Thank you!
You need to store your data within each object, not within its prototype.
Data stored in in the prototype of an object is shared between all instances and won't be serialised by JSON.stringify, so your object data never ends up in the local storage.
To fix, add data to this within the this.init() function:
MyChild.prototype = {
init: function() {
this.data = {
id: 0,
fullName: ""
};
},
...
}
MyParent.prototype = {
init: function() {
this.data = {
id: "",
children: []
}
},
...
}
Working sample at http://jsfiddle.net/alnitak/fdwVB/
Note that attaching the functions in MyChild to the retrieved data is tricky. The code below appears to work:
load: function(key) {
var temp = window.localStorage.getItem(key);
if (temp != null) {
this.data = JSON.parse(temp);
var children = this.data.children;
for (var i = 0; i < children.length; ++i) {
children[i] = $.extend(new MyChild(), children[i]);
}
}
},
But note that it works by constructing a new "default" child for each retrieved entry, and then overwriting that child's data with the serialised values.
This could be problematic if the constructor has "side effects".
A better approach might be to allow the MyChild() constructor to take an optional complete object of initial values to use instead of the default values.
Related
In my Ionic app I've added the plugin 'ngStorage' and it comes with a little demo code:
var add = function (thing) {
$localStorage.things.push(thing);
}
This works exactly as told. I add("foo") it, and do getAll() and the value is there. I remove the add(), but keep the getAll(), I still have the value "foo" (as expected).
This isn't very usefull for me, I want to access it with keys, so I've made the following:
var addByKey = function (key, value) {
$localStorage.things[key] = value;
// Or, I've also tried:
$localStorage.things.key = value;
}
When I do the addByKey("foo","bar") and then the getAll() I get the values exactly as I want. When I remove the addByKey() and reload, I expect it to still remember the set information, but it doesn't exist. However, the first attempt via the add() function still exists, "foo" is still there (meaning the array doesnt reset).
How do I make a key->value type of structure?
In case it's usefull:
.factory ('StorageService', function ($localStorage) {
$localStorage = $localStorage.$default({
things: []
});
var _getAll = function () {
return $localStorage.things;
};
var _add = function (thing) {
$localStorage.things.push(thing);
}
var _addByKey = function (thing, value) {
$localStorage.things[key] = value;
// Or, I've also tried:
$localStorage.things.key = value;
}
return {
getAll: _getAll,
add: _add,
addByKey: _addByKey
};
})
Assuming that you want a key value storage system you can simply use an object instead of an array so that every key can be set as a property of this object.
.factory('StorageService', function($localStorage) {
$localStorage = $localStorage.$default({
things: {}
});
var _getAll = function() {
return $localStorage.things;
};
var _addByKey = function(thing, value) {
$localStorage.things[thing] = value;
}
return {
getAll: _getAll,
addByKey: _addByKey
};
})
However, assuming that you want to keep a reference of all values on the main collection and access them through keys, you can consider using an object to store the things intead of an array. So that you can use a property to store all items (you can store in a different place as well) and use this object to store your keys by referencing the to a desired value on your collection.
You may need to implement the deletion logic to maintain the consistence between the collection and the dictionary.
Your factory would look like this:
.factory('StorageService', function($localStorage) {
$localStorage = $localStorage.$default({
things: {
items: []
}
});
var _getAll = function() {
return $localStorage.things.items;
};
var _add = function(thing) {
$localStorage.things.items.push(thing);
}
var _addByKey = function(thing, value) {
var i = $localStorage.things.items.push(value) - 1;
$localStorage.things[thing] = $localStorage.things.items[i];
}
return {
getAll: _getAll,
add: _add,
addByKey: _addByKey
};
})
I'm creating a grouped list of my pupils as per this example: http://msdn.microsoft.com/en-us/library/windows/apps/hh465464.aspx
The actual code for creating the grouping as well as the HTML are almost identical (safe for name changes).
I'm then push()ing some items into the original List() which then also updates the GroupedList(). That part works fine.
However, what I'm seeing is this:
This list should be grouped by firstnames (on display is "Lastname, Firstname"). What I'm seeing here is that item #1 should be in "S", #3 should be in "A" and #6 should be in "I".
The only thing that I'm doing different from the example is the DataSource, insofar as I'm push()ing an actual WinJS Class in there (with getter and setter functions for the attributes displayed in the List).
However, the getGroupKey(dataItem) and other grouping functions are working as they should, i.e. return the proper values.
Any ideas? Because otherwise I'd have to look at using two arrays (one being the List() and another the array where the class instances live) for which I'd then have to program sync routines to keep the data consistent and that's something I actually wanted to escape from...
Code follows below, relevant snippets only.
Defining the Lists and grouping functions:
function compareGroups(leftKey, rightKey) {
return leftKey.charCodeAt(0) - rightKey.charCodeAt(0);
}
function getGroupKey(dataItem) {
return dataItem.lastname.toUpperCase().charAt(0);
}
function getGroupData(dataItem) {
return {
title: dataItem.lastname.toUpperCase().charAt(0)
};
}
var pupilsList = new WinJS.Binding.List({ binding: true });
var groupedPupilsList = pupilsList.createGrouped(getGroupKey, getGroupData, compareGroups);
Where the Data comes from:
var Schueler = WinJS.Class.define(function (original, id, firstname, lastname, tutor_id, picture, email, phone, notes, birthday, classes) {
var that = this;
this._classnames = new Array();
if (original) {
[... irrelevant part snipped ...]
});
} else {
var row = id;
this._id = row.rowid;
this._firstname = row.firstname_enc;
this._lastname = row.lastname_enc;
this._tutor_id = row.tutor_id;
this._picture = row.picture_enc;
this._email = row.email_enc;
this._phone = row.phone_enc;
this._notes = row.notes_enc;
this._birthday = row.birthday_enc;
this._guid = row.guid;
this.updateClassnames();
}
},
{
id: {
get: function () {
return this._id;
},
set: function (id) {
this._id = id;
}
},
firstname: {
get: function () {
return this._firstname;
},
set: function () {
//TODO
}
},
lastname: {
get: function () {
return this._lastname;
},
set: function () {
//TODO
}
},
[... irrelevant parts snipped ...]
classnames: {
get: function () {
return this._classnames.join(", ");
},
set: function (names) {
this._classnames = names;
}
},
updateClassnames: function () {
var that = this;
SQLite3JS.openAsync(DataLayer.db_path)
.then(function (db) {
var sql = "SELECT Classes.name_enc FROM Classes JOIN Classes_Pupils ON Classes.rowid = Classes_Pupils.class_id JOIN Pupils ON Classes_Pupils.pupil_id = Pupils.rowid WHERE Pupils.rowid = {0};".format(that._id);
return db.allAsync(sql)
.then(function (results) {
db.close();
var names = new Array();
for (var i = 0; i < results.length; i++) {
names.push(results[i].name_enc.toString().decrypt());
}
that.classnames = names;
DataLayer.PupilsList.dispatchEvent("reload");
}, function (error) {
if (error.message.indexOf("database is locked") > -1) {
console.log("DB locked, will try again in 50 ms");
window.setTimeout(that.updateClassnames(), 50);
}
});
});
}
},
{
reconstructAll: function () {
DataLayer.retrieveSeveralRows("Pupils", function (results) {
for (var i = 0; i < results.length; i++) {
DataLayer.PupilsList.push(new Schueler(false, results[i]));
}
});
}
});
WinJS.Namespace.define("DataLayer", {
Schueler: Schueler
});
Workflow is as follows: First empty lists are created, then another routine checks for DB availability. As soon as that routine gives a green light, Schueler.reconstructAll() is called.
DataLayer.retrieveSeveralRows(table, callback) is a wrapper function for a call to the SQLite database, essentially doing a SELECT * FROM Pupils and returning the results to the callback function.
This callback then creates a new instance of the Schueler() class and pushes that to the list.
Addendum: If I use createSorted() everything is just dandy. Will use that for now.
Edit: As suggested by Kraig Brockschmidt, it seems to have indeed been a localization issues, so adding one line and modifying one function as follows fixes everything right up:
var charGroups = Windows.Globalization.Collation.CharacterGroupings();
function getGroupKey(dataItem) {
return charGroups.lookup(dataItem.lastname.toUpperCase().charAt(0));
}
I see that you're working with createSorted now, but there are a couple of other things you can do to diagnose the original issue.
First, try using some static data instead of populating your list dynamically.
Second, put some console.log output inside your getGroupKey and getGroupData functions so you can evaluate what you're returning, exactly.
The other thing I should mention is that the MSDN docs page shows code that isn't sensitive to all local languages. That is, using the first character of a string for sort order isn't always the right thing. There is an API in Windows.Globalization.Collation (http://msdn.microsoft.com/en-us/library/windows/apps/windows.globalization.collation.aspx) that is built to handle sort ordering properly. If you look at the [HTML ListView Grouping and Semantic Zoom sample][1], in the file groupeddata.js, you'll see how this is used. Offhand this shouldn't affect your data, but I wanted to mention it.
I have a JSON object as follows. After sending ajax call I want to clear this. How I can do?
var cfamFwdDtls = {
cndtwizid: [],
clientIdsInstnIds: [],
positioncd: '',
positionCnt: '',
rcrtrInstnId: '',
positionLocation: {
cntryIdFrCndt: '',
stateIdFrCndt: '',
zipIdFrCndt: '',
cityIdFrCndt: ''
},
searchPstnSkill: []
};
If you want to reset the entire object, just reset the variable back to {};
cfamFwdDtls = {};
// or
cfamFwdDtls = new Object;
// will prevent garbage collection
delete cfamFwdDtls;
However, if you want a more fine-grained way of "resetting" the object, you are going to need to define what specifically your requirements for a reset are. Regardless, you can always iterate through the object and make the necessary objects.
for (var key in cfamFwdDtls) {
if (typeof cfamFwdDtls[key] == "string") {
cfamFwdDtls[key] = '';
} else if (Array.isArray(cfamFwdDtls[key])) {
cfamFwdDtls[key] = [];
} else {
delete cfamFwdDtls[key];
}
}
The above definition could be a possible way to define your particular situation since I only see strings and arrays in your object. If the key is neither of those, it would just delete the key. This could be tailored as you find necessary.
for (var entry in cfamFwdDtls) delete cfamFwdDtls[entry];
If you simply reassign to {}, you'll get in trouble if there are multiple references to your object. Also may face garbage collection issues.
there is alternative for this if you want to remove object.Something like this
delete cfamFwdDtls;
you can use delete keyword for deleting a object.
more details read
example
The Problems
When I ran into this I wanted to solve two issues.
Have one location for a complex structure definition.
Reassurance the entire definition is reset.
Solutions
The basic idea is have a function return the empty structure. If it is not in a function you could change the structure itself not the instance of the structure.
Examples
Class
I personally use this but I also include the API functions in the class and make it an HTTP service.
class complexStructure {
constructor () {
this.payload = resetPayload();
}
resetPayload () {
this.payload = {
cndtwizid: [],
clientIdsInstnIds: [],
//...
};
}
}
Function
function resetStructure () {
return {
cndtwizid: [],
clientIdsInstnIds: [],
//...
};
}
let resetStructure = resetStructure()
function getJson(){
return {
cndtwizid: [],
clientIdsInstnIds: [],
positioncd: '',
positionCnt: '',
rcrtrInstnId: '',
positionLocation: {
cntryIdFrCndt: '',
stateIdFrCndt: '',
zipIdFrCndt: '',
cityIdFrCndt: ''
},
searchPstnSkill: []
};
}
var data = getJson();
data.positioncd = 'xyz';
data.rcrtrInstnId = 'abc';
$.ajax(...){
}
success(response){
data = getJson(); //re-initialize structure
}
I have the following:
// Child Array is Cards, trying to add computed observable for each child
var CardViewModel = function (data) {
ko.mapping.fromJS(data, {}, this);
this.editing = ko.observable(false);
};
var mapping = {
'cards': { // This never gets hit, UNLESS I remove the 'create' method below
create: function (options) {
debugger;
return new CardViewModel(options.data);
}
},
create: function(options) {
var innerModel = ko.mapping.fromJS(options.data);
innerModel.cardCount = ko.computed(function () {
return innerModel.cards().length;
});
return innerModel;
}
};
var SetViewModel = ko.mapping.fromJS(setData, mapping);
debugger;
ko.applyBindings(SetViewModel);
However I can't get the 'cards' binding to work - that code isn't reached unless I remove the 'create' method. I'm trying to follow the example from the knockout site:
http://knockoutjs.com/documentation/plugins-mapping.html
They do this for the child object definition:
var mapping = {
'children': {
create: function(options) {
return new myChildModel(options.data);
}
}
}
var viewModel = ko.mapping.fromJS(data, mapping);
With the ChildModel defined like this:
var myChildModel = function(data) {
ko.mapping.fromJS(data, {}, this);
this.nameLength = ko.computed(function() {
return this.name().length;
}, this);
}
I've spent the past day on this and cannot for the life of me figure out why this isn't working. Any tips would be awesome.
EDIT: Here's a fiddle of what I'm working with. It's only showing SIDE 1 in the result because "editing" isn't recognized here:
<div data-bind="visible: !$parent.editing()" class="span5 side-study-box">
http://jsfiddle.net/PTSkR/1/
This is the error I get in chrome when I run it:
Uncaught Error: Unable to parse bindings. Message: TypeError: Object
has no method 'editing'; Bindings value: visible: !$parent.editing()
You have overridden the create behavior for your view model. The mapping plugin will not call any of the other handlers for the properties for you. Since you're mapping from within the create method, move your cards handler in there.
var mapping = {
create: function(options) {
var innerModel = ko.mapping.fromJS(options.data, {
'cards': {
create: function (options) {
debugger;
return new CardViewModel(options.data);
}
}
});
innerModel.cardCount = ko.computed(function () {
return innerModel.cards().length;
});
return innerModel;
}
};
updated fiddle
you didnt needed to have parenthesis. I just changed from
!$parent.editing()
to
!$parent.editing
See the updated fiddle here
I'm binding data to a page using KnockoutJS, the ViewModel is being populated by an JSON response from an AJAX call using the mapping plugin, like this:
$(function () {
$.getJSON("#Url.Action("Get")",
function(allData) {
viewModel = ko.mapping.fromJS(allData);
viewModel.Brokers.Url = ko.computed(function()
{
return 'BASEURLHERE/' + this.BrokerNum();
});
ko.applyBindings(viewModel);
});
});
The middle part there doesn't work (it works fine without that computed property). "Brokers" is an observable array, and I want to add a computed value to every element in the array called URL. I'm binding that Brokers array to a foreach, and I'd like to use that URL as the href attribute of an anchor. Any ideas?
I've been working through very similar issues and I've found that you can intercept the creation of the Broker objects and insert your own fields using the mapping options parameter:
var data = { "Brokers":[{"BrokerNum": "2"},{"BrokerNum": "10"}] };
var mappingOptions = {
'Brokers': {
create: function(options) {
return (new (function() {
this.Url = ko.computed(function() {
return 'http://BASEURLHERE/' + this.BrokerNum();
}, this);
ko.mapping.fromJS(options.data, {}, this); // continue the std mapping
})());
}
}
};
viewModel = ko.mapping.fromJS(data, mappingOptions);
ko.applyBindings(viewModel);
Here is a fiddle to demonstrate this: http://jsfiddle.net/pwiles/ZP2pg/
Well, if you want Url in each broker, you have to add it to each broker:
$.each(viewModel.Brokers(), function(index, broker){
broker.Url = ko.computed(function(){return 'BASEURLHERE/' + broker.BrokerNum();});
});
I guess BrokerNum is not going to change, so you might as well just calculate Url once:
$.each(viewModel.Brokers(), function(index, broker){
broker.Url = 'BASEURLHERE/' + broker.BrokerNum();
});
You can also add Url property during mapping by providing "create" callback to ko.mapping.fromJS function. See mapping plugin docs for details.
If you only need url to bind to href, just bind the expression in html (within foreach binding):
<a data-bind="attr: {href: 'BASEURLHERE/' + BrokerNum()}">link to broker details</a>
Thanks to Peter Wiles i have very similar solution:
var ViewModel = function (data, ranges) {
var self = this;
this.productList = ko.observableArray();
var productListMapping = {
create: function (options) {
return (new (function () {
//this row above i don't understand...
this.len = ko.computed(function () {
//just test function returning lenght of object name
// and one property of this model
return this.name().length + ' ' + self.cons_slider_1();
}, this);
ko.mapping.fromJS(options.data, {}, this); // continue the std mapping
})());
}
}
this.cons_slider_1 = ko.observable(100);
ko.mapping.fromJS(data, productListMapping, this.productList);
};
Some differences:
I am not mapping to self, but on this.product.
The input json has not parent name like 'Brokers' in above example:
var products = [
{ "id": "pp1", "name": "Blue windy" },
{ "id": "pp1", "name": "Blue windy" }];
So in productMapping i'm typing just 'create:'
But, what i do not understand is the structure of create function. Could somebody explain me why the function returns new function, which has property. Couldn't it be simplified somehow?