javascriptjsonknockout.jsknockout-mapping-plugin

KnockoutJS Mapping Plugin (observableArray)


I am new to knockout and I am having a problem with using the mapping plugin as I do not understand how it maps my JSON data.
This is a sample json data similar to what is in my program:

contact: {
        name : 'John',
        email : 'address@domain.com',
        phones : [{
            phoneType : 'Home Phone',
            phoneNumber: '999-888-777'},
            {
            phoneType : 'Business Phone',
            phoneNumber: '444-888-777'},
            }]
        }

As you can see, this json data contains an array of phones.
I used knockout mapping plugin and I can bind the 'name', 'email' and loop the phone numbers in a 'foreach: phones' with no hassle until I try to make a ko.compute on the phoneNumber which is an object in the array phones.

@section scripts
{
    <script src="~/ViewModels/ContactModel.js"></script>
    <script type="text/javascript">
        var viewModel = new ContactModel(@Html.Raw(Model.ToJson()));
        $(document).ready(function () {
            ko.applyBindings(viewModel);
        });
</script>

<label>Name</label><input data-bind="value: name" />
<label>Email</label><input data-bind="value: email" />
<label>Phones</label>
<table>
  <tbody data-bind="foreach: phones">
     <tr>
      <td><strong data-bind='text: phoneType'></strong></td>
      <td><input data-bind='value: phoneNumber' /></td>
     </tr>
   /tbody>
 </table>

This is ContactModel.js

    var ContactModel = function (data) {
    var self = this;
    ko.mapping.fromJS(data, {}, self);

    self.reformatPhoneNumber = ko.computed(function(){
    var newnumber;
    newnumber = '+(1)' + self.phones().phoneNumber;
    return newnumber;
    });

    };

For visual representation this is how this looks right now:

Name: John
Email: address@domain.com
Phones:
<--foreach: phones -->
Home Phone: 999-888-777
Business Phone: 444-888-777

What Im trying to do is to reformat the phoneNumber to display it this way:

Name: John
Email: address@domain.com
Phones:
<--foreach: phones -->
Home Phone: (+1)999-888-777
Business Phone: (+1)444-888-777

I try to do it by using the reformatPhoneNumber in place of phoneNumber in my binding like this:

<table>
      <tbody data-bind="foreach: phones">
         <tr>
          <td><strong data-bind='text: phoneType'></strong></td>
          <td><input data-bind='value: $root.reformatPhoneNumber' /></td>
         </tr>
       /tbody>
     </table>

But when I do, the value of reformatPhoneNumber doesn't appear. I read somewhere here that I have to make the objects inside my observableArray also observable because ko.mapping doesn't do that by default. But I cannot picture how to do it as I was expecting ko.mapping plugin to do all the job automatically for me as I am new to this jslibrary.
Any help would be greatly appreciated. Thank you very much!!


Solution

  • Your use of naming (a computed named reformatPhoneNumber) suggests that you think of computeds as functions. While they are, technically, functions, they represent values. Treat them as values, just like you treat observables. In your case this means it should be called more like formattedPhoneNumber and should live as a property of the phone number, not as a property of the contact.

    Separate your models into individual units that can bootstrap themselves from raw data.

    The smallest unit of information in your model hierarchy is a phone number:

    function PhoneNumber(data) {
        var self = this;
    
        self.phoneType = ko.observable();
        self.phoneNumber = ko.observable();
        self.formattedPhoneNumber = ko.pureComputed(function () {
            return '+(1) ' + ko.unwrap(self.phoneNumber);
        });
    
        ko.mapping.fromJS(data, PhoneNumber.mapping, self);
    }
    PhoneNumber.mapping = {};
    

    Next in the hierarchy is a contact. It contains phone numbers.

    function Contact(data) {
        var self = this;
    
        self.name = ko.observable();
        self.email = ko.observable();
        self.phones = ko.observableArray();
    
        ko.mapping.fromJS(data, Contact.mapping, self);
    }
    Contact.mapping = {
        phones: {
            create: function (options) {
                return new PhoneNumber(options.data);
            }
        }
    };
    

    Next is a contact list (or phone book), it contains contacts:

    function PhoneBook(data) {
        var self = this;
    
        self.contacts = ko.observableArray();
    
        ko.mapping.fromJS(data, PhoneBook.mapping, self);
    }
    PhoneBook.mapping = {
        contacts: {
            create: function (options) {
                return new Contact(options.data);
            }
        }
    };
    

    Now you can create the entire object graph by instantiating a PhoneBook object:

    var phoneBookData = {
        contacts: [{
            name: 'John',
            email: 'address@domain.com',
            phones: [{
                phoneType: 'Home Phone',
                phoneNumber: '999-888-777'
            }, {
                phoneType: 'Business Phone',
                phoneNumber: '444-888-777'
            }]
        }]
    };
    var phoneBook = new PhoneBook(phoneBookData);
    

    Read through the documentation of the mapping plugin.

    Expand the following code snippet to see it work.

    function PhoneBook(data) {
        var self = this;
    
        self.contacts = ko.observableArray();
        
        ko.mapping.fromJS(data, PhoneBook.mapping, self);
    }
    PhoneBook.mapping = {
        contacts: {
            create: function (options) {
                return new Contact(options.data);
            }
        }
    };
    // ------------------------------------------------------------------
    
    function Contact(data) {
        var self = this;
        
        self.name = ko.observable();
        self.email = ko.observable();
        self.phones = ko.observableArray();
        
        ko.mapping.fromJS(data, Contact.mapping, self);
    }
    Contact.mapping = {
        phones: {
            create: function (options) {
                return new PhoneNumber(options.data);
            }
        }
    };
    // ------------------------------------------------------------------
    
    function PhoneNumber(data) {
        var self = this;
        
        self.phoneType = ko.observable();
        self.phoneNumber = ko.observable();
        self.formattedPhoneNumber = ko.pureComputed(function () {
            return '+(1) ' + ko.unwrap(self.phoneNumber);
        });
        
        ko.mapping.fromJS(data, PhoneNumber.mapping, self);
    }
    PhoneNumber.mapping = {};
    // ------------------------------------------------------------------
    
    var phoneBook = new PhoneBook({
        contacts: [{
            name: 'John',
            email: 'address@domain.com',
            phones: [{
                phoneType: 'Home Phone',
                phoneNumber: '999-888-777'
            }, {
                phoneType: 'Business Phone',
                phoneNumber: '444-888-777'
            }]
        }]
    });
    
    ko.applyBindings(phoneBook);
    <script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
    
    <ul data-bind="foreach: contacts">
        <li>
            <div data-bind="text: name"></div>
            <div data-bind="text: email"></div>
            <ul data-bind="foreach: phones">
                <li>
                    <span data-bind="text: phoneType"></span>:
                    <span data-bind="text: formattedPhoneNumber"></span>
                </li>
            </ul>
        </li>
    </ul>
    
    <hr />
    Model data:
    <pre data-bind="text: ko.toJSON(ko.mapping.toJS($root), null, 2)"></pre>
    
    Viewmodel data:
    <pre data-bind="text: ko.toJSON($root, null, 2)"></pre>