I want to write a computed so I can choose between text input and text from a select drop down, but somewhere down the route I´m stuck.
I have an '' collecting my text string
From a '' I can choose among 'Projects'.
Then I have a checkbox which decides whether the text from the selected project should override my input-textstring.
If the is empty. the selected project.title should set it.
This is my code:
HTML:
<input data-bind="textInput: $root.toDo" placeholder="What to do?" /><br/><br/>
<select data-bind="options: $root.Projects, optionsCaption: '< choose project >', optionsText: 'title', value: $root.selected"></select><br/>
<input id="useProjectTitle" type="checkbox" value="toDoUseProjectTitle" data-bind="checked: $root.toDoUseProjectTitle" />
<label for="useProjectTitle">Use project title as action</label>
<div data-bind="with: $root.toDo">
<label>I prefer:</label>
<ul >
<li >
Project: <span data-bind="text: $root.toDoProjectAction"></span><br/> <!-- Project title-->
To do: <span data-bind="text: $root.toDo"></span> <!-- toDo -->
</li>
</ul>
</div>
And my javascript:
Project = function(data){
var self = this;
self.id = data.id;
self.title = ko.observable(data.title);
};
var viewModel = function () {
var self = this;
self.Projects = ko.observableArray();
// data
self.Projects.push(new Project({
id: 1,
title: 'Friday night live'
}));
self.Projects.push(new Project({
id: 2,
title: 'Saturday morning gym'
}));
self.selected = ko.observable();
self.toDoUseProjectTitle = ko.observable(false);
self.toDoProjectAction = ko.computed(function () {
var title;
var project = self.selected();
if (project) {
title = project.title();
}
return title;
});
self.toDo = ko.computed({
read: function (value) {
if (self.selected()) { // not 'undefined' or null
if (self.toDoUseProjectTitle() || value === null) {
value = self.selected().title();
}
}
return value;
},
write: function (value) {
return value;
},
owner: self
});
};
ko.applyBindings(new viewModel());
Fiddle: http://jsfiddle.net/AsleG/srwr37k0/
Where do I go wrong?
I'm not sure I fully understand your desired behavior, but I have modified your Fiddle to use an extra variable and to correct your writable computed. It could be rearranged to work without a writable, but I didn't. :)
self.handEntered = ko.observable('');
self.toDo = ko.computed({
read: function () {
var value = self.handEntered();
if (self.selected()) { // not 'undefined' or null
if (self.toDoUseProjectTitle() || value === null) {
value = self.selected().title();
}
}
return value;
},
write: function (value) {
self.handEntered(value);
},
owner: self
});
http://jsfiddle.net/srwr37k0/14/
Related
In the following code, a product (represented with productVM) has an observable property (productName) containing its name in two languages (english and french).
Once a cartItem is added, and a product is selected, I want its displayed name to be updated when the button "change language" is clicked (e.g., if "Door" is selected, and "change language" is then clicked, the displayed name should be the french version (which is simply the english word plus a french-ish suffix "eux")).
But it doesn't work: The options do change, but the selected option is changed to the caption option.
What needs to be changed/added to fix it?
var handlerVM = function () {
var self = this;
self.cartItems = ko.observableArray([]);
self.availableProducts = ko.observableArray([]);
self.language = ko.observable();
self.init = function () {
self.initProducts();
self.language("english");
}
self.initProducts = function () {
self.availableProducts.push(
new productVM("Shelf", ['White', 'Brown']),
new productVM("Door", ['Green', 'Blue', 'Pink']),
new productVM("Window", ['Red', 'Orange'])
);
}
self.getProducts = function () {
return self.availableProducts;
}
self.getProductName = function (product) {
if (product != undefined) {
return self.language() == "english" ?
product.productName().english : product.productName().french;
}
}
self.getProductColours = function (selectedProductName) {
selectedProductName = selectedProductName();
// if not caption
if (selectedProductName) {
var matched = ko.utils.arrayFirst(self.availableProducts(), function (product) {
return (self.language() == "english" ? product.productName().english : product.productName().french) == selectedProductName;
});
return matched.availableColours;
}
}
self.addCartItem = function (a, b, c, d) {
self.cartItems.push(new cartItemVM());
}
self.changeLanguage = function () {
self.language() == "english" ?
self.language("french") :
self.language("english");
}
}
self.productVM = function (name, availableColours) {
var self = this;
self.productName = ko.observable({
english: name,
french: name + "eux",
});
self.availableColours = ko.observableArray(availableColours);
}
self.cartItemVM = function () {
var self = this;
self.cartItemName = ko.observable();
self.cartItemColour = ko.observable();
}
var handler = new handlerVM();
handler.init();
ko.applyBindings(handler);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>
<div data-bind="foreach: cartItems">
<div>
<select data-bind="options: $parent.getProducts(),
optionsText: function (item) { return $parent.getProductName(item); },
optionsValue: function (item) { return $parent.getProductName(item); },
optionsCaption: 'Choose a product',
value: cartItemName"
>
</select>
</div>
<div>
<select data-bind="options: $parent.getProductColours(cartItemName),
optionsText: $data,
optionsCaption: 'Choose a colour',
value: cartItemColour,
visible: cartItemName() != undefined"
>
</select>
</div>
</div>
<div>
<button data-bind="text: 'add cart item', click: addCartItem" />
<button data-bind="text: 'change language', click: changeLanguage" />
</div>
</div>
Your problem occurs when you change the options of your select. During the change, your value bound observable, cartItemName, contains the English string. For example: Door. As soon as you change the language, there is not a single option that returns Door for its optionsValue expression, thereby clearing the value altogether.
The best solution is to store a reference to your actual viewmodel, rather than just its string name. This does require you to move some other bits & pieces around, since you're manually updating quite a bit.
The starting point of the change:
// Remove
self.cartItemName = ko.observable();
// Add
self.cartItem = ko.observable();
// Change
<select data-bind="...
value: cartItem
" />
In a working snippet, with some other changes to make my work easier:
var handlerVM = function () {
var self = this;
self.cartItems = ko.observableArray([]);
self.language = ko.observable("english");
self.availableProducts = ko.observableArray([
new productVM("Shelf", ['White', 'Brown']),
new productVM("Door", ['Green', 'Blue', 'Pink']),
new productVM("Window", ['Red', 'Orange'])
]);
self.productNameFor = function(product) {
return product.productName()[self.language()];
};
self.addCartItem = function (a, b, c, d) {
self.cartItems.push(new cartItemVM());
}
self.changeLanguage = function () {
self.language() == "english" ?
self.language("french") :
self.language("english");
}
}
self.productVM = function (name, availableColours) {
var self = this;
self.productName = ko.observable({
english: name,
french: name + "eux",
});
self.availableColours = ko.observableArray(availableColours);
}
self.cartItemVM = function () {
var self = this;
self.cartItem = ko.observable();
self.cartItemColour = ko.observable();
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>
<div data-bind="foreach: cartItems">
<div>
<select data-bind="options: $root.availableProducts,
optionsText: $root.productNameFor,
optionsCaption: 'Choose a product',
value: cartItem"
>
</select>
</div>
<div data-bind="with: cartItem">
<select data-bind="options: availableColours,
optionsCaption: 'Choose a colour',
value: $parent.cartItemColour"
>
</select>
</div>
</div>
<div>
<button data-bind="text: 'add cart item', click: addCartItem" />
<button data-bind="text: 'change language', click: changeLanguage" />
</div>
</div>
I have a json that returns this:
[
{"home": [
{"name":"Federico","surname":"","et":"20","citt":"Milano"},
{"name":"Alberto","surname":"","et":"30","citt":"Milano"},
{"name":"Mirko","surname":"","et":"30","citt":"Roma"},
{"name":"Andrea","surname":"","et":"28","citt":"Firenze"}
]},
{"home": [
{"name":"Brad Pitt"},
{"name":"Tom Cruise"},
{"name":"Leonardo DiCaprio"},
{"name":"Johnny Depp"}
]},
{"home": [
{"name":"","surname":""},
{"name":"","surname":""},
{"name":"","surname":""},
{"name":"","surname":""}
]}
]
When there is a valid value provided for name, for example, I would like to change the background-color of the input box to white. But if the provided value is invalid, I would like to change the background-color back to red.
HTML:
<div class="context">
<div data-bind="foreach: personList">
<button data-bind="text: name,click: $root.getInfoPersona($index()), attr: {'id': 'myprefix_' + $index()}"/>
<button data-bind="text: $index,enable: false"></button>
</div>
<form>
<label>Name: </label>
<input id="idname" data-bind="value: name, css: { changed: name.isDirty(), notchanged : !name.isDirty() }" />
<label>Surname: </label>
<input id="idsurname" data-bind="value: surname, css: { changed: surname.isDirty }" />
<label>Years: </label>
<input id="idyears" data-bind="value: years, css: { changed: years.isDirty }" />
<label>Country: </label>
<input id="idcountry" data-bind="value: country, css: { changed: country.isDirty }" />
<button data-bind="click: save">Save Data</button>
<button data-bind="click: clear">Clear</button>
</form>
</div>
Javascript:
$(document).ready(function(){
ko.subscribable.fn.trackDirtyFlag = function() {
var original = this();
this.isDirty = ko.computed(function() {
return this() !== original;
}, this);
return this;
};
var ViewModel = function() {
var self=this;
var pi= (function(){
var json = null;
$.ajax({
'async': false,
'global': false,
'url': 'persona.json',
'dataType': 'json',
'success': function(data){
json=data;
}
});
return json;
})();
var questionsPerson= pi;
console.log(questionsPerson);
self.personList = ko.observableArray(questionsPerson[0].home);
var n=pi[0].home[0].name;
var c=pi[0].home[0].surname;
var e=pi[0].home[0].et;;
var ci=pi[0].home[0].citt;
self.name = ko.observable(n).trackDirtyFlag();
self.surname = ko.observable(c).trackDirtyFlag();
self.years = ko.observable(e).trackDirtyFlag();
self.country = ko.observable(ci).trackDirtyFlag();
self.save = function() {
alert("Sending changes to server: " + ko.toJSON(self.name));
alert("Sending changes to server: " + ko.toJSON(this));
};
self.clear = function(){
self.name("");
self.surname("");
self.years("");
self.country("");
};
self.getInfoPersona = function(indice){
var i=indice;
var ris= pi;
var n=ris[0].home[indice].name;
var c=ris[0].home[indice].surname;
var e=ris[0].home[indice].et;
var ci=ris[0].home[indice].citt;
self.name(n);
self.surname(c);
self.years(e);
self.country(ci);
self.getinfoPersona = ko.computed( function(){
return self.name() + " " + self.surname() + " " + self.years() + " " + self.country();
});
};
};
ko.applyBindings(new ViewModel());
});
First screenshot: the desired effect.
Second screenshot: the wrong effect.
The effect displayed on the second screenshot happens when I click on the second name to change person. The input box becomes "invalid" with background-color=red instead of background-color=white.
The quickest way to get it working is to modify your trackDirtyFlag extension:
ko.subscribable.fn.trackDirtyFlag = function() {
var original = ko.observable(this()); // make original observable
this.isDirty = ko.computed(function() {
return this() !== original(); // compare actual and original values
}, this);
// this function will reset 'dirty' state by updating original value
this.resetDirtyFlag = function(){ original(this()); };
return this;
};
...and call resetDirtyFlag after you reassigned values for editing:
self.name(n); self.name.resetDirtyFlag();
self.surname(c); self.surname.resetDirtyFlag();
self.years(e); self.years.resetDirtyFlag();
self.country(ci); self.country.resetDirtyFlag();
Look at the fiddle to see how it works.
However in general your approach is pretty far from optimal. Maybe this article will be useful for you.
I continue learning knockout and continue facing weird issues I don't know how to overcome.
I have the following html page and js script:
HTML:
<div data-bind="debug: $data, foreach: objects">
<span hidden="hidden" data-bind="value: type.id"></span>
<input type="text" data-bind="value: type.title" />
<button type="button" data-bind="click: $parent.removeObject">- </button>
</div>
<div class="control-group form-inline">
<select data-bind="options: availableTypes, optionsValue: function(item) {return item;},
optionsText: function(item) {return item.title;}, value: itemToAdd.type,
optionsCaption: 'Select types...'"></select>
<button type="button" data-bind="click: addObject">+</button>
</div>
</div>
JS:
function model() {
var self = this;
var types = [new Type("1"), new Type("2"), new Type("3")];
var objects = [new Object("1")];
self.objects = ko.observableArray(objects);
self.usedTypes = ko.computed(function() {
return types.filter(function(type) {
for (var j = 0; j < self.objects().length; j++) {
if (self.objects()[j].type.id === type.id) {
return true;
}
}
return false;
});
}, self);
self.availableTypes = ko.computed(function() {
return types.filter(function(type) {
for (var j = 0; j < self.usedTypes().length; j++) {
if (self.usedTypes()[j].id === type.id) {
return false;
}
}
return true;
});
}, self);
self.itemToAdd = new Object();
self.addObject = function() {
self.objects.push(self.itemToAdd);
self.itemToAdd = new Object();
};
self.removeObject = function(object) {
self.objects.remove(object);
};
};
function Object(type) {
var self = this;
self.type = new Type(type);
}
function Type(id) {
var self = this;
self.id = id;
self.title = id;
}
ko.applyBindings(new model());
I simplified model to show the error. The thing is that knockout claims it is illegal to call do this:
<span hidden="hidden" data-bind="value: type.id"></span>
Because it can't find property id in context. As far as I can see it is there and everything ok with it.
Could, please, anybody point me at my mistakes?
p.s. Here is a JsFiddle
ADDITION
Thanks to #Daryl's help I was able to localize the issue. If I replace
self.itemToAdd = new Object();
self.addObject = function() {
self.objects.push(self.itemToAdd);
self.itemToAdd = new Object();
};
with:
self.itemToAdd = new Object();
self.addObject = function() {
self.objects.push(new Object(1));
self.itemToAdd = new Object();
};
though, the following code still doesn't work:
self.itemToAdd = new Object("1");
self.addObject = function() {
self.objects.push(self.itemToAdd);
self.itemToAdd = new Object();
};
It seems itemToAdd objects is populated incorrectly from html elements it's binded to. But I still don't know what exactly is wrong.
You've allowed your type dropdown to be unset. When knockout shows the caption, it clears the actual value. This means that, by rendering the UI, your itemToAdd.type is cleared.
Your second approach solves this by not using the data-bound instance.
Furthermore:
I wouldn't overwrite the Object constructor if I were you... Find a different name.
Make sure your itemToAdd has observable properties if you want to do two-way binding to the UI.
in my MVC application im creating multiple Dropdowns by following:
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
optionsValue: 'id',
value: selectedItem(1),
event: { change: selectionChange }"></select>
the findgroup(x) and the selectedItem(x) are global functions in my ViewModel while those are for all the dropdowns the same.
the selectedItem(x) should return the currently selected Option of the dropdown. selectedItem(x) is a function to return a computed knockout observable.
Now im facing the Problem that the selectionChange Event is fired twice. See this fiddle for an example: http://jsfiddle.net/LGveR/20/
In this example, if you Change the value of the Dropdown box, you can see that the selectionCahnge Event is fired twice.
When i leave the value: selectedItem(x) out (and thus no computed function in the code) it doesnt: see: http://jsfiddle.net/LGveR/21/
I think that second time the Event is being fired Comes from the fact that in the computed function selectedItem(x) the observable
grp.selectedItem(grp.findItemByValue(value));
is setted.
How to prevent that the Setting of this observable leads to a "Change" Event ?
TIA,
Paul
HTML:
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
optionsValue: 'id',
value: selectedItem(1),
event: { change: selectionChange }"></select> <span data-bind="text: 'aantal: ' + findGroup(1).items().length"></span>
<br /> <span data-bind="text: 'Group Selected Country: ' + findGroup(1).selectedItem().country"></span>
<br /> <span data-bind="text: 'Computed Selected Country: ' + selectedItem(1)().country"></span>
<br /> <span data-bind="text: 'after select: ' + counter()"></span>
<br />
Javascript:
var group = function (id) {
this.id = id;
this.items = ko.observableArray() || {};
this.selectedItem = ko.observable();
this.addItem = function (data) {
this.items.push(data);
};
this.findItemByValue = function (id) {
return ko.utils.arrayFirst(this.items(), function (item) {
return item.id === id;
});
}
};
var grpItem = function (id, country) {
this.id = id;
this.country = country;
};
var ViewModel = function () {
this.groups = ko.observableArray() || {};
this.counter = ko.observable(0);
this.selectionChange = function (data, event, selector, item) {
this.counter(this.counter() + 1);
};
this.addGrp = function (data) {
this.groups.push(data);
};
this.findGroup = function (groupId) {
var ret = ko.utils.arrayFirst(this.groups(), function (c) {
return c.id === groupId;
});
return ret;
};
this.selectedItem = function (groupId) {
var grp = this.findGroup(groupId);
return ko.computed({
read: function () {
return this.findGroup(groupId).selectedItem();
},
write: function (value) {
grp.selectedItem(grp.findItemByValue(value));
}
}, this);
};
};
var vm = new ViewModel();
var p = new group(1);
var a = new grpItem(1, 'holland');
var b = new grpItem(2, 'germany');
var c = new grpItem(3, 'brasil');
p.addItem(a);
p.addItem(b);
p.addItem(c);
vm.addGrp(p);
ko.applyBindings(vm);
You're doing a couple odd things in your code which results in the computed being recomputed a bunch of times. Basically, you're setting the computed value by setting an observable with a function that relies on that observable, which recomputes your computed (or something crazy like that, see http://jsfiddle.net/LGveR/25/ to see how many times read and write are being called). There are a couple simple ways you can simplify and remove this issue:
Remove the optionsValue from your select data-bind. This will set
the value to the entire item in the observable array (instead of
just the id). You can then simplify the computed write function.
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
value: selectedItem(1),
event: { change: selectionChange }"></select>
and
this.selectedItem = function (groupId) {
var grp = this.findGroup(groupId);
return ko.computed({
read: function () {
return grp.selectedItem();
},
write: function (value) {
grp.selectedItem(value);
}
}, this);
};
see http://jsfiddle.net/LGveR/23/
Alternatively, you could remove the selectedItem on the viewmodel
entirely, and remove the optionsValue (as in #1). Then, you only need the group observable with the following html:
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
value: findGroup(1).selectedItem,
event: { change: selectionChange }"></select>
<span data-bind="text: 'aantal: ' + findGroup(1).items().length"></span>
<br />
<span data-bind="text: 'Group Selected Country: ' + findGroup(1).selectedItem().country"></span>
...
See http://jsfiddle.net/LGveR/24/
I have an array within an array, for example I have the following objects:
{ruleGroups: [{
rules: [{
dataField1:ko.observable()
,operator:ko.observable()
,dataField2:ko.observable()
,boolean:ko.observable()
,duration:ko.observable()
}]
}]
};
How can I edit the array within the array?
I was able to improve the issue but still have problems with adding row when adding group, the new group works but the old groups run dead:
A working example is found here (http://jsfiddle.net/abarbaneld/UaKQn/41/)
Javascript:
var dataFields = function() {
var fields = [];
fields.push("datafield1");
fields.push("datafield2");
return fields;
};
var operators = function() {
var operator = [];
operator.push("Plus");
operator.push("Minus");
operator.push("Times");
operator.push("Divided By");
return operator;
};
var booleanOperators = function() {
var operator = [];
operator.push("Equal");
operator.push("Not Equal");
operator.push("Greater Than");
operator.push("Less Than");
operator.push("Contains");
operator.push("Etc...");
return operator;
};
var ruleObj = function () {
return {
dataField1:ko.observable()
,operator:ko.observable()
,dataField2:ko.observable()
,boolean:ko.observable()
,duration:ko.observable()
}
};
var ruleGroup = function() {
return rg = {
rules: ko.observableArray([new ruleObj()]),
addRow: function() {
rg.rules.push(new ruleObj());
console.log('Click Add Row', rg.rules);
},
removeRow : function() {
if(rg.rules().length > 1){
rg.rules.remove(this);
}
}
}
};
var ViewModel = function() {
var self = this;
self.datafields = ko.observableArray(dataFields());
self.operators = ko.observableArray(operators());
self.booleanOperators = ko.observableArray(booleanOperators());
self.groupRules = ko.observableArray([new ruleGroup()]);
self.addGroup = function() {
self.groupRules.push(new ruleGroup());
};
self.removeGroup = function() {
if(self.groupRules().length > 1){
self.groupRules.remove(this);
}
};
self.save = function() {
console.log('Saving Object', ko.toJS(self.groupRules));
};
};
ko.applyBindings(new ViewModel());
HTML
<div data-bind="foreach: { data: groupRules, as: 'groupRule' }" style="padding:10px;">
<div>
<div data-bind="foreach: { data: rules, as: 'rule' }" style="padding:10px;">
<div>
<select data-bind="options: $root.datafields(), value: rule.dataField1, optionsCaption: 'Choose...'"></select>
<select data-bind="options: $root.operators(), value: rule.operator, optionsCaption: 'Choose...'"></select>
<select data-bind="options: $root.datafields(), value: rule.dataField2, optionsCaption: 'Choose...',visible: operator"></select>
<select data-bind="options: $root.booleanOperators(), value: rule.boolean, optionsCaption: 'Choose...'"></select>
<input data-bind="value: rule.duration" />
<span data-bind="click: groupRule.addRow">Add</span>
<span data-bind="click: groupRule.removeRow">Remove</span>
</div>
</div>
<span data-bind="click: $parent.addGroup">[Add Group] </span>
<span data-bind="click: $parent.removeGroup">[Remove Group]</span>
</div>
</div>
<div>
<span data-bind="click:save">[Save]</span>
</div>
I was able to fix the issue by rearranging the function of ruleGroup to:
var ruleGroup = function() {
var rg = {
rules: ko.observableArray([new ruleObj()]),
addRow: function() {
rg.rules.push(new ruleObj());
console.log('Click Add Row', rg);
},
removeRow : function() {
if(rg.rules().length > 1){
rg.rules.remove(this);
}
}
}
return rg;
};
Not exactly sure why this made a difference but I think its due to now a new var is being created and referenced.
Working JSFiddle is found here http://jsfiddle.net/abarbaneld/UaKQn/