Knockout nested binding in a loop - javascript

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.

Related

textarea knockout attr binding for row attribute dont work

I want change height of texterea by knockout 2.3.0
I bind the value of the texterea to "FileData" observable field
and want the texterea rows attribute will changed to the count of rows in "FileData"
the value bind works fine but attr don't work
var self = this;
self.FileData = ko.observable("");
self.lineBreakCount = function(str) {
/* counts \n */
try {
return ((str.match(/[^\n]*\n[^\n]*/gi).length)) + 1;
} catch (e) {
return 0;
}
}
self.buttonClick = function () {
$.get(url, { })
.success(function (serverData) { self.FileData(serverData);})
}
<button type="button" data-bind="click: buttonClick">Click Me</button>
<textarea readonly="readonly" data-bind="value: FileData, attr: { 'rows': lineBreakCount(FileData)}"></textarea>
Your lineBreakCount expects a string, but you're passing it an observable that contains a string.
To fix this, unwrap your observable either in the binding (lineBreakCount(FileData())), or in the method (str().match)
var VM = function() {
var self = this;
self.FileData = ko.observable("");
self.lineBreakCount = function(str) {
/* counts \n */
try {
return ((str.match(/[^\n]*\n[^\n]*/gi).length)) + 1;
} catch (e) {
return 0;
}
}
self.buttonClick = function() {};
};
ko.applyBindings(new VM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button type="button" data-bind="click: buttonClick">Click Me</button>
<textarea data-bind="textInput: FileData, attr: { 'rows': lineBreakCount(FileData())}"></textarea>

Restoring default drop down value with angularjs

Although I have got this working, the way it works seems improper. I have a drop down list (DDL) that displays a list of teams. The top and default entry is "Select Team... ". Although my DDL is tied to a model, "Select Team..." shouldn't be part of it since "Select Team..." has no meaning to the domain model.
When a user clicks "Add New" the form clears and all DDLs should revert to their default values.
Here are the related controller functions:
scope.addUser = function() {
resetToNewUser();
$scope.profileVisible = true;
$scope.oneAtATime = true;
$scope.accordionStatus = { isFirstOpen: true, isFirstDisabled: false };
}
function resetToNewUser() {
$scope.selectedUser.NtId = "";
$scope.selectedUser.UserId = -1;
$scope.selectedUser.IsActive = true;
$scope.selectedUser.FirstName = "";
$scope.selectedUser.LastName = "";
$scope.selectedUser.JobTitle = "";
$scope.selectedUser.Email = "";
$scope.selectedUser.SecondaryEmail = "";
$scope.selectedUser.PhoneNumber = "";
for(var i = 0; i < $scope.roleList.length; i++) {
if($scope.roleList[i].RoleSystemName.trim() === "BLU") {
$scope.selectedUser.Role = $scope.roleList[i];
}
}
$scope.selectedUser.SupervisorId = null;
//HACK BELOW//
document.getElementById('selTeam').selectedIndex = 0; // <-- This works, but feels like a hack.
$scope.selectedUser.IsRep = false;
for(var i = 0; i < $scope.signingAuthorityList.length; i++) {
if($scope.signingAuthorityList[i].SigningAuthoritySystemName === "SME") {
$scope.selectedUser.SigningAuthority = $scope.signingAuthorityList[i];
}
}
$scope.selectedUser.IsOutOfOfficeEnabled = false;
$scope.selectedUser.OutOfOfficeStartDate = null;
$scope.selectedUser.OutOfOfficeEndDate = null;
$scope.selectedUser.OutOfOfficeAppointedRepId = null;
}
Here's how the DDL is defined in the template:
<div class="form-group">
<label for="" class="control-label col-sm-2 required">Team</label>
<div class="col-sm-10">
<select class="form-control" id="selTeam"
ng-model="selectedUser.Team"
ng-options="team as team.TeamName for team in teamList track by team.TeamId">
<option value="">Select Team...</option>
</select>
</div>
</div>
Is there a better way to do this?
You could always just remove the ability for the user to select your placeholder option, right? Something like this:
<option value="" disabled selected hidden>Select Team...</option>
You html part looks good, but I think on js side you make a lot of logic. What happens if there will be added new options on the server? Better get state of the new user from the backend, customize it with the select and other widgets and keep it before it will be submitted. On pseudo code it will be looks like
$scope.addUser = function() {
//create empty user on the scope
$scope.selectedUser = {};
//get the new user state from the backend
UserService.resetToNewUser($scope.selectedUser);
//setup view options
$scope.accordionStatus = {isFirstOpen: true, isFirstDisabled: false}
};
app.service('UserService', function(){
this.resetToNewUser = function(user){
$http({
method: 'GET',
url: '/default_user/'
}).then(function successCallback(response) {
user = response;
);
};
});

How to make double input writeable ko.computed

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/

Managing an Array within an Array using KnockoutKS

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/

ko.subscribe on child model properties

Looking for a good example of how to set up child models in knockoutjs. This includes binding to child events such as property updates which I haven't been able to get working yet.
Also, it would be better to bind to a single child in this case instead of an array but I don't know how to set it up in the html without the foreach template.
http://jsfiddle.net/mathewvance/mfYNq/
Thanks.
<div class="editor-row">
<label>Price</label>
<input name="Price" data-bind="value: price"/>
</div>
<div class="editor-row">
<label>Child</label>
<div data-bind="foreach: childObjects">
<div><input type="checkbox" data-bind="checked: yearRound" /> Year Round</div>
<div><input type="checkbox" data-bind="checked: fromNow" /> From Now</div>
<div>
<input data-bind="value: startDate" class="date-picker"/> to
<input data-bind="value: endDate" class="date-picker"/>
</div>
</div>
</div>
var ChildModel= function (yearRound, fromNow, startDate, endDate) {
var self = this;
this.yearRound = ko.observable(yearRound);
this.fromNow = ko.observable(fromNow);
this.startDate = ko.observable(startDate);
this.endDate = ko.observable(endDate);
this.yearRound.subscribe = function (val) {
alert('message from child model property subscribe\n\nwhy does this only happen once?');
//if(val){
// self.startDate('undefined');
// self.endDate('undefined');
//}
};
}
var ParentModel = function () {
var self = this;
this.price = ko.observable(1.99);
this.childObjects = ko.observableArray([ new ChildModel(true, false) ]);
};
var viewModel = new ParentModel ();
ko.applyBindings(viewModel);
Try it with the following:
this.yearRound.subscribe(function (val) {
alert('value change');
});
If you want to have the subscriber also being called while loading the page do something like this:
var ChildModel= function (yearRound, fromNow, startDate, endDate) {
var self = this;
this.yearRound = ko.observable();
this.fromNow = ko.observable(fromNow);
this.startDate = ko.observable(startDate);
this.endDate = ko.observable(endDate);
this.yearRound.subscribe(function (val) {
alert('value change');
});
this.yearRound(yearRound);
}
http://jsfiddle.net/azQxx/1/ - this works for me with Chrome 16 and Firefox 10
Every time the checked button changes its value the callback fires.
The observableArray is fine in my opinion if you may have more than one child model associated to the parent.

Categories