I'm looking the best way to get my server model mapped into the knockout viewmodel
for this purpose here I'm using the mapping plugin,
Id, Title, People were mapped to something similar to the groupViewModel I have here,
How can I be sure and force this mapping to always be casted exactly to a new groupViewModel, the real issue here is Id, Title, People are bound to the view, not the addPerson method,
Please share the best workaround on this, or any better way you know to make it thiis mapping precise and yet simple and clean, thanks.
here our viewModel contains Groups Array, how to cast this Groups array into GroupViewModel items , maybe the question could be answered this way.
var model = #Html.Raw(Json.Encode(Model));
$(function() {
var root = this;
var viewModel = ko.mapping.fromJS(model, root);
// there are Groups as expected but not the addPerson
// will cause undefined exception
var groupViewModel = function (item) {
var self = this;
self.Id = ko.observable(item.Id);
self.Title = ko.observable(item.Title);
self.People = ko.observableArray([]);
self.addPerson = function (name) {
console.log('addPerson Clicked');
}; // .bind(self)
}
ko.mapping.fromJS(viewModel);
ko.applyBindings(viewModel);
});
Edit - Updated based to the answer :
Server ViewModel
In Server we have this :
class ServerVm {
public int Id { get; set; }
public IList<GroupMap> Groups { get; set; } // groupViewModel
}
In the client we have :
var model = #Html.Raw(Json.Encode(Model));
$(function() {
var root = this;
var viewModel = ko.mapping.fromJS(model, root);
//viewModel.Groups = ko.observableArray([]);
// No need because it's same as server model
viewModel.addGroup = function (teamName) {
console.log('addGroup Clicked');
}; // .bind(self)
// addGroup is fine
// it's defined here and working fine, no special mapping needed here
// "model" contains something
var groupViewModel = function (item) {
var self = this;
self.Id = ko.observable(item.Id);
self.Title = ko.observable(item.Title);
self.People = ko.observableArray([]);
// self.addPerson - this is hidden in the "model"
self.addPerson = function (name) {
console.log('addPerson Clicked');
}; // .bind(self)
}
ko.mapping.fromJS(viewModel);
ko.applyBindings(viewModel);
});
Our problem is the self.addPerson located inside our nested collection which because the container(groupViewModel) isn't bound automatically to the GroupMap. everytime I create groupViewModel by hand it's ok cause I'm casting it myself, but this is not the real mapping solution, what's yours, thanks
You could use different overload of ko.mapping.fromJS method that takes 3 parameters, from documentation:
Specifying the update target
...The third parameter to ko.mapping.fromJS indicates the target.
ko.mapping.fromJS(data, {}, someObject);
So in your case you could update your view model definition as follows:
function ViewModel() {
var self = this;
this.addPerson = function(data) {
$.ajax({
url: ...+ self.Id,
contentType: 'application/json;charset=utf-8',
dataType: 'JSON',
type: "POST",
success: function (result) // result
{
console.log('Success');
var avm = new childViewModel(result,self); // another defined vm
self.People.push(avm);
}
});
}
}
ViewModel.prototype.init = function(data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
}
And to initialize it:
...
var model = #Html.Raw(Json.Encode(Model));
...
var vm = new ViewModel();
vm.init(model);
ko.applyBindings(vm);
See working demo.
Another approach, a bit shorter, is to map your model first and then add methods to it, like this:
var vm = ko.mapping.fromJS(model);
vm.addPerson = function(data) {
...
See another demo.
I like first approach more since function is defined inside view model and not added later.
Update:
So, after some clarification in comments and after question update, here is what should be done:
You should use mentioned ko.mapping.fromJS method inside that child object to map it's properties automatically.
You should use mapping object to tell the mapping plugin how to map your child object.
The child object view model:
function groupViewModel(data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
self.addPerson = function(personData) {
// here should be your ajax request, I'll use dummy data again
};
}
The mapping object:
var mapping = {
"Groups" : {
'create': function(options) {
return new groupViewModel(options.data);
}
}
};
And initialization:
var vm = ko.mapping.fromJS(model, mapping);
Here is updated demo.
Related
I have a bootstrap nav-tab and I want to display dynamically content when I select a tab. Ajax returns an array of Results. Each result has Price,Logo,Companyname and an array of Covers. Each cover has Price,MaxCover,Optional and Description.
Rest of html code is here link but now I want to return a more complex type.
<script type='text/javascript'>
var cover = new
{
Price: ko.observable(),
MaxCover: ko.observable(),
Optional: ko.observable(),
Description: ko.observable(),
}
var result = new
{
Price: ko.observable(),
InsLogo: ko.observable(),
CompanyName: ko.observable(),
Covers: ko.observableArray()
};
var tab = function (Id, name, selected) {
this.Id = Id;
this.name = name;
this.Results = ko.observableArray();
this.isSelected = ko.computed(function () {
return this === selected();
}, this);
}
var ViewModel = function () {
var self = this;
self.selectedTab = ko.observable();
self.tabs = ko.observableArray([
new tab(0, 'Tab1', self.selectedTab),
new tab(1, 'Tab2', self.selectedTab),
new tab(2, 'Tab3', self.selectedTab)
]);
self.selectedTab(self.tabs()[0]);
self.selectedTab.subscribe(function () {
$.ajax({
url: '#Url.Action("GetSection")',
data: { Id: self.selectedTab().Id },
type: 'GET',
success: function (data) {
self.selectedTab().Results(data.Results); //Here I want to fill the results!!!!!!
}
});
});
}
ko.applyBindings(new ViewModel());
Your approach is okay with a few small glitches. Some suggestions to improve it:
Make your viewmodels so that they initialize themselves from a parameters object.
Don't introduce dependencies between viewmodels when you don't have have to. I'm thinking of the isSelected property here, this can be taken care of in the view. For example, when inside a foreach: tabs: data-bind="css: {selected: $data === $parent.selectedTab()}"
You have a timing issue: Subscribe to selectedTab first, then initialize it with self.selectedTab(self.tabs()[0]);. It should be obvious why. (Generally it's useful to split viewmodel creation into a "setup" and an "init" phase.)
Only send an Ajax request for tab details when tab details are still empty.
Subscribtions receive the new value of the observable as an argument, use it.
Observables are functions:
If you want to store an Ajax response in them you can use them as a callback directly.
In the same way you can use them as an event handler:
data-bind="click: $parent.selectedTab".
JS convention is to use PascalCase for constructor names (like in viewmodels) and camelCase for all other identifiers.
With all that, we get:
function Tab(data) {
this.Id = data.Id;
this.name = data.name;
this.Results = ko.observableArray();
}
function ViewModel(data) {
var self = this;
// setup
self.selectedTab = ko.observable();
self.selectedTab.subscribe(function (selectedTab) {
if (selectedTab.Results()) return;
$.get('#Url.Action("GetSection")', {Id: selectedTab.Id}).done(selectedTab.Results);
});
// init
self.tabs = ko.observableArray(ko.utils.arrayMap(data.tabs, function (tabData) {
return new Tab(tabData);
}));
self.selectedTab(self.tabs()[0]);
}
ko.applyBindings(new ViewModel({
tabs: [
{Id: 0, name: 'Tab1'},
{Id: 1, name: 'Tab2'},
{Id: 2, name: 'Tab3'}
]
}));
To convert plain data structures that come from the server (like your array results and covers) into a structure of viewmodels, observables and observable arrays I recommend looking into the mapping plugin.
I'm using knockout mapping to help map a serverside object into JSON. I have an object with numerous collections in it so I don't want to have to recreate and map each piece manually in javascript. So, I'm using knockout-mapping to do this for me.
I was having issues, so I decided to try it with a simple example, so here is what I have for an ASP.NET MVC application:
C# Model:
public class Vaccinations
{
public string Vaccination { get; set; }
public System.DateTime VaccinationDate { get; set; }
}
public class Dog
{
public string Name { get; set; }
public int Age { get; set; }
public Dog()
{
this.Vaccinations = new System.Collections.Generic.List<Vaccinations>();
}
public System.Collections.Generic.List<Vacinations> Vacinations { get; set; }
}
As you can see, each Dog has a list of vaccinations they may or may not have.
In my controller, I create and return a pre-populated Dog object:
public ActionResult Load()
{
Dog rambo = new Dog
{
Name = "Rambo",
Age = 5
};
rambo.Vacinations = new System.Collections.Generic.List<Vacinations> {
new Vacinations { Vacination = "Rabies", VacinationDate = DateTime.Now.AddYears(-1) },
new Vacinations { Vacination = "Mumps", VacinationDate = DateTime.Now.AddYears(-2) }
};
return Json(rambo, JsonRequestBehavior.AllowGet);
}
In my view (Index.cshtml), I have it set up to show the dog and a list of it's vaccinations. I want to allow the user to click on an Add Vaccination button to add a new line to the collection and allow them to enter the data.
Again, I'm using knockout.mapping to do the mapping for me. In the javascript section, this is what I have:
var ViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
self.isValid = ko.computed(function () {
return self.Name().length > 3;
});
// Operations
self.save = function () {
$.ajax({
url: "Dog/Save",
type: "post",
contentType: "application/json",
data: ko.mapping.toJSON(self),
success: function (response) {
alert(response.Status);
}
});
};
self.addVaccination = function () {
self.Vaccinations.push(new self.Vaccination()); // <--- This doesn't work and I know why, so how do I do this?
}
};
$(function () {
$.getJSON("Dog/Load", null, function (data) {
ko.applyBindings(new ViewModel(data));
});
});
My question revolves around the "addVaccination" function that I've added to the ViewModel object. How do I specify a new "Vaccination" object without having to "code" one in Javascript? That was the entire reason for me using knockout mapping so I don't have to do that. But, I don't see any other way around it.
Is there a way to access the base Vaccination object from the Vaccinations observable array so I can create a new one?
And then the final question is, how to I pass this back to my controller? I'm not sure if this will work or not.
You can't directly. But what you can do is define a Vaccination instance at the server side and return it as a the default instance.
So, you need to return the old data and the default instance.
public ActionResult Load()
{
...
var data = new {
defaultVacination = new Vacination(),
rambo = rambo,
};
return Json(data , JsonRequestBehavior.AllowGet);
}
And on the client side you receive the same data and the default instance.
var ViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data.rambo, {}, self);
var defaultInstance = data.defaultVacination;
...
self.addVaccination = function () {
// clone the default instance.
self.Vaccinations.push(ko.utils.extend({}, defaultInstance));
}
I hope it helps.
I have a simple MVC Model like this :
public class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string MobileNumber { get; set; }
public string EmailAddress { get; set; }
}
I would like to map one object of this type inside a knockout viewmodel in order to populate it:
var UserViewModel = function () {
var self = this;
self.user = ko.mapping.fromJS({});
$.getJSON("/UserManagement/CreateEmptyUser", function (data) {
ko.mapping.fromJS(data, self.user);
});
self.createUser = function (data, eventArgs) {
var user = data;
$.ajax({
type: "post",
contentType: "application/json",
data: ko.toJSON(user),
url: "#Url.Action("CreateUser")",
success: function () {
window.location = "#Url.Action("AddNew")";
}
});
};
};
The problem I am having is that I have no clue how to map any single object. I've tried using
self.user = ko.mapping.fromJS([]);
$.getJSON("/UserManagement/CreateEmptyUser", function (data) {
ko.mapping.fromJS(data,{}, self.user);
});
which is being used for arrays and trying to extract the element at index 0, the other solution I thought would work is the one in the 2nd comment block. Everything I found on google led me to mapping entire arrays, but nothing towards simple object mapping. Is there a proper way to do this? I would like to keep the model separated and not manually describe its properties in javascript.
Thanks,
Alex Barac
I think your problem is that you are using the mapping from method to instantiate your view model, which will give it no properties, so when you try to map the data after your JSON call, there are no properties on self.user to populate. Either create your user object with the properties you want on there, or else use mapping.fromJS to create your viewmodel in the first place:
self.user = { FirstName: ko.observable(''), etc... }
and leave your mapping line as it is, or
self.user;
$.getJSON("/UserManagement/CreateEmptyUser", function (data) {
self.user = ko.mapping.fromJS(data);
}
I kept working and checking this and I found the solution I wanted at the beginning. The idea is using #Paul Manzotti 's suggestion,
self.user;
$.getJSON("/UserManagement/CreateEmptyUser", function (data) {
self.user = ko.mapping.fromJS(data);
}
but replacing the self.user; with self.user={}; and
self.user = ko.mapping.fromJS(data)
with
ko.mapping.fromJS(data,{},self.user);
If using this, there is no need to manually declare every object's property in the viewmodel. Hope this will be of use to those that stumble upon the same issue.
If your viewModel in question needs to be an observable itself, then I found that use the mapping to return an observable array as the OP mentioned, and then returning the first item in that array works. This is also useful for return one object in ajax that you then wish to push onto an existing observableArray.
I have a helper function for this:
koMappingUsingArray = function (jsObject, mapping) {
var observableObjectArray = ko.observableArray();
var objectArray = new Array(jsObject);
ko.mapping.fromJS(objectArray, mapping, observableObjectArray);
return observableObjectArray()[0];
};
and then call it such as:
self.myObject = ko.observable();
$.ajax({
type: "POST",
url: "....",
data: ko.mapping.toJSON(???),
contentType: "application/json; charset=utf-8",
success: function (returnedObject) {
self.myObject (koMappingUsingArray(returnedObject));
});
I started using knockout.js and i really like it.
I use ASP.Net mvc , jQuery and knockout.js
My question is this:
lets say i have a management screen of a user, the user is my view model
inside the user i want an array of permissions
my user viewModel:
var userViewModelClass = function () {
var self = this;
ko.mapping.fromJS({
ID: "",
permissions: []
}, {}, self);
}
now.. if i do an ajax request to the server and get a JSON back i user the mapping plugin, and everything goes as expected
but... now i want my rendered permissions list to have an action like delete.
so i will need a permission object and then the array of permissions will be an array of permission objects. but how do i do that? how will the mapping plugin know that the object returned to him from the server in an array is actually on object like this one:
function permission() {
var self = this;
this.delete = function () {
};
ko.mapping.fromJS({
name: "",
level: ""
}, {}, self);
}
that's my first part of the question.
the second part:
lets say i got the model with an array of all permissions and they are all of this permission object. now i want each delete button in my view to be bind the the delete function inside the permission object.
using:
data-bind="click: delete"
what is the best implementation for a delete function? i thought about something like: makeing an ajax call to the server which will actually delete the permission from the user. then if the call succeeds remove the current permission from the observable array, then the view will update...
is that a good practice?
thanks!
First part. You need to use mapping options. In your userViewModelClass do this.
var userViewModelClass = function () {
var self = this;
ko.mapping.fromJS({
ID: "",
permissions: []
}, {
permissions: {
create: function(options) {
return new permission(options.data);
}
}
}, self);
}
And modify your permission object like so
function permission(config) {
var self = this;
this.delete = function () {
};
ko.mapping.fromJS($.extend({
name: "",
level: ""
}, config), {}, self);
}
Note I added the extend in so that your default structure will remain and be overwritten by incoming data.
Second part of your question. One possible way would be to pass a reference to the parent in your constructor. So the above mapping options would become
permissions: {
create: function(options) {
return new permission(options.data, self);
}
}
Then your delete could be something like.
this.delete = function () {
$.ajax(deleteurl, yourdata, function(result) {
// success
parent.permissions.remove(self);
}, function() {
// failure
display error message
}
};
EDIT
Alternate way as discussed in comments.
var userViewModelClass = function () {
var self = this;
ko.mapping.fromJS({
ID: "",
permissions: []
}, {
permissions: {
create: function(options) {
return new permission(options.data);
}
}
}, self);
this.delete = function(permission) {
self.permissions.remove(permission);
};
}
data-bind="click: $parent.delete"
Hope this helps.
I am creating a view model in Knockout from a model in mvc4. I am using the mapping plugin. Current code looks like this.
First: I merge the different MVC4 models I need into a single ko viewmodel.
var mergedData = $.extend(true, {}, initialEventData,
{ "Tickets": initialTicketData }, { "TimeZones": timeZones }
);
Second: I add some mapping to add a computed function to my viewmodel.
var mapping = {
'Tickets': {
create: function (options) {
return new updatedTicket(options.data);
}
}
}
var updatedTicket = function (data) {
ko.mapping.fromJS(data, {}, this);
this.formattedPrice = ko.computed(function () {
return "$" + parseFloat(this.Price()).toFixed(2);
}, this);
}
Finlly: I apply the bindings.
var eventViewModel = ko.mapping.fromJS(mergedData, mapping);
However: Sometimes the Tickets model may come back empty. When this happens, the mapping plugin does not create the observable array (obviously). I need to have an empty array with mapped properties created so that I can push new tickets.
I would just add a check to the mapping configuration. Check if the data is there in the updatedTicket function and if it's not, manually create an observable array
just make a container viewmodel where you can create your default tickets array...
also in your callback functions, make sure you use a variable referring to "this"
var mapping = {
'Tickets': {
create: function (options) {
return new updatedTicket(options.data);
}
}
}
var updatedTicket = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, this);
this.formattedPrice = ko.computed(function () {
return "$" + parseFloat(self.Price()).toFixed(2);
}, this);
}
var viewModel = function ( ) {
this.Tickets = ko.observableArray([]);
ko.mapping.fromJS(mergedData, mapping, this);
}
var eventViewModel = new viewModel();