Extending knockout.js mapping with moment.js support - javascript

Are there any possibilities of extending the knockout mapping plugin to handle the mapping of different types when unmapping?
I have this model:
var Model = function () {
var self = this;
self.requestFrom = ko.observable(moment().subtract("days", 1));
self.requestTo = ko.observable(moment());
// additional properties.
};
And to get a plain JavaScript object out of that, I'm doing:
var model = new Model();
var obj = mapping.toJS(model, {
"ignore": ["requestFrom", "requestTo"]
});
obj.requestFrom = model().requestFrom().toISOString();
obj.requestTo = model().requestTo().toISOString();
I would like to avoid handling the translation of moment objects manually, and instead write some extension to knockout mapping that knows how to handle objects of type moment, and return them as their ISO string representation.
Any ideas?

Usually toJSON() is called automatically on your object when you pass it as an Ajax parameter.
It may not be directly the answer that you're looking for, but adding a custom toJSON() on your Models works pretty good I think. You have very precise control on what part of the data is send and in what format.
var Model = function () {
var self = this;
self.requestFrom = ko.observable(moment().subtract("days", 1));
self.requestTo = ko.observable(moment());
// additional properties.
self.toJSON = function()
{
return {
requestFrom: self.requestFrom(),
requestTo: self.RequestTo(),
otherProperty: self.otherProperty()
}
}
};
You can than use it directly on your Ajax requests, without converting it manually.
var model = new Model();
$.ajax({
url: '/test/PersonSubmit',
type: 'post',
dataType: 'json',
data: model
});
Note that the default toJSON() implementation of moment() is already toISOString(). See the source code of moment.js:
// add aliased format methods
moment.fn.toJSON = moment.fn.toISOString;

have you thought about using prototype to extend the ko object? I would clone the observable method into a new momentObservable method that could then be called like so:
var Model = function () {
var self = this;
self.requestFrom = ko.momentObservable.subtract("days", 1));
//other operations
};

Related

How to serialize a javascript object into json file that include all Class methods? [duplicate]

I am new to "object-oriented" JavaScript. Currently, I have an object that I need to pass across pages. My object is defined as follows:
function MyObject() { this.init(); }
MyObject.prototype = {
property1: "",
property2: "",
init: function () {
this.property1 = "First";
this.property2 = "Second";
},
test: function() {
alert("Executing test!");
}
}
On Page 1 of my application, I am creating an instance of MyObject. I am then serializing the object and storing it in local storage. I am doing this as shown here:
var mo = new MyObject();
mo.test(); // This works
window.localStorage.setItem("myObject", JSON.stringify(mo));
Now, on Page 2, I need get that object and work with it. To retrieve it, I am using the following:
var mo = window.localStorage.getItem("myObject");
mo = JSON.parse(mo);
alert(mo.property1); // This shows "First" as expected.
mo.test(); // This does not work. In fact, I get a "TypeError" that says "undefined method" in the consol window.
Based on the outputs, it looks like when I serialized the object, somehow the functions get dropped. I can still see the properties. But I can't interact with any of my functions. What am I doing wrong?
JSON doesn't serialize functions.
Take a look at the second paragraph here.
If you need to preserve such values, you can transform values as they are serialized, or prior to deserialization, to enable JSON to represent additional data types.
In other words, if you really want to JSONify the functions, you can convert them to strings before serializing:
mo.init = ''+mo.init;
mo.test = ''+mo.test;
And after deserializing, convert them back to functions.
mo.init = eval(mo.init);
mo.test = eval(mo.test);
However, there should be no reason to do that. Instead, you can have your MyObject constructor accept a simple object (as would result from parsing the JSON string) and copy the object's properties to itself.
Functions can not be serialized into a JSON object.
So I suggest you create a separate object (or property within the object) for the actual properties and just serialize this part.
Afterwards you can instantiate your object with all its functions and reapply all properties to regain access to your working object.
Following your example, this may look like this:
function MyObject() { this.init(); }
MyObject.prototype = {
data: {
property1: "",
property2: ""
},
init: function () {
this.property1 = "First";
this.property2 = "Second";
},
test: function() {
alert("Executing test!");
},
save: function( id ) {
window.localStorage.setItem( id, JSON.stringify(this.data));
},
load: function( id ) {
this.data = JSON.parse( window.getItem( id ) );
}
}
To avoid changing the structure, I prefer to use Object.assign method on object retrieval. This method merge second parameter object in the first one. To get object methods, we just need an empty new object which is used as the target parameter.
var mo = window.localStorage.getItem("myObject");
// this object has properties only
mo = JSON.parse(mo);
// this object will have properties and functions
var completeObject = Object.assign(new MyObject(), mo);
Note that the first parameter of Object.assign is modified AND returned by the function.
it looks like when I serialized the object, somehow the functions get dropped... What am I doing wrong?
Yes, functions will get dropped when using JSON.stringify() and JSON.parse(), and there is nothing wrong in your code.
To retain functions during serialization and deserialization, I've made an npm module named esserializer to solve this problem -- the JavaScript class instance values would be saved during serialization on Page 1, in plain JSON format, together with its class name information:
var ESSerializer = require('esserializer');
function MyObject() { this.init(); }
MyObject.prototype = {
property1: "",
property2: "",
init: function () {
this.property1 = "First";
this.property2 = "Second";
},
test: function() {
alert("Executing test!");
}
}
MyObject.prototype.constructor=MyObject; // This line of code is necessary, as the prototype of MyObject has been overridden above.
var mo = new MyObject();
mo.test(); // This works
window.localStorage.setItem("myObject", ESSerializer.serialize(mo));
Later on, during the deserialization stage on Page 2, esserializer can recursively deserialize object instance, with all types/functions information retained:
var mo = window.localStorage.getItem("myObject");
mo = ESSerializer.deserialize(mo, [MyObject]);
alert(mo.property1); // This shows "First" as expected.
mo.test(); // This works too.
That's because JSON.stringify() doesn't serialize functions i think.
You're right, functions get dropped. This page might help:
http://www.json.org/js.html
"Values that do not have a representation in JSON (such as functions and undefined) are excluded."

Pushing to properties in Backbone [duplicate]

This question already has an answer here:
Backbone View extends is polluted
(1 answer)
Closed 8 years ago.
I spent a lot of time trying to catch a bug in my app. Eventually I set apart this piece of code which behavior seems very strange to me.
var Model = Backbone.Model.extend({
myProperty: []
});
var one = new Model();
var two = new Model();
one.myProperty.push(1);
console.log(two.myProperty); //1!!
What's the reason behind it? Why it acts so? How to avoid this type of bugs in code?
Inheritance in JavaScript is prototypical - objects can refer directly to properties higher up in the prototype chain.
In your example, one and two both share a common prototype, and do not provide their own values for myProperty so they both refer directly to Model.protoype.myProperty.
You should create new myProperty array for each model you instantiate. Model.initialize is the idiomatic place for this kind of initialisation - overriding constructor is unnecessarily complex.
var Model = Backbone.Model.extend({
initialize: function() {
this.myProperty = [];
}
});
Alternatively you could make myProperty as an attribute of the model:
var Model = Backbone.Model.extend({
defaults: function() {
return {
myProperty: []
}
}
});
It is important to note that defaults is a function - if you were to use a simple object you would encounter the same shared reference issue.
Actually its because myProperty is an array, and as you know arrays will be stored by reference. Just to test consider the following code:
var Model = Backbone.Model.extend({
myProperty: [],
messege: ''
});
var one = new Model();
var two = new Model();
one.messege = 'One!';
two.messege = 'Two!';
console.log(one.messege ); // 'One!'
console.log(two.messege ); // 'Two!'
An alternative around this could be:
var Model = Backbone.Model.extend({
constructor: function() {
this.myProperty = [];
Backbone.Model.apply(this);
}
});
var one = new Model();
one.myProperty.push(1);
var two = new Model();
console.log(two.myProperty); // []
The documentation says:
constructor / initialize new Model([attributes], [options])
When creating an instance of a model, you can pass in the initial values of the attributes, which will be set on the model. If you define an initialize function, it will be invoked when the model is created.
In rare cases, if you're looking to get fancy, you may want to override constructor, which allows you to replace the actual constructor function for your model.
So, following the documentation, you'd want to do something like this to get your case running:
var Model = Backbone.Model.extend({
initialize: function() {
this.myProperty = [];
}
});
source: http://backbonejs.org/#Model-extend

Match values in nested object to corresponding knockout bindings?

Let's say I have a list of knockout bindings placed in a nested/namespaced object, resembling this:
var bindings = {
event: {
eventid: ko.observable(),
office: ko.observable(),
employee: {
name: ko.observable(),
group: ko.observable()
}
},
...
}
Now let's say there are a number of different sets of data that might be loaded into this - so one does an ajax query and gets a JSON result like this:
{
"defaults": {
"event": {
"eventid": 1234,
"employee": {
"name": "John Smith"
}
},
...
}
}
Note that not every binding has a default value - but all defaults are mapped to a binding. What I want to do is read the defaults into whatever knockout binding they correspond to.
There are definitely ways to traverse a nested object and read its values. Adding an extra argument to that example, I can keep track of the default's full key (eg event.employee.name). Where I'm getting stumped is taking the default's key and using it to target the associated knockout binding. Obviously, even if i have key = "event.employee.name", bindings.key doesn't reference what I want. I can only think of using eval(), and that leaves me with a bad taste in my mouth.
How would one go about using a key to reference the same location in a different object? Perhaps knockout provides a way to auto-map an object to its bindings, and I've just overlooked it? Any insight would be helpful. Thanks in advance!
I would suggest you have a look at the Knockout Mapping Plugin which will do most of what you want to do. If that doesn't workout then you can turn your bindings object into a series of constructor functions that accepts a data parameter. Something like
var Employee = function (data){
var self = this;
self.name = ko.observbale(data.name || '');
self.group = ko.observable(data.group);
};
var Event = function(data){
var self = this;
self.eventid = ko.observable(data.id || 0);
self.office = ko.observable(data.office || '');
self.employee = ko.observable(new Employee(data.employee));
};
var bindings = function(data){
var self = this;
self.event = ko.observable(new Event(data));
}
I'll be putting Nathan Fisher's solution into a future update, but I wanted to share the fix I found for now as well. Each time the defaults object recurses, I simply pass the corresponding bindings object instead of tracking the entire keypath.
var setToDefaults = function(data){
loopDefaults(data.defaults, bindings);
};
var loopDefaults = function(defaults, targ){
for(var d in defaults){
if(defaults.hasOwnProperty(d) && defaults[d]!==null){
if(typeof(defaults[d])=="object"){
loopDefaults(defaults[d], targ[d]);
}else{
// defaults[d] is a value - set to corresponding knockout binding
targ[d](defaults[d]);
}
}
}
};

set attribute of all models in backbone collection

I understand that using pluck method we can get an array of attributes of each model inside a backbone collection
var idsInCollection = collection.pluck('id'); // outputs ["id1","id2"...]
I want to if there is a method that sets up an attribute to each model in the collection,
var urlArray = ["https://url1", "https://url1" ...];
collection.WHAT_IS_THIS_METHOD({"urls": urlArray});
There's not exactly a pre-existing method, but invoke let's you do something similar in a single line:
collection.invoke('set', {"urls": urlArray});
If you wanted to make a re-usable set method on all of your collections, you could do the following:
var YourCollection = Backbone.Collection.extend({
set: function(attributes) {
this.invoke('set', attributes);
// NOTE: This would need to get a little more complex to support the
// set(key, value) syntax
}
});
* EDIT *
Backbone has since added its own set method, and if you overwrite it you'll completely break your Collection. Therefore the above example should really be renamed to setModelAttributes, or anything else which isn't set.
I don’t there is a method for it, but you can try:
collection.forEach(function(model, index) {
model.set(url, urlArray[index]);
});
Expanding on David's answer, you can easily put this functionality into a custom method on the collection. Here's my way of doing it using coffeescript:
class Collection extends Backbone.Collection
setAll: () ->
_args = arguments
#models.forEach (model) -> model.set _args...
class SomeCollection extends Collection
url: '/some-endpoint.json'
myLovelyCollection = new SomeCollection()
myLovelyCollection.fetch
success: (collection, response) ->
collection.setAll someVar: true
collection.setAll anotherVar, 'test'
If you wanted to do it in vanilla JS it's exactly the same but not harnessing the power of classes or splats. So more like:
var Collection = Backbone.Collection.extend({
setAll: function () {
var _args = arguments;
this.models.forEach(function (model) {
model.set.apply(model, _args);
});
}
});
Just thought I'd post my slightly updated method based on machineghost's version. This uses lodash invokeMap method instead of underscore's invoke. It supports the same optional syntax as the standard model.set method ... e.g. ('prop', 'val') or ({prop: 'val', prop: 'val'}) as well accepting and passing an options object.
var YourCollection = Backbone.Collection.extend({
setModels: function(key, val, options) {
var attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
if (attrs) {
_.invokeMap(this, 'set', attrs, options);
}
return this;
}
});
If you are using invoke the syntax according to the underscore site should be
_.invoke(list, methodName, *arguments) http://underscorejs.org/#invoke
So the above function mentioned by machineghost should be
collection.invoke({'url': someURL},'set');
Hope that helps :)

Backbone: Id not being set to model

I have tried the following to set an id to my model:
var globalCounter = 1;
var Model = Backbone.Model.extend({
initialize: function () {
this.id = globalCounter;
globalCounter += 1;
}
});
myModel = new Model();
console.log(myMode.get('id')); // prints undefined
How can I set an id to my models?
You need to use the set() function instead (http://jsbin.com/agosub/1/);
var globalCounter = 1;
var Model = Backbone.Model.extend({
initialize: function () {
this.set('id', globalCounter);
globalCounter += 1;
}
});
myModel = new Model();
console.log(myModel.get('id')); // prints 1
You must use :
this.set('id', globalCounter);
instead of this.id = globalCounter;
You are adding the id value to the Model object, but you want to add it to Model.attributes object. And that what is doing Model.set() method.
model.set("key", value) will put the value in model.attributes.key;
model.get("key") will return the value inside model.attributes.key
This is a little weird for new comers to Backbone, but it's a major (and easy) point to get. It's designed so that using model.set(...) will fire change events you can easily catch to update your views.
Backbone and ES6 Update :
The Backbone attribute object is outdates by ES6 getters and setters. Theses functions can overwrite the standard access.
Warning : this is pseudo-code that may be one day used with ES6 !
class MyModel extends Backbone.Model{
get id(){ return this.attributes.id; }
set id(id){ this.attributes.id = id; }
}
This would allow to write :
let myModel = new Model();
myModel.id = 13; // will use myModel.id(13)
console.log (myModel.id); // will show myModel.id()
As of today, this is only a dream of a Backbone 2. After basic searches, I've seen nothing about that coming.

Categories

Resources