1

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 : '[email protected]',
        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: [email protected]
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: [email protected]
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!!

3
  • Test question: What value does self.phones().phoneNumber refer to, exactly? Commented Apr 25, 2015 at 10:06
  • Also, what is self in your case? Please post a minimal but complete example. StackOverflow has the tools to include a runnable example right inside your question. Commented Apr 25, 2015 at 10:09
  • @Tomalak hi tomalak, thank you for your response, I editted my post to show my whole code as you requested. Thank you! Please show me the way to knocking my program out. :( Commented Apr 25, 2015 at 10:39

1 Answer 1

4

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: '[email protected]',
        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: '[email protected]',
        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>

Sign up to request clarification or add additional context in comments.

13 Comments

Wow. You spotted right away how much knowledge I lack regarding Knockout that you go into full detail and explanation. Thank you so much man, you're a beast. Honestly I just jump right into it by copy-pasting examples into my program without reading the documentation. This helped me a lot Sir Tomalak, better than any jumpstart video I've watched so far. Thank you again!
Hi sir, I have a follow up question, what if for some reason, I need to access an object from a higher position in the hierarchy. For example I want to get the name from Contact viewModel and attach it to the formattedPhoneNumber in the PhoneNumber viewModel.
self.formattedPhoneNumber = ko.pureComputed(function () { return Contact().Name + ko.unwrap(self.phoneNumber); });
I assumed it should be something like this but it doesn't work
I would add that purely in the view. This works inside a foreach binding: <span data-bind="text: phoneNumber"></span> (<span data-bind="text: $parent.name"></span>). The view knows how the bindings relate to each other (it knows the binding context) and is the natural place to solve these kinds of display problems.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.