Dependent observables with two view models? - javascript

I am working with the wonderful Knockout.js library. I am using javascript classes to capture the structure. For example, one of several classes is:
function OverridableFormItemText(defaultId, defaultText, defaultHelpText, overrideId, overrideText, overrideHelpText)
{
this.DefaultFormItemTextId = ko.observable(defaultId);
this.DefaultText = ko.observable(defaultText);
this.DefaultHelpText = ko.observable(defaultHelpText);
this.OverrideFormItemTextId = ko.observable(overrideId);
this.OverrideText = ko.observable(overrideText);
this.OverrideHelpText = ko.observable(overrideHelpText);
}
If I have two view models in the page and want to add a dependent observable property to my class OverridableFormItemText, then do I need to do this twice due to the requirement to pass to view model to the function?
viewModel1.OverridableFormItemText.SomeDependentProperty = ko.dependentObservable(function() {
return this.DefaultText() + " " + this.OverrideText();
}, viewModel1);
viewModel2.OverridableFormItemText.SomeDependentProperty = ko.dependentObservable(function() {
return this.DefaultText() + " " + this.OverrideText();
}, viewModel2);

Yes, but you could make it more maintainable with the DRY principle, have a look at this example with the following view:
<p>First name: <span data-bind="text: viewModel2.firstName"></span></p>
<p>Last name: <span data-bind="text: viewModel2.lastName"></span></p>
<h2>Hello, <input data-bind="value: viewModel2.fullName "/>!</h2>
<p>First name: <span data-bind="text: viewModel.firstName"></span></p>
<p>Last name: <span data-bind="text: viewModel.lastName"></span></p>
<h2>Hello, <input data-bind="value: viewModel.myFullName "/>!</h2>
And this code:
var viewModel = {
firstName: ko.observable("Planet"),
lastName: ko.observable("Earth")
};
var viewModel2 = {
firstName: ko.observable("Exoplanet"),
lastName: ko.observable("Earth")
};
function FullNameDependentObservable(viewmodel, f, property) {
viewmodel[property] = ko.dependentObservable(f, viewmodel);
}
var AddNames = function() {
return this.firstName() + " " + this.lastName();
};
FullNameDependentObservable(viewModel, AddNames, "myFullName");
FullNameDependentObservable(viewModel2, AddNames, "fullName");
ko.applyBindings(viewModel);
ko.applyBindings(viewModel2);

OP here. Found that if you use classes as above, you can refer to 'this' when creating a dependent property, so this means I don't need to define the dependent property for each view model:
function OverridableFormItemText(defaultId, defaultText, defaultHelpText, overrideId, overrideText, overrideHelpText)
{
this.DefaultFormItemTextId = ko.observable(defaultId);
this.DefaultText = ko.observable(defaultText);
this.DefaultHelpText = ko.observable(defaultHelpText);
this.OverrideFormItemTextId = ko.observable(overrideId);
this.OverrideText = ko.observable(overrideText);
this.OverrideHelpText = ko.observable(overrideHelpText);
this.SomeDependentProperty = ko.dependentObservable(function() { return ('Dependent' + this.DefaultText() )}, this);
}

Related

Use function parameters as a variable call

what has already been described in the title, but basically I want to send a function perimeter and use it to call one of the three different variables. Also so it doesn't come to a miss understanding the "$('#'+id)" part of the code works all I need is the correct syntax for the "id =" part (if even possible). And I know there is a workaround but I am trying to minimize code and this seems like the most optimal solution.
my code:
<div class="one">
<p>ime:</p>
<input type="text" id="name">
<p>kraj:</p>
<input type="text" id="city">
<p>starost:</p>
<input type="text" id="age">
<p id="one_output"></p>
</div>
var name = "1";
var city = "2";
var age = "3";
function statement(id) {
id = $('#'+id+'').val();
$("#one_output").text("Sem " + name + " in živim v " + city + ". Star sem " + age);
};
$('.one input[type="text"]').keyup(function() {
switch($(this).attr("id")) {
case "name":
statement(the_id);
break;
case "city":
statement(the_id);
break;
case "age":
statement(the_id);
break;
}
});
ok, I think I finally understood what you're after
so you're passing a variable name and want to dynamically call it, instead of going the global way using this, I would recommend to do it by having all your vars in just one global one, for example
var formInputs = { name: '1', city: '2', age: '3' }
and then you can easily read/write them with formInputs[ var_name_here ]
so your example, would be written as
var formInputs = { name: '1', city: '2', age: '3' }
function statement(name, val) {
formInputs[name] = val
var txt = `Sem ${formInputs.name} in živim v ${formInputs.city}. Star sem ${formInputs.age}`
$("#one_output").text(txt)
}
$('.one input[type="text"]').keyup(function() {
var elm = $(this)
statement(elm.attr("id"), elm.val())
})
var formInputs = { name: '...', city: '...', age: '...' }
var statement = function(name, val) {
formInputs[name] = val // assign value to variable
var txt = `Sem <b>${formInputs.name}</b> in živim v <b>${formInputs.city}</b>. Star sem <b>${formInputs.age}</b>` // the new text
$("#one_output").html(txt) // output
}
$('.one input[type="text"]').keyup(function() {
var elm = $(this) // our key element
statement(elm.attr("id"), elm.val()) // pass id and why not the value, so we dont need the element again
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="one">
<p>ime: <input type="text" id="name"></p>
<p>kraj: <input type="text" id="city"></p>
<p>starost: <input type="text" id="age"></p>
<p id="one_output"></p>
</div>
If you want to minimize your code, you can start by doing this:
id = $(`#${id}`).val();
Instead of this:
id = $('#'+id+'').val();
Modify your keyup callback to a more cleaner code:
$('.one input[type="text"]').keyup(function() {
var myId = $(this).attr("id"));
var id = $('#'+myId+'').val();
$("#one_output").text("Sem " + name + " in živim v " + city + ". Star
sem " +
age);
});

Having trouble assigning values to an array using Javascript

OK, yeah, I know this is basic stuff, but it's got me by you know what.
Here's the code:
myApp.controller('keyExpController', function ($scope, KeyExpDataService) {
var ctrlExp = this;
ctrlExp.keyExp = [];
$scope.company = {};
ctrlExp.achKeys = {
company: [{
achieves: [],
details: {
super: "",
whyleft: ""
}
}]
};
$scope.keyachievements = [];
$scope.compName = null;
ctrlExp.fetchKeyExp = function () {
//Resume Data
KeyExpDataService.getKeyExpData().then(function (result) {
ctrlExp.keyExp = result.data.resume.proexperience;
console.log("Result: " + ctrlExp.keyExp);
$scope.groupBy(ctrlExp.keyExp.length);
});
};
ctrlExp.fetchKeyExp();
// I group the friends list on the given property.
$scope.groupBy = function (nbrComps) {
//Set the global value for number of companies
nbrCompanies = nbrComps;
var compValue = "_INVALID_GROUP_VALUE_";
for (var i = 0; i < nbrComps; i++) {
$scope.keyachievements = ctrlExp.keyExp[i].keyachievements;
if (ctrlExp.keyExp[i].companyat !== compValue) {
$scope.company = [{
achievements: [],
details: {
companyName: ctrlExp.keyExp[i].companyat,
super: ctrlExp.keyExp[i].supervisor,
whyleft: ctrlExp.keyExp[i].reasonforleaving
}
}];
compValue = $scope.company.companyName;
$scope.compName = compValue;
//It's HERE, with the first line that I continually get the following error:
//TypeError: Cannot read property 'achieves' of undefined
//at Scope.$scope.groupBy (controllers.js:151)
//This is line 151 just below:
achievements.company[i].achieves[i] = $scope.keyachievements;
achievements.company[i].details.super = ctrlExp.keyExp[i].supervisor;
achievements.company[i].details.whyleft = ctrlExp.keyExp[i].reasonforleaving;
ctrlExp.achKeys.company[i].achieves[i] = $scope.keyachievements;
ctrlExp.achKeys.company[i].details.super = ctrlExp.keyExp[i].supervisor;
ctrlExp.achKeys.company[i].details.whyLeft = ctrlExp.keyExp[i].reasonforleaving;
}
}
};
});
Now what I'm doing is fine until I hit this in the controller. My OBJECT looks like this broken out for brevity:
ctrlExp.achKeys = {
company: [{
achieves: [],
details: {
super: "",
whyleft: ""
}
}]
};
I cannot figure out, for the life of me, yeah, it's got to be because I'm over 50, why I cannot assign anything to the first part of the object:
THIS:
ctrlExp.achKeys.company[0].achieves[0] = $scope.keyAchievements
The $scope.keyAchievements hold all the bullets for a particular company. There could be "n" number of companies on a person's resume. Hence, I'm looping through the companies to get the name of the company and the "key achievements" that reside "under" that company for a person.
That's pretty much it.
I'll post the "service" but this site is a cut for my own site, not posted yet, that I'll be using as a template for future considerations. Once I get this, I'm golden.
Thanks everyone for your contributions to our craft.
It looks like it's trying to set a value to a variable which is as yet undefined, namely achievements.company[i]
So, if you added something like:
achievements.company[i]={};
before your line 151, it could work. Well, you wouldn't get the
TypeError: Cannot read property 'achieves' of undefined
message anymore.
In JavaScript, you can have undefined variables/objects, but you can't assign properties to them.
From the error message above, it looks like achievements is defined, whereas achievements.company[i] is not, hence the issue when you want to assign the achieves[i] property to it.
Had achievements been undefined, you would have got the message:
Cannot read property 'company' of undefined
What browser are you using to test this? I ask because I've run into some incompatibilities in the past with similar commands and so I just avoid them now.
Instead I would do a more compatible Array.push() changing:
ctrlExp.achKeys.company[0].achieves[0] = $scope.keyAchievements
to
ctrlExp.achKeys.company[0].achieves.push($scope.keyAchievements);
So, yeah, I figured it out RIGHT after I posted the question.
Here's how I did it:
myApp.controller('keyExpController', function ($scope, KeyExpDataService) {
var ctrlExp = this;
ctrlExp.keyExp = [];
$scope.company = {};
ctrlExp.achKeys = {
company: {
achieves: [],
details: []
}
};
$scope.keyachievements = [];
$scope.compName = null;
ctrlExp.fetchKeyExp = function () {
//Resume Data
KeyExpDataService.getKeyExpData().then(function (result) {
ctrlExp.keyExp = result.data.resume.proexperience;
console.log("Result: " + ctrlExp.keyExp);
$scope.groupBy(ctrlExp.keyExp.length);
});
};
ctrlExp.fetchKeyExp();
// I group the friends list on the given property.
$scope.groupBy = function (nbrComps) {
//Set the global value for number of companies
nbrCompanies = nbrComps;
var compValue = "_INVALID_GROUP_VALUE_";
for (var i = 0; i < nbrComps; i++) {
$scope.keyachievements = ctrlExp.keyExp[i].keyachievements;
if (ctrlExp.keyExp[i].companyat !== compValue) {
$scope.company = {
achievements: [$scope.keyachievements],
details: [{
companyName: ctrlExp.keyExp[i].companyat,
super: ctrlExp.keyExp[i].supervisor,
whyleft: ctrlExp.keyExp[i].reasonforleaving
}]
};
compValue = $scope.company.companyName;
$scope.compName = compValue;
achievements.company.achieves[i] = $scope.keyachievements;
details.company.hr[i] = [
{
super: ctrlExp.keyExp[i].supervisor,
whyleft: ctrlExp.keyExp[i].reasonforleaving
}
];
ctrlExp.achKeys.company.achieves[i] = $scope.keyachievements;
ctrlExp.achKeys.company.details[i] = [
{
super: ctrlExp.keyExp[i].supervisor,
whyleft: ctrlExp.keyExp[i].reasonforleaving
}
];
}
}
};
});
So up above all the controller calls I put these ARRAYS in a global scope:
var achievements = {
company: {
achieves: []
}
};
var details = {
company: {
hr: []
}
};
var edu1 = {
hsandcol: {
school: []
}
};
var edu2 = {
other: {
school: []
}
};
And here's an example service I simply copy and paste and rename as I need
myApp.factory('EduDataService', function ($http, URL) {
var getEduExpData = function () {
return $http.get(URL + 'resume.json')
.success(function (data) {
console.log("SUCCESS!");
//console.log("The Key Experiences array length: " + data.resume.proexperience.length);
return data.resume.education;
})
.error(function (e) {
console.log("He\'s dead Jim!", e);
return e;
});
};
return {
getEduExpData: getEduExpData
};
});
In the HTML file, I figured out how to isolate and embed controllers within controllers calling the parent controller to do everything while the child controller get all the data.
It's pretty cool.
Note: ctrlRes is the call from the PARENT CONTROLLER: resumeController way above. I'll post the final site completion in a wee bit. It's pretty awesome and the JSON object holds everything about a person's resume and it's a great template for anyone needing a core resume website with ANGULAR and Bootstrap exclusively. Actually, it's for me and I don't mind sharing but it'll expose my last name.
and the HTML snippet:
<div class="my-education">
<h3>My Education</h3>
<!-- Education Start -->
<div class="education" ng-controller="eduController">
<h4>High School and College</h4>
<div class="panel-group" id="accordionedu{{$index}}">
<div class="panel panel-default" ng-repeat="edu in ctrlRes.eduHsCol[0]">
<div class="panel-heading">
<h5 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapseEdu{{$index}}">
{{edu.name}} {{edu.type}}
<span class="pull-right">[{{edu.start}} - {{edu.end}}]</span>
</a>
</h5>
</div>
<div id="collapseEdu{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<div>
<h5>
<span class="pull-left">
<strong>Location:</strong>
{{edu.locale}}
</span>
<span class="pull-right">
<strong>Major:</strong>
{{edu.maj}}
</span>
</h5>
<br><br>
<span class="pull-left">
<strong>Graduated:</strong>
{{edu.grad}}
<strong>GPA:</strong>
{{edu.gpa}}
</span>
<span class="pull-right">
<strong>Minor:</strong>
{{edu.min}}
</span>
<br>
<hr/>
<p>
<strong><em>Comments: </em></strong>
</p>
<div>
{{edu.comments}}
</div>
<br>
<div ng-show="edu.type === 'College'">
<a href="documents/Academic Transcript.pdf" target="_blank">
Download Academic Transcript - PASSWORD PROTECTED
</a>
</div>
</div>
</div>
</div>
</div>
<pre hidden="hidden">{{ctrlRes.eduHsCol|json}}</pre>
</div>
<!-- Education 1 End -->
</div>
<pre hidden="hidden">{{ctrlRes.eduHsCol|json}}</pre>
<pre hidden="hidden">{{ctrlRes.eduOther|json}}</pre>
</div>

How to bind Knockout viewModel for only available keys?

I am creating knockout data-bind properties dynamically using .cshtml in MVC. I want to bind only those properties which are available in viewModel which again I am creating dynamically from the result of restful WCF.
So there may be or may be not some keys available in the viewModel for which
e.g.: <span data-bind="text: cli"></span> is created.
But when I bind the viewModel, I get an error along the lines of "'cli' property not found in viewModel". However, I wanted to bind that property only if that key is there in viewModel in the first place.
$(document).ready(function () {
debugger;
$.ajax({
cache: false,
type: "GET",
async: false,
dataType: "json",
url: requestURL,
success: function (data) {
debugger;
if (data.GetCircuitCheckStatusResponse.Status.HasErrors == false) {
networkData = data.GetCircuitCheckStatusResponse.Response.RunningStatus.networkData;
diagnosticData = data.GetCircuitCheckStatusResponse.Response.RunningStatus.diagnosticData;
diagnosticsInfo = {};
//To Create boxPanel Datas
for (var i = 0; i < networkData.length; i++) {
diagnosticsInfo[networkData[i].ItemTitle] = networkData[i].itemValue;
}
//To Bind the data using Knockout
}
},
error: function (xhr) {
debugger;
alert(xhr.responseText);
}
});
debugger;
var viewModel = ko.mapping.fromJS(diagnosticsInfo);
ko.applyBindings(viewModel);
// Every time data is received from the server:
//ko.mapping.fromJS(data, viewModel);
});
#foreach (var nameValue in childContainer.NameValueImageItemsList)
{
var cssClass = "nameValueItem floatLeft" + " " + nameValue.DataBindName;
<div class="#cssClass" style="">#nameValue.DisplayName</div>
<div class="#cssClass" style="width: 200px; margin-right: 10px;" ><span data-bind="text: CLI"></span></div>
<div class="#cssClass" style="width: 200px; margin-right: 10px;">
<a>
#if (nameValue.IconImageURL != null && nameValue.IconImageURL != "")
{
<img src="#nameValue.IconImageURL" alt="i"/>
}
</a>
</div>
<div class="clearBOTH"></div>
}
Here's a really straightforward way to do this:
ko.applyBindings({
description: 'some description'
// ,title: 'my title'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Description: <span data-bind="text: description"></span><br />
Title: <span data-bind="text: !!title ? title : ''"></span>
A related option may be that you create a safeTitle computed property on your view model:
var Vm = function() {
var self = this;
self.description = 'my description';
//self.title = 'my title';
self.safeTitle = ko.computed(function() {
return !!self.title ? self.title : '';
});
};
ko.applyBindings(new Vm());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Description: <span data-bind="text: description"></span><br />
Title: <span data-bind="text: safeTitle"></span>
Furthermore, you could also do it with a function, so you don't have to create an observable for each property:
var Vm = function() {
var self = this;
self.description = 'my description';
//self.title = 'my title';
self.safeGet = function(prop) {
return !!self[prop] ? self[prop] : '';
};
};
ko.applyBindings(new Vm());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Description: <span data-bind="text: description"></span><br />
Title: <span data-bind="text: safeGet('title')"></span>
Note that this code would be slightly different if those properties are observables, and even more different (and complicated) if it can be either.
Yet another option may be to check out point 3 of this blog post, on wrapping existing bindings: you could create another "text" binding that guards against this situation.
PS. I'd carefully rethink your design. Most likely the fact that properties are "optional" is related to some domain concept.
PPS. You could also consider using the Null Object Pattern server side, and this problem goes away entirely.
PPPS. Here's one final way to circumvent the problem, (logical, but) much to my surprise:
ko.applyBindings({
desc: 'some description'
// ,title: 'my title'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Description: <span data-bind="text: $data.desc"></span><br />
Title: <span data-bind="text: $data.title"></span>
This is how it worked for me:
<span data-bind="text: $data['#nameValue.DataBindName'] "></span>

How can I toggle the display of a textarea via a button using knockout with the foreach binding?

I am new to knockout. For my problem, I am trying to make it so that for each project, there is a button and textarea. The textarea will be hidden upon page load. If I click the button, it will show the textarea (toggle). Currently, if I click the button, ALL textareas on the page will show, rather than just the corresponding textarea.
I'm hoping the fix for this isn't too dramatic and involving a complete reworking of my code as by some magic, every other functionality has been working thus far. I added the {attr id: guid} (guid is a unique identifier of a project retrieved from the database) statement in an attempt to establish a unique ID so that the right controls were triggered...although that did not work.
Sorry I do not have a working jfiddle to show the issue... I tried to create one but it does not demonstrate the issue.
JS:
//if a cookie exists, extract the data and bind the page with cookie data
if (getCookie('filterCookie')) {
filterCookie = getCookie('filterCookie');
var cookieArray = filterCookie.split(",");
console.log(cookieArray);
$(function () {
var checkboxes = new Array();
for (var i = 0; i < cookieArray.length; i++) {
console.log(i + cookieArray[i]);
checkboxes.push(getCheckboxByValue(cookieArray[i]));
//checkboxes.push(document.querySelectorAll('input[value="' + cookieArray[i] + '"]'));
console.log(checkboxes);
checkboxes[i].checked = true;
}
})
filterCookie = getCookie('filterResultsCookie');
cookieArray = filterCookie.split(",");
filterCookieObj = {};
filterCookieObj.action = "updateProjects";
filterCookieObj.list = cookieArray;
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify(filterCookieObj)
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
//if the cookie doesn't exist, just bind the page
else {
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify({
action: "getProjects"
})
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
View Model:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
};
};
HTML:
<!-- ko foreach: projects -->
<div id="eachOppyProject" style="border-bottom: 1px solid #eee;">
<table>
<tbody>
<tr>
<td><a data-bind="attr: { href: '/tools/oppy/' + guid }" style="font-size: 25px;"><span class="link" data-bind=" value: guid, text: name"></span></a></td>
</tr>
<tr data-bind="text: projectDescription"></tr>
<%-- <tr data-bind="text: guid"></tr>--%>
</tbody>
</table>
<span class="forminputtitle">Have you done project this before?</span> <input type="button" value="Yes" data-bind="click: $parent.toggleTextArea" class="btnOppy"/>
<textarea placeholder="Tell us a little of what you've done." data-bind="visible: $parent.show, attr: {'id': guid }" class="form-control newSessionAnalyst" style="height:75px; " /><br />
<span> <input type="checkbox" name="oppyDoProjectAgain" style="padding-top:10px; padding-right:20px;">I'm thinking about doing this again. </span>
<br />
</div><br />
<!-- /ko -->
Spencer:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.projects().forEach(function() { //also tried proj.forEach(function())
self.projects().showComments = ko.observable(false);
self.projects().toggleComments = function () {
self.showComments(!self.showComments());
};
})
};
It's weird that
data-bind="visible: show"
doesn't provide any binding error because context of binding inside ko foreach: project is project not the ProjectViewModel.
Anyway, this solution should solve your problem:
function ViewModel() {
var self = this;
var wrappedProjects = proj.map(function(p) {
return new Project(p);
});
self.projects = ko.observableArray(wrappedProjects);
}
function Project(proj) {
var self = proj;
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
}
return self;
}
The problem is that the show observable needs to be defined in the projects array. Currently all the textareas are looking at the same observable. This means you'll have to move the function showTextArea into the projects array as well.
Also you may want to consider renaming your function or getting rid of it entirely. Function names which imply they drive a change directly to the view fly in the face of the MVVM pattern. I'd recommend a name like "toggleComments" as it doesn't reference a view control.
EDIT:
As an example:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
foreach(var project in self.projects()) {
project.showComments = ko.observable(false);
project.toggleComments = function () {
self.showComments(!self.showComments());
};
}
};
There is probably a much cleaner way to implement this in your project I just wanted to demonstrate my meaning without making a ton of changes to the code you provided.

Inconsistency between Knockout mapping plugin retrieved viewModel objects and newed ones

I'm trying to add new objects into my knockout mapping plugin dowloaded viewmodel, like so:
<script type="text/javascript">
var myViewModel = {};
var Fighter = function (data) {
var self = this;
self.Name = ko.observable(data.Name);
self.Country = ko.observable(data.Country);
self.TopSpeed = ko.observable(data.TopSpeed);
};
var WarCraft = function (data) {
var self = this;
self.fighter = ko.observable(data.fighter);
};
var dataMappingOptions = {
key: function (data) {
return data.id;
},
create: function (options) {
if (data.id == 1)
return new Fighter(options.data);
else
if (data.id == 2)
return new WarCraft(options.data);
}
};
$.getJSON("/Home/GetServerData", function (model) {
ko.mapping.fromJS(model, dataMappingOptions, myViewModel);
ko.applyBindings(myViewModel);
}).error(function () { alert("Oops!") }).success(function () { alert("Yeah!") });
myViewModel.AddToData = function () {
var newFighter = new Fighter(
{
id : 1,
Name: myViewModel.warCraft.fighter.Name(),
Country: myViewModel.warCraft.fighter.Country(),
TopSpeed: myViewModel.warCraft.fighter.TopSpeed()
});
var newWarCraft = new WarCraft({ id: 2, fighter: newFighter });
myViewModel.WW2Machines.push(newWarCraft);
}.bind(myViewModel);
</script>
The server-side model:
DataModel model = new DataModel();
model.warCraft = new WarCraft();
model.warCraft.fighter = new Fighter();
model.warCraft.fighter.Name = "Spitfire";
model.WW2Machines = new List<WarCraft>();
WarCraft w1 = new WarCraft();
w1.fighter = new Fighter() { Name = "Spitfire" };
model.WW2Machines.Add(w1);
WarCraft w2 = new WarCraft();
w2.fighter = new Fighter() { Name = "Hurricane" };
model.WW2Machines.Add(w2);
WarCraft w3 = new WarCraft();
w3.fighter = new Fighter() { Name = "Tomcat" };
model.WW2Machines.Add(w3);
... the method invoked by the Ajax call, which initializes and sends the data :
DataModel model = new DataModel();
model.warCraft = new WarCraft();
model.warCraft.fighter = new Fighter();
model.warCraft.fighter.Name = "Spitfire";
model.WW2Machines = new List<WarCraft>();
WarCraft w1 = new WarCraft();
w1.fighter = new Fighter() { Name = "Spitfire" };
model.WW2Machines.Add(w1);
WarCraft w2 = new WarCraft();
w2.fighter = new Fighter() { Name = "Hurricane" };
model.WW2Machines.Add(w2);
WarCraft w3 = new WarCraft();
w3.fighter = new Fighter() { Name = "Tomcat" };
model.WW2Machines.Add(w3);
return Json(model, JsonRequestBehavior.AllowGet);
. . . and my html:
<div id="show" data-bind="visible: WW2Machines().length>0">
<h2>Information Display:</h2>
NewNumber : <span data-bind="text: WW2Machines().length"></span>
<ul data-bind="foreach: WW2Machines">
<li>
Name: <span data-bind="text: fighter.Name"></span>
<br> </br>
</li>
</ul>
</div>
<div id="theForm">
#using (Html.BeginForm())
{
<legend WW2 Fighter Planes: >
<fieldset>
Name:
<br> </br>
<select data-bind="value: warCraft.fighter.Name, optionsCaption: 'Please Select . . '">
<option>Mosquito</option>
<option>Mustang</option>
<option>Messerschmidt 109</option>
</select>
<br> </br>
<span data-bind="text: warCraft.fighter.Name"></span>
<br> </br>
Country:
<br> </br>
<select data-bind="value: warCraft.fighter.Country, optionsCaption: 'Please Select . . '">
<option>England</option>
<option>USA</option>
<option>Germany</option>
</select>
<br> </br>
<span data-bind="text: warCraft.fighter.Country"></span>
<br> </br>
Top Speed:
<br> </br>
<select data-bind="value: warCraft.fighter.TopSpeed, optionsCaption: 'Please Select . . '">
<option>390 km/h</option>
<option>275 km/h</option>
<option>250 km/h</option>
</select>
<br> </br>
<span data-bind="text: warCraft.fighter.TopSpeed"></span>
<input type="button" data-bind="click: AddToData">Add</input>
</fieldset>
</legend>
}
The problem is that my Add button is triggered and does add rows to the display area, except they're empty. In the debugger console in Firefox, I see that I can reference both the rows sent from the server, and the rows added by the Add button (containing data correctly) with one puzzling difference :
I get to the existing rows with this syntax:
myViewModel.WW2Machines()[0].fighter.Name()
. . . and my added rows with THIS syntax :
myViewModel.WW2Machines()[3].fighter().Name()
. . . and swopping syntax between each other for respective subscripts throws an error. This may be a clue as to why my rows are being added as blank, but I have no clue why? Can anybody help please?
At least some of the problem—and the explanation for the puzzling difference you're running into at the bottom—is that the rows you're adding later make WarCraft.fighter an observable, which the mapping plugin is not doing.
So you could exchange this:
var WarCraft = function (data) {
var self = this;
self.fighter = ko.observable(data.fighter);
};
for this
var WarCraft = function (data) {
var self = this;
self.fighter = data.fighter;
};
and you should be able to access both the mapped data and added data the same way:
myViewModel.WW2Machines()[0].fighter.Name()
Adrien, can you perhaps offer an explanation why the mapping plugin doesn't make WarCraft.fighter observable? I thought all objects and sub-properties mapped using the plugin default to observable. Thanks,

Categories

Resources