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 : FirstName" />
            <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 : LastName" />
          </td>
          <td>
            <table>
              <tbody>
<!-- ko foreach: Phones -->
                  <tr>
                    <td><input data-bind="value : Type" />
                    </td>
                    <td><input data-bind="value : Number" />
                    </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>