Contacts editor

Live example

First name Last name Phone numbers
Delete
Add number

Model

public class ContactsEditorPhoneModel
{
    public string Type { get; set; }
    public string Number { get; set; }
}

public class ContactsEditorContactModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<ContactsEditorPhoneModel> Phones { get; set; }

    public void AddPhone()
    {
        Phones.Add(new ContactsEditorPhoneModel());
    }

    public void DeletePhone(int phoneIndex)
    {
        if (phoneIndex >= 0 && phoneIndex < Phones.Count)
            Phones.RemoveAt(phoneIndex);
    }
}

public class ContactsEditorModel
{
    public List<ContactsEditorContactModel> Contacts { get; set; }
    public string LastSavedJson { get; set; }

    public void AddContact()
    {
        Contacts.Add(new ContactsEditorContactModel());
    }

    public void DeleteContact(int contactIndex)
    {
        if (contactIndex >= 0 && contactIndex < Contacts.Count)
            Contacts.RemoveAt(contactIndex);
    }

    public void AddPhone(int contactIndex)
    {
        if (contactIndex >= 0 && contactIndex < Contacts.Count)
            Contacts[contactIndex].AddPhone();
    }

    public void DeletePhone(int personIndex, int phoneIndex)
    {
        if (personIndex >= 0 && personIndex < Contacts.Count)
            Contacts[personIndex].DeletePhone(phoneIndex);
    }

    public void SaveJson()
    {
        LastSavedJson = new JavaScriptSerializer().Serialize(this);
    }
}

Razor

@using PerpetuumSoft.Knockout
@model KnockoutMvcDemo.Models.ContactsEditorModel
@{
  var ko = Html.CreateKnockoutContext();
}
<div>
  <table>
    <tr>
      <th>First name</th>
      <th>Last name</th>
      <th>Phone numbers</th>
    </tr>
    <tbody>
      @using (var contacts = ko.Foreach(m => m.Contacts))
      {
        <tr>
          <td style="vertical-align: top">
            @contacts.Html.TextBox(m => m.FirstName)
            <div>@contacts.Html.HyperlinkButton("Delete", "DeleteContact", "ContactsEditor", new { contactIndex = contacts.GetIndex() })</div>
          </td>
          <td style="vertical-align: top">
            @contacts.Html.TextBox(m => m.LastName)
          </td>
          <td>
            <table>
              <tbody>
                @using (var phones = contacts.Foreach(m => m.Phones))
                {
                  <tr>
                    <td>@phones.Html.TextBox(m => m.Type)
                    </td>
                    <td>@phones.Html.TextBox(m => m.Number)
                    </td>
                    <td>@ko.Html.HyperlinkButton("Delete", "DeletePhone", "ContactsEditor", new { contactIndex = contacts.GetIndex(), phoneIndex = phones.GetIndex() })
                    </td>
                  </tr>
                }
              </tbody>
            </table>
            @ko.Html.HyperlinkButton("Add number", "AddPhone", "ContactsEditor", new { contactIndex = contacts.GetIndex() })
          </td>
        </tr>
      }
    </tbody>
  </table>
</div>
<p>
  @ko.Html.Button("Add a contact", "AddContact", "ContactsEditor")
  @ko.Html.Button("Save to JSON", "SaveJson", "ContactsEditor").Enable(m => m.Contacts.Count > 0)
</p>
@ko.Html.TextArea(m => m.LastSavedJson, new { rows = 5, cols = 50 })
<style scoped="scoped">
  input {
    width: 120px;
  }

  textarea {
    width: 500px;
  }
</style>

@ko.Apply(Model)

Controller

public class ContactsEditorController : BaseController
{
    public ActionResult Index()
    {
        InitializeViewBag("Contacts editor");
        var model = new ContactsEditorModel();
        model.Contacts = new List<ContactsEditorContactModel>();
        model.Contacts.Add(new ContactsEditorContactModel
        {
            FirstName = "Danny",
            LastName = "LasRusso",
            Phones = new List<ContactsEditorPhoneModel>
      {
        new ContactsEditorPhoneModel {Type = "Mobile", Number = "(555) 121-2121"},
        new ContactsEditorPhoneModel {Type = "Home", Number = "(555) 123-4567"},
      }
        });
        model.Contacts.Add(new ContactsEditorContactModel
        {
            FirstName = "Sensei",
            LastName = "Miyagi",
            Phones = new List<ContactsEditorPhoneModel>
      {
        new ContactsEditorPhoneModel {Type = "Mobile", Number = "(555) 444-2222"},
        new ContactsEditorPhoneModel {Type = "Home", Number = "(555) 999-1212"},
      }
        });
        return View(model);
    }

    public ActionResult AddContact(ContactsEditorModel model)
    {
        model.AddContact();
        return Json(model);
    }

    public ActionResult DeleteContact(ContactsEditorModel model, int contactIndex)
    {
        model.DeleteContact(contactIndex);
        return Json(model);
    }

    public ActionResult AddPhone(ContactsEditorModel model, int contactIndex)
    {
        model.AddPhone(contactIndex);
        return Json(model);
    }

    public ActionResult DeletePhone(ContactsEditorModel model, int contactIndex, int phoneIndex)
    {
        model.DeletePhone(contactIndex, phoneIndex);
        return Json(model);
    }

    public ActionResult SaveJson(ContactsEditorModel model)
    {
        model.SaveJson();
        return Json(model);
    }
}

Html (autogenerated)

<div>
  <table>
    <tr>
      <th>First name</th>
      <th>Last name</th>
      <th>Phone numbers</th>
    </tr>
    <tbody>
<!-- ko foreach: Contacts -->
        <tr>
          <td style="vertical-align: top">
            <input data-bind="value : $data.FirstName" name="FirstName" type="text" />
            <div><a data-bind="click : function() {executeOnServer(viewModel, &#39;/ContactsEditor/DeleteContact?contactIndex=&#39;+$index()+&#39;&#39;);}" href="#">Delete</a></div>
          </td>
          <td style="vertical-align: top">
            <input data-bind="value : $data.LastName" name="LastName" type="text" />
          </td>
          <td>
            <table>
              <tbody>
<!-- ko foreach: $data.Phones -->
                  <tr>
                    <td><input data-bind="value : $data.Type" name="Type" type="text" />
                    </td>
                    <td><input data-bind="value : $data.Number" name="Number" type="text" />
                    </td>
                    <td><a data-bind="click : function() {executeOnServer(viewModel, &#39;/ContactsEditor/DeletePhone?contactIndex=&#39;+$parentContext.$index()+&#39;&amp;phoneIndex=&#39;+$index()+&#39;&#39;);}" href="#">Delete</a>
                    </td>
                  </tr>
<!-- /ko -->
              </tbody>
            </table>
            <a data-bind="click : function() {executeOnServer(viewModel, &#39;/ContactsEditor/AddPhone?contactIndex=&#39;+$index()+&#39;&#39;);}" href="#">Add number</a>
          </td>
        </tr>
<!-- /ko -->
    </tbody>
  </table>
</div>
<p>
  <button data-bind="click : function() {executeOnServer(viewModel, &#39;/ContactsEditor/AddContact&#39;);}">Add a contact</button>
  <button data-bind="click : function() {executeOnServer(viewModel, &#39;/ContactsEditor/SaveJson&#39;);},enable : (Contacts().length > 0)">Save to JSON</button>
</p>
<textarea cols="50" data-bind="value : LastSavedJson" rows="5"></textarea>
<style scoped="scoped">
  input {
    width: 120px;
  }

  textarea {
    width: 500px;
  }
</style>

<script type="text/javascript"> 
var viewModelJs = {"Contacts":[{"FirstName":"Danny","LastName":"LasRusso","Phones":[{"Type":"Mobile","Number":"(555) 121-2121"},{"Type":"Home","Number":"(555) 123-4567"}]},{"FirstName":"Sensei","LastName":"Miyagi","Phones":[{"Type":"Mobile","Number":"(555) 444-2222"},{"Type":"Home","Number":"(555) 999-1212"}]}],"LastSavedJson":""};
var viewModel = ko.mapping.fromJS(viewModelJs); 
ko.applyBindings(viewModel);
</script>