Svish
Svish

Reputation: 158021

How to add stuff to Knockout mapping view model?

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
                },
                ...
            ]
        },
        ...
    ]
}

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

Answers (1)

Rafael Companhoni
Rafael Companhoni

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:

  1. numberOfFiles: returns the total number of files in every FileGroup
  2. selectedFiles: returns an array containing all the selected files in every FileGroup

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

Related Questions