I have recently started playing with Knockout and I have hit a problem. I have tried Googling this in all sort of ways but I couldn't find any applicable results.
Let's say that I have this model:
var model = new function () {
var that = this;
this.parameterRegex = ko.observable(/\##{1}\w+/ig);
this.query = ko.observable('SELECT ##par1 from ##par2');
this.parameterNames = ko.computed(function () {
var allParameters = that.query().match(that.parameterRegex());
return (allParameters == undefined) ? [] : jQuery.unique(allParameters);
});
this.parameters = ko.computed(function () {
return ko.utils.arrayMap(that.parameterNames(), function (item) {
return {
Name: ko.observable(item),
Example: ko.observable()
}
});
});
};
In the HTML I am binding with the Parameters computed observable, but every time the Query observable changes and the Parameters observable recomputes, I lose all the state of the items in that computed.
What I mean by this is that if I bind a foreach in HTML with Parameters and I have some input boxes in that foreach, such as:
<textarea name="query" class="form-control" data-bind="value: query, valueUpdate:'afterkeydown'" rows="10" style="margin-bottom:20px"></textarea>
<div data-bind="foreach: parameters">
<p data-bind="text: Name"></p>
<input type="text"></input>
</div>
Any text that the user has typed in the input will be lost once the Computed Observeable is recalculated.
How would I go about solving this?
The solution is to keep a separate array with the objects in them and then re-use the objects if they exist in the array instead of re-creating them each time.
var parameters = [];
this.parameters = ko.computed(function () {
var newParams = [];
for (var i = 0; i < that.parameterNames().length; i++) {
var name = that.parameterNames()[i];
var result = $.grep(parameters, function(p){ return p.Name() == name; });
var param;
if (result.length === 0) {
param = {
Name: ko.observable(name),
Example: ko.observable()
};
}
else {
param = result[0];
}
newParams.push(param);
}
parameters = newParams;
return newParams;
});
jsfiddle
Related
Im struggling to find a way to get the properties Override & Justification available outside of the function. The code is:
self.CasOverridesViewModel = ko.observable(self.CasOverridesViewModel);
var hasOverrides = typeof self.CasOverridesViewModel === typeof(Function);
if (hasOverrides) {
self.setupOverrides = function() {
var extendViewModel = function(obj, extend) {
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
extend(obj[property]);
}
}
};
extendViewModel(self.CasOverridesViewModel(), function(item) {
item.isOverrideFilledIn = ko.computed( function() {
var result = false;
if (!!item.Override()) {
result = true;
}
return result;
});
if (item) {
item.isJustificationMissing = ko.computed(function() {
var override = item.Override();
var result = false;
if (!!override) {
result = !item.hasAtleastNineWords();
}
return result;
});
item.hasAtleastNineWords = ko.computed(function() {
var justification = item.Justification(),
moreThanNineWords = false;
if (justification != null) {
moreThanNineWords = justification.trim().split(/\s+/).length > 9;
}
return moreThanNineWords;
});
item.isValid = ko.computed(function() {
return (!item.isJustificationMissing());
});
}
});
}();
}
I've tried it by setting up a global variable like:
var item;
or
var obj;
if(hasOverrides) {...
So the thing that gets me the most that im not able to grasp how the connection is made
between the underlying model CasOverridesviewModel. As i assumed that self.CasOverridesViewModel.Override() would be able to fetch the data that is written on the screen.
Another try i did was var override = ko.observable(self.CasOverridesViewModel.Override()), which led to js typeError as you cannot read from an undefined object.
So if anyone is able to give me some guidance on how to get the fields from an input field available outside of this function. It would be deeply appreciated.
If I need to clarify some aspects do not hesitate to ask.
The upmost gratitude!
not sure how far outside you wanted to go with your variable but if you just define your global var at root level but only add to it at the moment your inner variable gets a value, you won't get the error of setting undefined.
var root = {
override: ko.observable()
};
root.override.subscribe((val) => console.log(val));
var ViewModel = function () {
var self = this;
self.override = ko.observable();
self.override.subscribe((val) => root.override(val));
self.load = function () {
self.override(true);
};
self.load();
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
I've got two ways that I'm filling in an observableArray, one for testing purposes and one for the way I intend on using this array.
The first way I'm defining these objects and pushing them in one at a time, the second way I'm reading a JSON stream and pushing them in with a loop.
Here's my code for this shuttle-menu I'm using.
var StateModel = function() {
var self = this;
// initialize containers
self.leftStateBox = ko.observableArray();
self.rightStateBox = ko.observableArray();
// selected ids
self.selectedLeftStateBox = ko.observableArray();
self.selectedRightStateBox = ko.observableArray();
self.moveLeft = function () {
var sel = self.selectedRightStateBox();
for (var i = 0; i < sel.length; i++) {
var selCat = sel[i];
var result = self.rightStateBox.remove(function (item) {
return item.id == selCat;
});
if (result && result.length > 0) {
self.leftStateBox.push(result[0]);
}
}
self.selectedRightStateBox.removeAll();
}
self.moveRight = function () {
var sel = self.selectedLeftStateBox();
for (var i = 0; i < sel.length; i++) {
var selCat = sel[i];
var result = self.leftStateBox.remove(function (item) {
return item.id == selCat;
});
if (result && result.length > 0) {
self.rightStateBox.push(result[0]);
}
}
self.selectedLeftStateBox.removeAll();
}
self.leftStateBox.push({
id: "CAA"
, name: 'State 1'
});
self.leftStateBox.push({
id: "VAA"
, name: 'State 2'
});
self.leftStateBox.push({
id: "BAA"
, name: 'State 3'
});
self.loadStates = function() {
var self = this;
$.getJSON("${baseAppUrl}/public/company/" + companyId + "/json/searchStates/list",
function (searchStatesData) {
var states = JSON.parse(searchStatesData).searchStates;
for(var i = 0; i < states.length; i++) {
self.leftStateBox.push(new State(states[i]));
}
});
};
self.loadStates();
}
var State = function (state) {
var self = this;
self.name = ko.observable(state.name);
self.id = ko.observable(state.id);
}
$(function () {
ko.applyBindings(new StateModel(), document.getElementById("statesBox"));
});
Here's my view section:
'<div id="statesBox">
<div>
Available States:
<select multiple='multiple' data-bind="options: leftStateBox, optionsText: 'name', optionsValue: 'id', selectedOptions: selectedLeftStateBox"></select>
</div>
<div>
<p><button data-bind="click: moveRight">Add Selected</button></p>
<p><button data-bind="click: moveLeft">Remove Selected</button></p>
</div>
<div>
Selected States:
<select multiple='multiple' data-bind="options: rightStateBox, optionsText: 'name', optionsValue: 'id', selectedOptions: selectedRightStateBox"></select>
</div>
<br /><br />
</div>'
When I try to shuttle things back and forth on the list it works for the three I manually entered in but it doesn't work for the ones imported through the JSON call. They all show up on the list though and seem to have the same information, I structured the manually created objects after how the JSON objects look. When I trace the JS function moveRight, the remove works for the manually created objects but fails on the imported ones. I'm not really sure what I'm doing wrong at this point, has anyone seen something like this?
I grabbed the shuttle menu code from this post
The items you're adding have an important difference.
self.leftStateBox.push({
id: "BAA"
, name: 'State 3'
});
...
var State = function (state) {
var self = this;
self.name = ko.observable(state.name);
self.id = ko.observable(state.id);
}
The first have non-observable property values and the second have observable property values. In general, you should only make things observable if they need to be so (are you ever going to want to change the name or id of an item?).
var State = function (state) {
var self = this;
self.name = state.name;
self.id = state.id;
}
If a property is always observable, you can just "unwrap" it directly: item.id(). If it may sometimes be observable, you can use ko.unwrap(item.id).
var result = self.rightStateBox.remove(function (item) {
return ko.unwrap(item.id) == selCat;
});
Manipulating arrays is very easy by using underscore js
you can easily remove an item in KO observable array by the following code.
self.rightStateBox(_.without(self.rightStateBox(), toRemove));
toRemove is the object to remove from the array.
The Scenario
I have multiple input fields. The fields are NOT allowed to be empty. If any field is empty, I want to show some sort of error message.
The issue
The issue I am dealing with is that I have an observable array populating some inputs through a knockout foreach for the view.
Everything loads, displays, and saves properly, however, my validation (which is a computed) is only called when the last element in the observable array changes and not when any of the other elements change.
I found This SO Question, but OP's issue here was that he/she did not have their value as an observable which is not my problem as my value is wrapped as an observable.
The Code
Here's a fiddle
Here's the code:
View
<div data-bind="with: itemsModel">
<label data-bind="text: validMessage">Totally valid</label>
<div data-bind="foreach: items">
<div>
<label>Item: </label>
<input type="text " data-bind="value: name " />
</div>
</div>
</div>
JS
function ItemModel(item) {
self = this;
self.item = item;
self.name = ko.observable(item.name);
self.isValid = ko.computed(function() {
return self.name() && self.name().length <= 256;
});
}
function ItemsModel(itemsModel) {
var self = this;
self.itemsModel = itemsModel;
self.items = ko.observableArray([
new ItemModel(itemsModel.items[0]),
new ItemModel(itemsModel.items[1]),
new ItemModel(itemsModel.items[2])
]);
// This is only getting called when the last element in self.items changes
self.isValid = ko.computed(function() {
var isValid = true;
for (i = 0; i < 3; i++) {
isValid = isValid && self.items()[i].isValid();
}
return isValid;
});
self.validMessage = ko.computed(function() {
if (self.isValid()) {
return "Totally Valid";
}
return "Totally NOT Valid";
});
}
function ViewModel(data) {
var self = this;
self.data = data;
self.itemsModel = ko.observable(new ItemsModel(data.itemsModel));
}
var modelData = {
itemsModel: {
items: [{
name: "Item One"
}, {
name: "Item Two"
}, {
name: "Item Three"
}]
}
};
ko.applyBindings(new ViewModel(modelData));
You're not declaring your first self locally, so it's global.
function ItemModel(item) {
self = this;
should be
function ItemModel(item) {
var self = this;
Knockout way to determine when a computed need to be updated is a little tricky, you need to executed at least once every observable to get them registered.
Try something like this.
self.isValid = ko.computed(function() {
var isValid = true;
for (i = 0; i < 3; i++) {
//if isValid is false second part will not executed
//isValid = isValid && self.items()[i].isValid();
isValid = self.items()[i].isValid() && idValid;
}
return isValid;
});
I'd have a similar case here https://stackoverflow.com/a/38131131/2233835
Hope it helps!
So I bind my Knockout template as follows:
First ajax, get data then I pass the data can call a function named bindKo:
function bindKo(data) {
var length = data.length;
var insertRecord = {};
if (length > 0) {
insertRecord = data[data.length - 1]; //last record is an empty PremlimViewModel for insert
insertRecord.Add = true;
data.splice(data.length - 1, 1); //remove that blank insert record
}
function prelims(data) {
var self = this;
var model = ko.mapping.fromJS(data, { copy: ["_destroy"] }, self);
self.BidPriceFormatted = ko.computed({
read: function () {
var bidPrice = this.BidPrice();
if (bidPrice) {
if (!isNaN(bidPrice)) {
var input = '<input type="text" value="' + bidPrice + '"/>';
return $(input).currency({ decimals: 0 }).val();
}
}
},
write: function (value) {
value = value.replace(/\D/g, '');
this.BidPrice(value);
},
owner: this
});
return model;
}
var mapping = {
create: function (options) {
return new prelims(options.data);
}
};
function viewModel(prelimData) {
var self = this;
self.prelims = ko.mapping.fromJS(prelimData, mapping);
self.remove = function (prelim) {
self.prelims.destroy(prelim);
};
self.addOption = function () {
var clone = jQuery.extend(true, {}, insertRecord);
self.prelims.push(ko.mapping.fromJS(clone));
};
}
ViewModel = new viewModel(data);
ko.applyBindings(ViewModel);
}
I have a template defined where you can add and remove records, and user does just that:
<script type="text/html" id="PrelimsTemplate">
<!--Template Goodness-->
</script>
Then, ajax call, records updated in datanbase, latest results returned and I do:
ko.mapping.fromJS(newestData, ViewModel)
But this does not work because my ViewModel is complex.
So I would just like to reBind the template entirely. Make is disappear and reappear with latest data.
Wrap your template in a container than you can hook onto with jQuery.
When you need to trash it use ko.cleanNode and jQuery .empty()
emptyTemplate: function(){
ko.cleanNode($('#template-container')[0]);
$('#template-container').empty();
}
Load your template back up
fillTemplate: function(){
$('#template-container').html('<div data-bind="template: {name:\'templateId\', data: $data}"></div>');
ko.applyBindings(data,$('#template-container')[0])
},
See my fiddle
First I'm new to using knockout.
I have bound array1 to my template now I would like change it to use array2 is this possible with knockout?
What I was messing with
var viewModel = function(){
var _this = this;
this.test = [{ name: 'Fruit4'}, {name: 'Vegetables'}];
this.categories = ko.observableArray(this.test);
this.changeItems = function()
{
this.test= [{ name: 'Fruit2'}, {name: 'Vegetables2'}];
categories = ko.observableArray(this.test);
}
};
ko.applyBindings(viewModel());
Create a computed observable that will return one of the two arrays based on your conditions whatever they would be and bind to it. Make sure that the conditions that decide which to choose are also observable so it will update properly.
function ViewModel(data) {
this.array1 = ko.observableArray(data.array1);
this.array2 = ko.observableArray(data.array2);
// change this value to true to use array2
this.chooseArray2 = ko.observable(false);
this.array = ko.computed(function () {
return this.chooseArray2()
? this.array2()
: this.array1();
}, this);
}
<div data-bind="foreach: array">
...
</div>
Of course the logic could be more complex than that. To be more manageable, I would make the condition observable computed as well and create the logic in there. The computed observable that returns the array wouldn't have to change much.
function ViewModel(data) {
this.array1 = ko.observableArray(data.array1);
this.array2 = ko.observableArray(data.array2);
// which to choose depends on a number of conditions
this.someCondition = ko.observable(false);
this.anotherCondition = ko.observable(true);
this.someNumber = ko.observable(132);
this.chooseArray2 = ko.computed(function () {
// some complex logic
if (this.someNumber() < 0) {
return this.someCondition();
}
return this.someCondition() || !this.anotherCondition();
}, this);
this.array = ko.computed(function () {
return this.chooseArray2()
? this.array2()
: this.array1();
}, this);
}