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.
Related
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!
I've been working a project that allows a user to manage Option Types and Options. Basically user can add a new Option Type, let's say they name it Color and then they add the options - Black, Red, Purple, etc. When the collection first loads up the existing records, an empty option should be added at the end
When a user starts typing in the text field, I want to add a new empty option , thereby always giving the user a new field to work with.
I have this almost working, but can't figure how to properly add new empty option to a new Option Type or to existing option types. The push method keeps crashing Plunkr. Any input is appreciated, short sample review of the plunkr is below
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.optionTypeId = 0;
$scope.productOptionId = 0;
$scope.productEditorModel = {
"ProductOptions": [0],
"OptionTypes": [0]
};
$scope.productEditorModel.optionTypeName = '';
$scope.addEmptyOption = function (optionTypeId) {
var emptyOption = { ProductOptionId: 3, ProductId: '1066', OptionTypeId: 1, OptionValue: '', Price: '', IsStocked: true };
console.log(emptyOption);
//$scope.productEditorModel.ProductOptions.push(emptyOption);
};
$scope.loadData = function () {
$scope.productEditorModel.OptionTypes = [{ OptionTypeId: 1, OptionName: 'Color' },{ OptionTypeId: 2, OptionName: 'Size' },];
$scope.productEditorModel.ProductOptions = [{ ProductOptionId: 1, ProductId: '1066', OptionTypeId: 2, OptionValue: 'Medium', Price: '', IsStocked: true, },{ ProductOptionId: 2, ProductId: '1066', OptionTypeId: 1, OptionValue: 'Black', Price: '', IsStocked: true }];
angular.forEach($scope.productEditorModel.ProductOptions, function (item) {
//console.log(item.OptionTypeId);
$scope.addEmptyOption(item.OptionTypeId);
});
};
$scope.loadData();
$scope.removeOption = function (option) {
var index = $scope.productEditorModel.ProductOptions.indexOf(option);
$scope.productEditorModel.ProductOptions.splice(index, 1);
};
$scope.filterEmptyElements = function (optionTypeId) {
$scope.emptyElements = $.grep($scope.productEditorModel.ProductOptions, function (k) { return k.OptionValue === "" || angular.isUndefined(k.OptionValue) && k.OptionTypeId == optionTypeId });
};
$scope.update = function (option, index) {
var optionTypeId = option.OptionTypeId;
$scope.filterEmptyElements(optionTypeId);
if (!angular.isUndefined(option.OptionValue) && $scope.emptyElements.length == 1 && option.OptionValue.length > 0) {
$scope.addOption(optionTypeId);
} else if (angular.isUndefined(option.OptionValue)) {
$scope.removeOption(option);
}
};
$scope.addOptionType = function () {
var optionTypeId = --$scope.optionTypeId;
var optionName = $scope.productEditorModel.optionTypeName;
var newOptionType = { OptionTypeId: optionTypeId, OptionName: optionName };
$scope.productEditorModel.OptionTypes.push(newOptionType);
$scope.addEmptyOption(optionTypeId);
};
$scope.editOptionType = function (optionType) {
$scope.editing = true;
};
$scope.saveOptionType = function (optionType) {
$scope.editing = false;
};
$scope.trackOptionTypesCount = function () {
if ($scope.productEditorModel.OptionTypes.length == 3) {
$scope.isMaxOptionTypes = true;
} else {
$scope.isMaxOptionTypes = false;
}
};
$scope.removeOptionType = function (optionType) {
var index = $scope.productEditorModel.OptionTypes.indexOf(optionType);
$scope.productEditorModel.OptionTypes.splice(index, 1);
$scope.trackOptionTypesCount();
};
});
See the plunker below:
http://plnkr.co/edit/YHLtSwQWVb2swhNVTQzU?p=info
The error you get that $ is not defined is because you haven't included jQuery. You don't need jQuery for this though, array.map should be able to perform the same functionality.
$scope.emptyElements = $scope.productEditorModel.ProductOptions.map(function (k) {
return k.OptionValue === "" || angular.isUndefined(k.OptionValue) && k.OptionTypeId == optionTypeId
});
And it crashes because inside $scope.loadData you have
angular.forEach($scope.productEditorModel.ProductOptions, function (item) {
$scope.addEmptyOption(item.OptionTypeId);
});
and then inside $scope.addEmptyOption you try to
$scope.productEditorModel.ProductOptions.push(emptyOption);
So the foreach will loop for each item in $scope.productEditorModel.ProductOptions, which you keep adding options to so....? Infinite loop.
Non-crashing version: http://plnkr.co/edit/5Sc2sWfhKBs9kLCk83f1?p=preview
What you really should do though is look over your data structure. Make the ProductOptions a sub-object of OptionTypes and just rename it Options. Remove ALL code about creating id's here in your GUI, that should be handled by the backend. Instead in the GUI there should be a Sortorder property on the Options (which also of course gets stored by the backend). Then when you store, the ones without an id get inserted, the ones with an id get updated. Much easier to handle everything that way.
I'd also break out optionTypes and options to their own services/providers. Much easier to keep track of what needs to be done. And each just basically contains add, remove and maybe a find/getJSON or something.
Here's a restructured version. Much easier to keep track of what belongs where. And it has more features than the original with less code. http://plnkr.co/edit/BHcu6vAfcpEYQpZKHc5G?p=preview
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
I have a question similar to Bind text to property of child object and I am having difficulty properly creating the KO observable for a child object.
For example, I do an HTTP Get to return a JSON array of People, and the People array is inside a property called "payload". I can get the basic binding to work, and do a foreach on the payload property, displaying properties of each Person; however, what I need to do is add a "status" property to each Person, which is received from a different JSON, example
/api/people (firstname, lastname, DOB, etc.)
/api/people/1/status (bp, weight, height, etc)
I have tried binding to status.bp, and status().bp, but no luck.
The js example:
var TestModel = function (data) {
var len = data.payload.length;
for (var i = 0; i < len; i++) {
var elem = data.payload[i];
var statusdata = $.getJSON("http://localhost:1234/api/people/" + elem.id + "/status.json", function (statusdata) {
elem.status = statusdata;
data.payload[i] = elem;
});
}
ko.mapping.fromJS(data, {}, this);
};
var people;
var data = $.getJSON("http://localhost:1234/api/people.json", function (data) {
people= new TestModel(data);
ko.applyBindings(people);
});
2 important things I will need:
1) properly notify KO that "payload" is an array to key on ID property
2) make "status" an observable
Help!
[UPDATE] EDIT with working fix based on Dan's answer:
var TestModel = function(data) {
...
this.refresh = function () {
$.getJSON("http://localhost:1234/api/people", function (data) {
self.payload = ko.observableArray(); // this was the trick that did it.
var len = data.payload.length;
for (var i = 0; i < len; i++) {
var elem = data.payload[i];
$.getJSON("http://localhost:1234/api/people/" + elem.id + "/status", function (statusdata) {
// statusdata is a complex object
elem.status = ko.mapping.fromJS(statusdata);
self.payload.push(elem);
});
}
// apply the binding only once, because Refresh will be called with SetInterval
if (applyBinding) {
applyBinding = false;
ko.applyBindings(self);
}
}
I am still new to Knockout and improvements to the refresh function are most welcome. The mapping is still being reapplied each time.
You need to define an observable array and then push your data into it.
elem.status = ko.observableArray();
for (var i = 0; i < statusdata.length; i++) {
elem.status.push(statusdata[i]);
}
I can't tell what the full structure of the data is by the example. But if status is a complex object, you may what to give it its own model.
for (var i = 0; i < statusdata.length; i++) {
elem.status.push(new statusModel(statusdata[i]));
}
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);
}