Reputation: 158021
I'm new to Knockout and I'm having trouble understanding how to "edit" a view model when using the Knockout mapping plugin. Was hoping someone could help me out. I have a list with lists. Below is a similar example. Basically multiple groups with multiple files.
[
{
"group": "Alice",
"files": [
{"filename": "red.mp3", "length": 5},
{"filename": "blue.mp3","length": 6},
{"filename": "yellow.mp3","length": 5}
]
},
{
"group": "Bob",
"files": [
{"filename": "green.mp3","length": 2},
{"filename": "purple.mp3","length": 10}
]
}
]
And I can get the basic model from this:
$.getJSON('api/get-list', function(data)
{
view = ko.mapping.fromJS(data);
ko.applyBindings(view);
});
It works, and I've managed to bind it up in the HTML so it's visible and all is fine in that area. But, I need to add a couple of things, and I'm not sure how to do this. And more important, how to do it cleanly and well.
I'm outputting the files with a checkbox, and I want a 'select' property bound to it. I've been able to do it by adding the field in the backend, but don't want that as it really shouldn't be there. Also need to show a count of how many is currently selected, out of how many, per group, and total.
So, basically I want something like this:
{
"formSubmit": ?,
"totalNumberOfFiles": ?,
"totalNumberOfSelectedFiles": ?,
"groups":
[
{
"group": "Alice",
"numberOfFiles": ?,
"selectedFiles": ?,
"files": [
{
"filename": "red.mp3",
"length": 5,
"selected": boolean
},
...
]
},
...
]
}
numberOfFiles
is probably not even needed? Can get that from files.length
or something?selectedFiles
be a function/observable that counts the number of selected files (how would that look?) or should it rather be a list that was added to/removed from somehow (and how would one do that?)Basically, I know (can figure out) how to do the binding when just the model is working, but don't understand how to build it in a good way when using the mapping plugin (and I really don't want to do it manually).
Hope someone can help me out, cause I just can't figure this out 😕
Upvotes: 2
Views: 463
Reputation: 1790
When you use ko.mapping.fromJS each property is converted to an observable and each array is converted to an observableArray.
The main view model, MyViewModel, has a list of FileGroups which is initialized with a mapping that uses a custom mapping object. This object has a 'create' callback (as explained in http://knockoutjs.com/documentation/plugins-mapping.html) that instantiates a new FileGroup.
In the FileGroup constructor, just before creating the new sub view model, a property 'selected' is added with false being its default value.
Also, the main view model has two computed observables:
Within the submit method there's a simple alert to demonstrate how to access the array of selected files.
// data obtained from the server
var data = [
{
"group": "Alice",
"files": [
{ "filename": "red.mp3", "length": 5 },
{ "filename": "blue.mp3", "length": 6 },
{ "filename": "yellow.mp3", "length": 5 }
]
},
{
"group": "Bob",
"files": [
{ "filename": "green.mp3", "length": 2 },
{ "filename": "purple.mp3", "length": 10 }
]
}
];
// sub view model representing a single file grouping
var FileGroup = function (data) {
data.files.map(f => f.selected = false);
ko.mapping.fromJS(data, {}, this);
}
// main view model
var MyViewModel = function (data) {
this.fileGroups = ko.mapping.fromJS(data, { create: options => new FileGroup(options.data) });
this.numberOfFiles = ko.computed(() => {
return this.fileGroups().reduce((total, fg) => {
total += fg.files().length;
return total;
}, 0);
}, this);
this.selectedFiles = ko.computed(function() {
return this.fileGroups().reduce((selectedFiles, fg) => {
selectedFiles.push.apply(selectedFiles, fg.files().filter(f => f.selected()));
return selectedFiles;
}, [])
}, this);
this.submit = function() {
alert("FILES POSTED TO SERVER: " + this.selectedFiles().length);
}
}
var viewModel = new MyViewModel(data);
ko.applyBindings(viewModel);
.fileGroup {
border: 1px solid lightgray;
margin-bottom: 15px;
padding: 10px;
}
.selected {
border: 1px solid lightgreen;
margin-bottom: 15px;
padding: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js" type="text/javascript"></script>
<div data-bind="foreach: fileGroups">
<h3 data-bind="text: group"></h3>
<div data-bind="foreach: files" class="fileGroup">
<input type="checkbox" data-bind="checked: selected">
<span data-bind="text: filename" />
</div>
</div>
<h4>Number of Files: <span data-bind="text: numberOfFiles"></span></h4>
<div data-bind="foreach: selectedFiles, visible: selectedFiles().length > 0" class=selected>
<span data-bind="text: filename" />
</div>
<button data-bind="click: submit">Submit</button>
Upvotes: 3