This is driving me absolutely insane, and I just cannot see what I have done wrong. Please help before I start bibbling and gnawing my colleagues leg off. He doesn't deserve that.
I have an object I am mapping, which has a property that can or cannot contain an object of that same type. That is the only level of nesting there is. It is very simple; it is complicated only by the fact that the object is calling a base class constructor to set some default behaviour.
This base class sets up all the fields that can appear in the model (it is a generated file) and then maps the datasource, if it has one.
The mapping of the nested field to the correct constructor works if the field is set up initially as an observable. It does not if it is set up as a plain object.
var NS = {};
var _itest = 0;
NS.FieldModelBase = function(data, mapping)
{
var _this = this;
this.Text = ko.observable();
// DOES NOT WORK
this.AlternateField = {};
// WORKS
//this.AlternateField = ko.observable();
ko.mapping.fromJS(data, mapping, _this);
};
// =====================================================================
NS.FieldModel = function(data, mapping, parent)
{
var _this = this;
window.console && console.log('CREATING FIELD', data);
var _mapping = $.extend({}, mapping, {
'include': [ 'Test' ],
'AlternateField': {
create:
function(o)
{
window.console && console.log('FOUND SUBFIELD', o.data);
return o.data ? new NS.FieldModel(o.data) : null;
}
}
});
this.Test = ko.observable(_itest++);
NS.FieldModelBase.call(_this, data, _mapping);
}
// =====================================================================
var model = new NS.FieldModel({
Text: "Main option",
AlternateField: {
Text: "Alternate option",
AlternateField: null
}
}, { include: [ 'Test' ] });
ko.applyBindings(model);
https://jsfiddle.net/whelkaholism/fkr0w98u/
So, when setup as an object, printing model out after running the code gives:
{"Test":0,"Text":"Main option","AlternateField":{"Text":"Alternate option","AlternateField":null}}
There is no Test property on the alternate field. If you check the console, what happens is that the mapping create is in fact called, but the o.data property is null.
Change to an observable, and the output is, as expected:
{"Test":0,"Text":"Main option","AlternateField":{"Test":1,"Text":"Alternate option","AlternateField":null}}
So, what is the mapping plugin doing here? It was my understanding that it would map everything in source data, regardless of the existence or type of any existing properties on the object?
EDIT: I have solved my immediate problem with this change:
NS.FieldModel = function(data, mapping, parent)
{
var _this = this;
var _mapping = {
copy: [ 'AlternateField' ]
};
NS.FieldModelBase.call(_this, data, _mapping);
this.AlternateField = data.AlternateField ? new NS.FieldModel(data.AlternateField, null, _this) : null;
}
This manually creates the correct object type for the alternate field after the mapping. The copy directive in the mapping is absolutely required, or the newly created object has no properties mapped.
I don't now why this is, so I'm still looking for the answer on why the mapping plugin works differently depending on the content of pre-existing variables, because I despise having code that I don't know exactly why it works!
The mapping plugin maps only values, not objects e.g.
"myString" => ko.observable("myString")
null => ko.observable(null)
{ myStringProperty: "myString" } => { myStringProperty: ko.observable("myString") }
{ myProperty: null } => { myPropery: ko.observable(null) }
{} => {}
Related
Quite a long question.But it basically describe pretty much describe what i want.This code here for creating any object according to user input to a input text field.If i want to create Number,String or Object, i just have to write Number/String/Object in the text field.
a button has been introduced to call createObj.getObjName and get input from the text field.It tries to match the input for any three type of object which are Number/String/Object.Then it calls a bool function [problem lies here i think] which iterate over **list_keys array** using Array.prototype.some which is created using a json object that holds different object name.They are assigned with objects created using new keyword.
problem is to call check function i tried to forced the THIS keyword to indicate createObj object inside it instead of window object.I tried to console.log(this) it gives me this output which is the input html button element.
<input type='button' id='btn' value="create">
But i want THIS to refer to the createObj itself.Why it is referring to HTML element ??How can i solve this?
Full Code:
(function() {
var list = {
"Number": new Number(),
"String": new String(),
"Object": new Object(),
"Array": new Array()
}
var list_keys = Object.keys(list);
var createObj = {
re: null,
text: null,
match: null,
newObj: null,
getObjName: function() {
this.re = /Number|Object|String/;
this.text = document.getElementById('text').value;
this.match = this.text.match(this.re);
var bool = check.call(this);
if (bool) {
this.makeObj(list.this.match[0]);
}
},
makeObj: function(obj) {
this.newObj = obj;
}
};
function check() {
console.log(this);
return list_keys.some(function(el, indx, arr) {
return this.match[0] === el;
});
}
document.getElementById('btn').addEventListener('click', createObj.getObjName);
})();
You can just bind the object, like this
createObj.getObjName.bind(createObj)
This will return a new function, with this referring createObj inside getObjName.
Also, what if there is no match at all?
return this.match[0] === el;
this will fail at run time, since this.match will be null. So you might want to do something like this
return this.match && list_keys.some(...);
If I were to write the same code, in a better way, I would do something like this
(function() {
var MyObject = {
newObject: null,
createObject: function() {
var text = document.getElementById('text').value;
if (window.hasOwnProperty(text)) {
this.newObject = new window[text];
}
}
}
document.getElementById('btn').addEventListener('click',
MyObject.createObject.bind(MyObject));
})();
when you bind a function to an event, this refers to the html node the event was bound to. You can get around this in a variety of ways. The first that comes to mind is referencing createObj directly:
getObjName: function() {
createObj.re = /Number|Object|String/;
createObj.text = document.getElementById('text').value;
createObj.match = this.text.match(createObj.re);
var bool = check.call(createObj);
if (bool) {
this.makeObj(list.createObj.match[0]);
}
},
this is less than ideal, because it references the object by name, which means if you change the object's name, you'll have to change all references to it. It also presents a problem if you (or another developer working with your code) defines a new createObj down the line. Since it is referencing the object by name it would start using the newly declared object instead of yours. An improvement would be to create an alias for the object (typically called that):
getObjName: function() {
that.re = /Number|Object|String/;
that.text = document.getElementById('text').value;
that.match = this.text.match(that.re);
var bool = check.call(that);
if (bool) {
this.makeObj(list.that.match[0]);
}
},
...
var that = createObj
the problem with this is that that is usually meant to reference this in a scope where the context is lost, not an arbitrary object (in this case createObj).
Furthermore, I'm not sure that function belongs as a method of the createObj, or at least to the struct containing your data. Separation of concerns is important, and while
var createObj = {
re: null,
text: null,
match: null,
newObj: null,
is concerned with the data you're manipulating, getObjName and makeObj are concerned with event handling and fabricating an object using the collected data. I'd extract a struct out to hold the data, which I'd then use in my other objects:
var descriptors = {
re: null,
text: null,
match: null,
newObj: null
}
var getObjName = function() {
descriptors.re = /Number|Object|String/;
descriptors.text = document.getElementById('text').value;
descriptors.match = descriptors.text.match(descriptors.re);
var bool = check.call(descriptors);
if (bool) {
makeObj(list.descriptors.match[0]);
}
}
var makeObj = function(obj) {
this.newObj = obj;
}
function check() {
console.log(this);
return list_keys.some(function(el, indx, arr) {
return this.match[0] === el;
});
}
document.getElementById('btn').addEventListener('click', getObjName);
this separates struct from functionality, and better conveys the intent of the parts (descriptors holds all data, getObjName handles the event, and makeObj instantiates the new object).
there's still one issue with this though, from a design perspective, getObjName violates the Single Responsibility Principle. It is tasked with handling an event and getting the object's name. I'd refactor the event handling portion out to stay true to getObjName's intent:
var getObjName = function(descriptors) {
descriptors.re = /Number|Object|String/;
descriptors.text = document.getElementById('text').value;
descriptors.match = this.text.match(descriptors.re);
return check.call(descriptors);
}
document.getElementById('btn').addEventListener('click', function() {
if (getObjName(descriptors)) {
makeObj(list.descriptors.match[0]);
}
});
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]);
}
}
}
};
I have a class in JS with field
Widget = function ()
{
this.Attributes = []; // key=value
}
and another class iherited from Widget
BusinessStatisticWidget = function ()
{
// some code
};
BusinessStatisticWidget.prototype = new Widget();
At initialization stage I have assigned this Attributes field with values (only once) and at some point Atttibutes field becomes empty:
BusinessStatisticWidget.prototype.SetEventsOnControls = function ()
{
var dropDown = document.getElementById(this.DropDownName + this.type + "Id");
var _this = this; // **Not empty here**
dropDown.addEventListener("change", function (event)
{
// **Not empty even here**
_this.CalculateAndSetTimeRangeForTimeSpan(event.target.value);
}, false);
}
BusinessStatisticWidget.prototype.CalculateAndSetTimeRangeForTimeSpan = function (val)
{
// **Empty here**
if (this.Attributes["fromDate"].value != '' && this.Attributes["toDate"].value != '')
{}
}
The code above works fine in Chrome and IE10 (I mean that array is not empty) but dont work in Firefox(20.0.1)
As array is empty I get TypeError: this.Attributes.fromDate is undefined.
And I dont know why it is empty and how to fix this.
There are multiple problems with your code:
Don't use arrays for arbitrary key, value pairs. Use only numerical keys for arrays.
Each instance will share the same Attributes array. This is usually not the desired behaviour.
Solutions:
Use an object instead.
Setup inheritance properly and call the parent constructor in the child constructor.
Code:
Widget = function () {
this.Attributes = {}; // use an pbject
};
var BusinessStatisticWidget = function () {
// call parent constructor
Widget.call(this);
// some code
};
// set up inheritance
BusinessStatisticWidget.prototype = Object.create(Widget.prototype);
More information (and polyfill) about Object.create.
Now, I don't know if that fixes your problem, but it makes your code at least more correct so that finding the issue becomes easier. I recommend to learn how to debug JavaScript.
I have a simple question about Backbone.js' get and set functions.
1) With the code below, how can I 'get' or 'set' obj1.myAttribute1 directly?
Another question:
2) In the Model, aside from the defaults object, where can/should I declare my model's other attributes, such that they can be accessed via Backbone's get and set methods?
var MyModel = Backbone.Model.extend({
defaults: {
obj1 : {
"myAttribute1" : false,
"myAttribute2" : true,
}
}
})
var MyView = Backbone.View.extend({
myFunc: function(){
console.log(this.model.get("obj1"));
//returns the obj1 object
//but how do I get obj1.myAttribute1 directly so that it returns false?
}
});
I know I can do:
this.model.get("obj1").myAttribute1;
but is that good practice?
While this.model.get("obj1").myAttribute1 is fine, it's a bit problematic because then you might be tempted to do the same type of thing for set, i.e.
this.model.get("obj1").myAttribute1 = true;
But if you do this, you won't get the benefits of Backbone models for myAttribute1, like change events or validation.
A better solution would be to never nest POJSOs ("plain old JavaScript objects") in your models, and instead nest custom model classes. So it would look something like this:
var Obj = Backbone.Model.extend({
defaults: {
myAttribute1: false,
myAttribute2: true
}
});
var MyModel = Backbone.Model.extend({
initialize: function () {
this.set("obj1", new Obj());
}
});
Then the accessing code would be
var x = this.model.get("obj1").get("myAttribute1");
but more importantly the setting code would be
this.model.get("obj1").set({ myAttribute1: true });
which will fire appropriate change events and the like. Working example here: http://jsfiddle.net/g3U7j/
I created backbone-deep-model for this - just extend Backbone.DeepModel instead of Backbone.Model and you can then use paths to get/set nested model attributes. It maintains change events too.
model.bind('change:user.name.first', function(){...});
model.set({'user.name.first': 'Eric'});
model.get('user.name.first'); //Eric
Domenic's solution will work however each new MyModel will point to the same instance of Obj.
To avoid this, MyModel should look like:
var MyModel = Backbone.Model.extend({
initialize: function() {
myDefaults = {
obj1: new Obj()
}
this.set(myDefaults);
}
});
See c3rin's answer # https://stackoverflow.com/a/6364480/1072653 for a full explanation.
I use this approach.
If you have a Backbone model like this:
var nestedAttrModel = new Backbone.Model({
a: {b: 1, c: 2}
});
You can set the attribute "a.b" with:
var _a = _.omit(nestedAttrModel.get('a')); // from underscore.js
_a.b = 3;
nestedAttrModel.set('a', _a);
Now your model will have attributes like:
{a: {b: 3, c: 2}}
with the "change" event fired.
There is one solution nobody thought of yet which is lots to use. You indeed can't set nested attributes directly, unless you use a third party library which you probably don't want. However what you can do is make a clone of the original dictionary, set the nested property there and than set that whole dictionary. Piece of cake.
//How model.obj1 looks like
obj1: {
myAttribute1: false,
myAttribute2: true,
anotherNestedDict: {
myAttribute3: false
}
}
//Make a clone of it
var cloneOfObject1 = JSON.parse(JSON.stringify(this.model.get('obj1')));
//Let's day we want to change myAttribute1 to false and myAttribute3 to true
cloneOfObject1.myAttribute2 = false;
cloneOfObject1.anotherNestedDict.myAttribute3 = true;
//And now we set the whole dictionary
this.model.set('obj1', cloneOfObject1);
//Job done, happy birthday
I had the same problem #pagewil and #Benno had with #Domenic's solution. My answer was to instead write a simple sub-class of Backbone.Model that fixes the problem.
// Special model implementation that allows you to easily nest Backbone models as properties.
Backbone.NestedModel = Backbone.Model.extend({
// Define Backbone models that are present in properties
// Expected Format:
// [{key: 'courses', model: Course}]
models: [],
set: function(key, value, options) {
var attrs, attr, val;
if (_.isObject(key) || key == null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}
_.each(this.models, function(item){
if (_.isObject(attrs[item.key])) {
attrs[item.key] = new item.model(attrs[item.key]);
}
},this);
return Backbone.Model.prototype.set.call(this, attrs, options);
}
});
var Obj = Backbone.Model.extend({
defaults: {
myAttribute1: false,
myAttribute2: true
}
});
var MyModel = Backbone.NestedModel.extend({
defaults: {
obj1: new Obj()
},
models: [{key: 'obj1', model: Obj}]
});
What NestedModel does for you is allow these to work (which is what happens when myModel gets set via JSON data):
var myModel = new MyModel();
myModel.set({ obj1: { myAttribute1: 'abc', myAttribute2: 'xyz' } });
myModel.set('obj1', { myAttribute1: 123, myAttribute2: 456 });
It would be easy to generate the models list automatically in initialize, but this solution was good enough for me.
Solution proposed by Domenic has some drawbacks. Say you want to listen to 'change' event. In that case 'initialize' method will not be fired and your custom value for attribute will be replaced with json object from server. In my project I faced with this problem. My solution to override 'set' method of Model:
set: function(key, val, options) {
if (typeof key === 'object') {
var attrs = key;
attrs.content = new module.BaseItem(attrs.content || {});
attrs.children = new module.MenuItems(attrs.children || []);
}
return Backbone.Model.prototype.set.call(this, key, val, options);
},
While in some cases using Backbone models instead of nested Object attributes makes sense as Domenic mentioned, in simpler cases you could create a setter function in the model:
var MyModel = Backbone.Model.extend({
defaults: {
obj1 : {
"myAttribute1" : false,
"myAttribute2" : true,
}
},
setObj1Attribute: function(name, value) {
var obj1 = this.get('obj1');
obj1[name] = value;
this.set('obj1', obj1);
}
})
If you interact with backend, which requires object with nesting structure.
But with backbone more easy to work with linear structure.
backbone.linear can help you.
I really never used a javascript function type or class before, I understand Java and Python, but not javascript. So, I build a class like this:
function FormStore (type) {
this.setup = () =>{
this.store = {};
this.ERR_LINE_PREFIX = '#err_';
this.NO_DISPLAY_CLASS = 'no-display';
this.settings = {
'myID':{'hide':false},
}
}
this.checkVal= () => {
var geoArr = ['id_xx','myID', (...)];
var id;
$.each( geoArr, function(val) {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
};
var FS = new FormStore();
FS.setup();
The store is filled by components on document.ready. There is a function that looks up if the aligned components (glyph, label, input) have some classes or values and for the specific component fills a dict: {label:false,glyph:false, input:false}. However, for some reason it doesn't matter. Even if I enter some values in to the store right away (in setup) or create them on the fly, in checkVal the store doesn't exist, it's undefined.
Please, anybody, what am I not understanding about javascript type and classes here? I am googling this a lot and trying to find good resources but, "javascipt variable class" (or type) just yields a lot of DOM manipulation.
edit
There is a context problem in checkVal, you are using a non-arrow (and not explicitly bound) callback function and trying to access this inside of it. Change that to an arrow function as well, and the parent context (this) will be preserved:
$.each( geoArr, (val) => {
id = geoArr[val];
console.log(this.store)
if (!(this.store[id])) {
return false;
}
});
And while you are at changing that section, it's not going to work. You will not get access to $.each's return value. You should rely on native array APIs for this task and use Array.every to determine if all geoArr items are in the store (assuming that's your goal):
// returns false if not all geoArr items are in the store
geoArr.every(id => this.store[id])
original
I don't see you calling checkVal() anywhere, but based on the error you are getting it is called prior to setup() (since setup initializes the store). You could solve that problem straight away by moving this.store = {} out of setup (right at the top), e.g.:
function FormStore(type) {
this.store = {};
...
Having said that, I would suggest either defining your methods on the prototype, or utilizing ES6 classes. Here is a simplified version of both:
ES5 class
function FormStore(type) {
// make sure user didn't forget new keyword
if (this === window) {
throw new Error('FormStore must be called with "new" keyword')
}
// initialize state, this is the constructor
this.type = type;
this.store = {};
// any other state the class manages
}
FormStore.prototype = {
setup: function() {
// do setup stuff
// "this" points to instance
console.log('setup', this.type)
},
checkVal: function() {
}
}
var formStore = new FormStore('foo')
console.log(formStore.store) // <-- not undefined
formStore.setup()
ES6 Class
class FormStore {
constructor(type) {
this.type = type;
this.store = {};
}
setup() {
console.log('setup', this.type)
}
checkVal() {
}
}
const formStore = new FormStore('bar')
console.log(formStore.store) // <-- not undefined
formStore.setup()
It has to do with scoping. Your $.each in checkVal has a normal function. Inside the function the scope if this is different. If you want to keep the original scope you could use a fat arrow function like you do when defining the methods.
this.checkVal= () => {
var geoArr = ['id_xx','myID', (...)];
var id;
$.each( geoArr, val => {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
}
When you run your original code and place a breakpoint on the line with console.log you can see in the inspector that this is set to the Window object and no longer points to your FormStore.
function FormStore () {
this.setup = function(){
this.store = {};
this.ERR_LINE_PREFIX = '#err_';
this.NO_DISPLAY_CLASS = 'no-display';
this.settings = {
'myID':{'hide':false},
}
}
this.checkVal= function(){
var geoArr = ['id_xx','myID'];
var id;
$.each( geoArr, function(val) {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
}
};
var FS = new FormStore();
FS.setup();
Works absolutely fine, the code you provided had a missing bracket and you were using some broken es6 syntax