In the name of keeping things DRY, I'd like to ask what the typical approach is when trying to avoid declaring duplicate properties. I have two viewModels: set and folder. Here they are:
Folder:
var folderViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
self.isHovering = ko.observable(false);
self.showCheckbox = function () {
self.isHovering(true);
};
self.hideCheckbox = function () {
self.isHovering(false);
};
self.checkboxIsVisible = ko.computed(function () {
return selectedItemsCount() > 0 || self.isHovering();
}, self);
self.softCheckboxIsVisible = ko.computed(function () {
return selectedItemsCount() > 0 && self.isHovering() == false;
}, self);
self.canDrag = ko.computed(function () {
if (selectedItemsCount() == 0 && !isAddingNewContent()) {
return true;
} else {
return false;
}
}, self);
self.isSelected = ko.observable(false);
self.toggleSelected = function () {
self.isSelected(!self.isSelected());
};
self.textSelected = ko.observable(false);
self.toggleTextSelected = function () {
self.textSelected(!self.textSelected());
};
self.isSet = ko.observable(false);
self.isDeleting = ko.observable(false);
self.isNew = ko.observable(false);
// If the folder hasn't been created yet, it won't have a folderId
if (typeof self.folderId === 'undefined') {
self.isNew(true);
}
self.isEditing = ko.observable(false).publishOn("IS_EDITING_CONTENT");
// monitor for clicks
// temp title
self.oldTitle = ko.observable();
};
Set:
var setViewModel = function (data) {
var self = this;
// Checkbox controls
self.isHovering = ko.observable(false);
self.showCheckbox = function () {
self.isHovering(true);
};
self.hideCheckbox = function () {
self.isHovering(false);
};
self.checkboxIsVisible = ko.computed(function () {
return selectedItemsCount() > 0 || this.isHovering();
}, self);
self.softCheckboxIsVisible = ko.computed(function () {
return selectedItemsCount() > 0 && this.isHovering() == false;
}, self);
self.canDrag = ko.computed(function () {
if (selectedItemsCount() == 0 && !isAddingNewContent()) {
return true;
} else {
return false;
}
}, self);
self.isSelected = ko.observable(false);
self.toggleSelected = function () {
self.isSelected(!self.isSelected());
};
self.textSelected = ko.observable(false);
self.toggleTextSelected = function () {
self.textSelected(!self.textSelected());
};
self.isSet = ko.observable(true);
ko.mapping.fromJS(data, {}, self);
self.isDeleting = ko.observable(false);
self.isNew = ko.observable(false);
// If the folder hasn't been created yet, it won't have a folderId
if (typeof self.setId === 'undefined') {
self.isNew(true);
}
self.isEditing = ko.observable(false).publishOn("IS_EDITING_CONTENT");
// temp title
self.oldTitle = ko.observable();
};
A lot of these properties are duplicated between the viewModels. Should I just keep them as is, or is there a nice way to condense this code?
Create a helper method that both viewmodel constructors call to add all of the common properties...
var helper = function (self, data) {
self.isHovering = ko.observable(false);
// ...
return self;
};
var setViewModel = function (data) {
var self = helper(this, data);
// extra stuff
};
var folderViewModel = function (data) {
var self = helper(this, data);
// extra stuff
};
What about trying inheritance? You could program a prototype viewModel with the properties and functions that both (set and folder) have and then define new "classes" for setViewModel and folderViewModel that have the same prototype as viewModel, just added the properties and functions that only they have.
Introduction to inheritance in javascript can be found here...
Related
How to create a simple web page that check dirty value with knockout js?
ps:simple code written
This should be plenty to get you started, note the author is rniemeyer:
http://jsfiddle.net/rniemeyer/dtpfv/?utm_source=website&utm_medium=embed&utm_campaign=dtpfv
//not used in this example. one time flag, that drops its subscriptions after the first change.
ko.oneTimeDirtyFlag = function (root) {
var _initialized;
//one-time dirty flag that gives up its dependencies on first change
var result = ko.computed(function () {
if (!_initialized) {
//just for subscriptions
ko.toJS(root);
//next time return true and avoid ko.toJS
_initialized = true;
//on initialization this flag is not dirty
return false;
}
//on subsequent changes, flag is now dirty
return true;
});
return result;
};
ko.dirtyFlag = function(root, isInitiallyDirty) {
var result = function() {},
_initialState = ko.observable(ko.toJSON(root)),
_isInitiallyDirty = ko.observable(isInitiallyDirty);
result.isDirty = ko.computed(function() {
return _isInitiallyDirty() || _initialState() !== ko.toJSON(root);
});
result.reset = function() {
_initialState(ko.toJSON(root));
_isInitiallyDirty(false);
};
return result;
};
function Item(id, name) {
this.id = ko.observable(id);
this.name = ko.observable(name);
this.dirtyFlag = new ko.dirtyFlag(this);
}
var ViewModel = function(items) {
this.items = ko.observableArray([
new Item(1, "one"),
new Item(2, "two"),
new Item(3, "three")
]);
this.save = function() {
alert("Sending changes to server: " + ko.toJSON(this.dirtyItems));
};
this.dirtyItems = ko.computed(function() {
return ko.utils.arrayFilter(this.items(), function(item) {
return item.dirtyFlag.isDirty();
});
}, this);
this.isDirty = ko.computed(function() {
return this.dirtyItems().length > 0;
}, this);
};
ko.applyBindings(new ViewModel());
I want to create a factory that is responsible for generating a PlayerList, but I'm having problems accessing the variables I set in the initialize function. The code is
app.factory("PlayerList", function(){
// Define the PlayerList function
var PlayerList = function() {
this.initialize = function() {
// create an array for our players
var players = [];
};
this.add = function(player) {
this.players.push(player);
}
this.remove = function(player) {
if ( players.length > 0 )
{
this.players.splice(players.indexOf(player), 1);
}
}
this.initialize();
};
return (PlayerList);
});
I want to refer to the players array inside the add and remove methods but I'm getting back undefined.
Here var players = []; is local variable for initialize but your are expecting this.players.push(player); means players should be in PlayerList scope.
So your factory should be look like
app.factory("PlayerList", function () {
// Define the PlayerList function
var PlayerList = function () {
var self = this;
this.players = [];
this.initialize = function () {
self.players = [];
};
this.add = function (player) {
self.players.push(player);
console.log(self.players);
}
this.remove = function (player) {
if (self.players.length > 0) {
self.players.splice(self.players.indexOf(player), 1);
}
}
this.initialize();
};
return (PlayerList);
});
var playerList = (function (){
var playerLists = {};
playerList.playerList = function() {
this.initialize = function() {
// create an array for our players
var players = [];
};
this.add = function(player) {
this.players.push(player);
}
this.remove = function(player) {
if ( players.length > 0 )
{
this.players.splice(players.indexOf(player), 1);
}
}
this.initialize();
};
return playerLists;
})();
app.factory("PlayerList",playerList.playerList);
I need to use logic like visitor pattern and I've created new
sample which failed in visitor.visit(self); and I got error undefined is not a function,
any idea what am I missing?
var Entity = function (file,name) {
var self = this;
var name;
var type;
var log = {};
this.setName = function (name) {
this.name = name;
};
this.accept = function (visitor) {
visitor.visit(self);
};
this.getName = function () {
return name;
};
this.getType = function () {
return type;
};
this.getLog = function () {
return log;
};
};
//Start using visitor
var verifyFile = function () {
this.visit = function (file) {
alert("test");
};
};
function test(){
var file = new Entity();
file.accept(verifyFile);
};
You are injecting a function that defines a function, but your code is looking for an object that contains a function - see below
var Entity = function(file, name) {
var self = this;
var name;
var type;
var log = {};
this.setName = function(name) {
this.name = name;
};
this.accept = function(visitor) {
visitor.visit(self);
};
this.getName = function() {
return name;
};
this.getType = function() {
return type;
};
this.getLog = function() {
return log;
};
};
//Start using visitor
var verifyFile = {
visit : function(file) {
alert("test");
}
};
function test() {
var file = new Entity();
file.accept(verifyFile);
};
test()
I'm trying to write a basic form using KnockoutJS using the following code:
var Form = function() {
var self = this;
self.name = {
value: ko.observable(""),
isValid: ko.computed(function () {
return self.name.value().length > 0;
}),
},
self.email = {
value: ko.observable(""),
isValid: ko.computed(function () {
return false;
})
},
self.message = {
value: ko.observable(""),
isValid: ko.computed(function () {
return false;
})
},
self.isValid = ko.computed(function () {
return self.name.isValid && self.email.isValid && self.message.isValid;
})
};
However, when I try to run this code I get the following error: Unable to get property 'value' of undefined or null reference. To me, this looks like a scope issue, but I'm not familiar enough with Knockout to understand why this is happening. Without Knockout I seem to be able to get this code working (replacing all observables with true for value and functions for isValid), but I'm looking to get these values updated in real-time. I could always separate out the validation functions to top-level functions but it seems like the improper way to do this. Each form field has a value and it's own unique validation, so it makes sense to make each form field it's own object with both properties.
Any help or guidance is appreciated.
This
self.name = {
value: ko.observable(""),
isValid: ko.computed(function () {
return self.name.value().length > 0;
}),
},
doesn't work because the inner function (the computed callback) refers to self.name, which is not even completely defined yet. Since computeds are called immediately, you see an error.
You could use the deferEvaluation option to delay evaluating the isValid computed until something actually requests its value:
self.name = {
value: ko.observable(""),
isValid: ko.computed({
read: function () { return self.name.value().length > 0; },
deferEvaluation: true
}),
},
That would work, but it would also quickly start to become repetitive and unwieldy.
As an alternative you can use Knockout extenders to make an observable validatatable in an abstract/decoupled fashion.
ko.extenders.validator = function (target, validationCallback) {
// create an isValid property on the target observable,
// immediately calculate validity of its current value
target.isValid = ko.observable(validationCallback(target()));
// subscribe to future changes
target.subscribe(function (newValue) {
target.isValid(validationCallback(newValue));
});
// return target observable for chaining
return target;
};
Now with this extender defined, all you need to do is create callback functions that validate a value and return true or false.
var Form = function () {
var self = this;
self.name = ko.observable("").extend({
validator: function (value) { return value.length > 0; }
});
self.email = ko.observable("").extend({
validator: function (value) { return true; }
});
self.message = ko.observable("").extend({
validator: function (value) { return true; }
});
self.isValid = ko.computed(function () {
var overallValid = true;
ko.utils.objectForEach(self, function (key, value) {
if (value.hasOwnProperty("isValid")) {
overallValid = overallValid && value.isValid();
}
});
return overallValid;
});
};
Further separating your validation functions now becomes very simple:
var validators = {
email: function (value) { return /* email check here */; },
minLen: function (minLen) {
return function (value) { return value.length >= minLen; }
},
maxLen: function (maxLen) {
return function (value) { return value.length <= maxLen; }
},
minmaxLen: function (minLen, maxLen) {
return function (value) { return value.length >= minLen && value.length <= maxLen; }
},
matches: function (regex) {
return function (value) { return regex.test(value); }
}
}
and
self.name = ko.observable("").extend({ validator: validators.minLen(1) });
self.age = ko.observable("").extend({ validator: validators.matches(/^\d+$/) });
You will find that of course somebody already did all of this (and much more).
It's best to create separate functions for each of your objects:
var Form = function() {
var self = this;
self.name = new NameFormField();
}
var NameFormField = function() {
var self = this;
self.value = ko.observable("");
self.isValid = ko.computed(function () {
//not sure why you are checking length here
return self.value().length > 0;
});
}
The other way that you could get it to work is by doing this:
var Form = function() {
var self = this;
self.name = {
value: ko.observable("")
};
self.name.isValid = ko.computed(function () {
//not sure why you are checking length here
return self.name.value().length > 0;
});
I have three relatively similar knockout models in my application and I would like to extend a base model to combine common properties rather than repeat myself three times.
example
var ItemModel = function (item) {
var self = this;
self.order = ko.observable(item.order);
self.title = ko.observable(item.title);
self.price = ko.observable(item.price);
self.type = ko.observable(item.type);
};
var StandardItemModel = function (item, cartItemTypes) {
var self = this;
self.order = ko.observable(item.order);
self.title = ko.observable(item.title);
self.price = ko.observable(item.price);
self.type = ko.observable(item.type);
self.isInCart = ko.computed(function () {
return cartItemTypes().indexOf(item.type) > -1;
}, self);
self.itemClass = ko.computed(function () {
return self.isInCart() ? "icon-check" : "icon-check-empty";
}, self);
};
var CustomItemModel = function (item) {
var self = this;
self.order = ko.observable(item.order);
self.title = ko.observable(item.title);
self.price = ko.observable(item.price);
self.type = ko.observable(item.type);
self.icon = item.icon;
};
I would like to use ItemModel as a base class and just add the extra properties as necessary.
I think you can use ko.utils.extend like this
ko.utils.extend(self, new ItemModel(item));
inside the StandardItemModel
like this: http://jsfiddle.net/marceloandrader/bhEQ6/
I guess you can do something like this:
var StandardItemModel = function (item, cartItemTypes) {
var self = this;
self.standard = new ItemModel(item);
self.isInCart = ko.computed(function () {
return cartItemTypes().indexOf(item.type) > -1;
}, self);
self.itemClass = ko.computed(function () {
return self.isInCart() ? "icon-check" : "icon-check-empty";
}, self);
}
You can chain constructor calls using .call or .apply
function ItemModel (item) {
var self = this;
self.order = ko.observable(item.order);
self.title = ko.observable(item.title);
self.price = ko.observable(item.price);
self.type = ko.observable(item.type);
}
function StandardItemModel(item, cartItemTypes) {
var self = this;
ItemModel.call(this, item);
self.isInCart = ko.computed(function () {
return cartItemTypes().indexOf(item.type) > -1;
}, self);
self.itemClass = ko.computed(function () {
return self.isInCart() ? "icon-check" : "icon-check-empty";
}, self);
}
function CustomItemModel (item) {
var self = this;
ItemModel.apply(this, [item]);
self.icon = item.icon;
}
The advantage over ko.utils.extend (or similar methods from jQuery, underscore, etc) is that you are not creating an additional object just to grab references to its methods.
function MyBaseType() {
var self = this;
self.Id = 1
}
function MyComplexType() {
var self = this;
//Extending this class from MyBaseType
ko.utils.extend(self, new MyBaseType());
self.Name = 'Faisal';
self.MyComplexSubType = new MyComplexSubType();
}
function MyComplexSubType() {
var self = this;
self.Age = 26;
}
JSFIDDLE EXAMPLE
I've done something similar, with a lot of trial and error, but I got this to work for me:
var StandardItemModel = function (item, cartItemTypes) {
var self = this;
ItemModel.call(self, item)
}
You then need to add a prototyped constructor:
StandardModel.prototype = new ItemModel();
If you want to have common methods, then you need to add them to the base classes using prototype to add them, then call them in the higher class using:
ItemModel.prototype.methodName.call(self, parameters);