I am trying to get to grips with KnockoutJS. I have had a bit of success replicating Steven Sanderson's seminar example from mix11 (Person with Friends on Twitter) .
I am trying to extend it so that I can get JSON from my ASP.NET MVC4 controller, and automatically bind the data to the viewModel.
I managed to get it working quite quickly with manual mapping of my JSON object to knockout objects with observables, however, this is a simple model with low complexity. When I come to use this for real, the models will likely be more complicated, and manual mapping will be less attractive.
I think this being an ASP.NET MVC4 page is irrelevant, as I am getting valid JSON into the markup.
Here is the full mark up:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Person</title>
<link href="/KnockoutSample/Content/site.css" rel="stylesheet"/>
<script src="/KnockoutSample/Scripts/jquery-2.0.0.js"></script>
<script src="/KnockoutSample/Scripts/modernizr-2.6.2.js"></script>
<script src="/KnockoutSample/Scripts/knockout-2.2.1.debug.js"></script>
<script src="/KnockoutSample/Scripts/knockout.mapping-latest.js"></script>
</head>
<body>
<h2>Person</h2>
<p>Full name: <span data-bind="text: fullName" ></span></p>
<p>First name: <input type="text" data-bind="value: firstName" /></p>
<p>Last name: <input type="text" data-bind="value: lastName" /></p>
<h2>Friends (<span data-bind="text: friends().length"></span>)</h2>
<ol data-bind="template: { name: 'friendsTemplate', foreach:friends}"></ol>
<script id="friendsTemplate" type="text/html">
<li>
<input data-bind="value: fullName"/>
<button data-bind="click: removeFriend">Remove</button>
<label><input type="checkbox" data-bind="checked: isOnTwitter" />Is On Twitter</label>
<input type="text" placeholder="Please enter username" data-bind="value: twitterName, visible: isOnTwitter" />
</li>
</script>
<button data-bind="click: addFriend, enable: friends().length < 5">Add Friend</button>
<button data-bind="click: save">Save</button>
<script type="text/javascript">
function friend() {
function instanceOfConstructor(newFriend) {
return {
fullName: newFriend.fullName,
isOnTwitter: newFriend.isOnTwitter,
twitterName: newFriend.TwitterName,
removeFriend: function () {
viewModel.friends.remove(this);
}
};
}
function paramatisedConstructor(name, onTwitter, twitterName) {
return {
fullName: ko.observable(name),
isOnTwitter: ko.observable(onTwitter),
twitterName: ko.observable(twitterName),
removeFriend: function () {
viewModel.friends.remove(this);
}
};
}
switch (arguments.length) {
case 1 :
return instanceOfConstructor(arguments[0]);
case 3 :
return paramatisedConstructor(arguments[0], arguments[1], arguments[2]);
}
}
var viewModel = {
firstName : ko.observable(),
lastName: ko.observable(),
friends: ko.observableArray(),
addFriend: function () {
this.friends.push(new friend("New Friend", false, null));
},
save: function () {
$.ajax({
url: "/KnockoutSample/Main/Person",
type: "POST",
data: ko.toJSON(this),
contentType: "application/json",
}).success(function(result){
alert(result.message);
}).fail(function (data) {
alert(data);
});
}
};
viewModel.fullName = ko.dependentObservable(function () {
return this.firstName() + " " + this.lastName();
}, viewModel);
ko.applyBindings(viewModel);
var initialData = '{"firstName":"Ian","lastName":"Robertson","friends":[{"isOnTwitter":false,"TwitterName":"","fullName":"Friend One"},{"isOnTwitter":true,"TwitterName":"@FriendTwo","fullName":"Friend Two"}]}';
var tmp = ko.mapping.fromJSON(initialData);
//Convention based auto-mapping does not work
//ko.mapping.fromJSON(initialData, viewModel);
//Manual mapping does work
viewModel.firstName(tmp.firstName());
viewModel.lastName(tmp.lastName());
$.each(tmp.friends(), function (i, _friend) {
viewModel.friends.push(new friend(_friend));
});
</script>
</body>
</html>
I am hoping its possible to avoid the manual mapping at the end:
//Convention based auto-mapping does not work
//ko.mapping.fromJSON(initialData, viewModel);
//Manual mapping does work
viewModel.firstName(tmp.firstName());
viewModel.lastName(tmp.lastName());
$.each(tmp.friends(), function (i, _friend) {
viewModel.friends.push(new friend(_friend));
});
Any pointers on how I can use the mapping plugin to avoid manual mapping would be much appreciated.
UPDATE:
<script type="text/javascript">
function friend(name, onTwitter, twitterName) {
return {
fullName: ko.observable(name),
isOnTwitter: ko.observable(onTwitter),
TwitterName: ko.observable(twitterName),
removeFriend: function () {
viewModel.friends.remove(this);
}
};
}
var initialData = '@Html.Raw(ViewBag.InitialData)';
var viewModel = {
firstName: ko.observable(),
lastName: ko.observable(),
friends: ko.observableArray()
};
viewModel = ko.mapping.fromJSON(initialData, viewModel);
viewModel.save = function () {
$.ajax({
url: "@Url.Action("Person")",
type: "POST",
data: ko.toJSON(this),
contentType: "application/json",
}).success(function (result) {
alert(result.message);
}).fail(function (data) {
alert(data);
});
};
viewModel.addFriend = function () {
this.friends.push(new friend("New Friend", false, null));
};
try {
viewModel.fullName = ko.dependentObservable(function () {
return this.firstName() + " " + this.lastName();
}, viewModel);
$.each(viewModel.friends(), function (i, _friend) {
_friend.removeFriend = function () {
viewModel.friends.remove(this);
}
});
ko.applyBindings(viewModel);
} catch (e) {
alert(e);
}
This is almost the solution I wanted to achieve. The only thing I will continue to try and improve upon is using a jquery $.each function to add the "removeFriend" function to each friend element in the array.