1
votes

I have simple HTML markup with a list of my viewmodels in it and template. Also, I have my viewmodel in <pre> section:

<div class="container">
  <div class="row">
    <div class="col-md-6">
      <div class="col-md-12">
        <div class="panel panel-primary">
          <div class="panel-heading">
            Names
            <button type="button" class="btn btn-link" data-bind="click: addName">
              <span class="glyphicon glyphicon-plus"></span>
            </button>
          </div>
          <div class="panel-body">
            <ul class="list-group" id="namesList" data-bind="foreach: Names">
              <li class="list-group-item"><my-comp></my-comp></li>
            </ul>
          </div>
        </div>
      </div>
    </div>
    <div class="col-md-6">
      <div class="col-md-12">
        <pre data-bind="text: ko.toJSON(Names, null, 2)"></pre>
      </div>
    </div>
  </div>
</div>
<script type="text/html" id="fullNameTmpl">
  <p>First name: <strong data-bind="text: firstName"></strong></p>
  <p>Last name: <strong data-bind="text: lastName"></strong></p>

  <p>First name: <input data-bind="textInput: firstName" /></p>
  <p>Last name: <input data-bind="textInput: lastName" /></p>

  <p>Full name: <input data-bind="textInput: fullName"></p>

  <button data-bind="click: capitalizeLastName">Go caps</button>
</script>
<script>
  function AppViewModel() {
    var self = this;
    self.firstName = ko.observable("Bert");
    self.lastName = ko.observable("Bertington");

    this.fullName = ko.pureComputed({
      read: function () {
        return self.firstName() + " " + self.lastName();
      },
      write: function (value) {
        var lastSpacePos = value.lastIndexOf(" ");
        if (lastSpacePos > 0) { 
          self.firstName(value.substring(0, lastSpacePos)); 
          self.lastName(value.substring(lastSpacePos + 1)); 
        }
      },
      owner: self
    });

    self.capitalizeLastName = function () {
      var currentVal = this.lastName();        
      this.lastName(currentVal.toUpperCase()); 
    };
  }
  ko.components.register('my-comp', {
    viewModel: AppViewModel,
    template: { element: "fullNameTmpl" }
  });

  function NamesViewModel() {
    var self = this;

    self.Names = ko.observableArray();

    self.addName = function () {
      var vm = ko.observable(new AppViewModel());
      self.Names.push(vm);
    }
  }

  ko.applyBindings(new NamesViewModel());
</script>

It works good, I can add an item to list and data binding in these items works. When I add new AppViewModel, I can see it in <pre> section, in Names array. The problem is that when I'm changing some values in nested viewmodel, like typing new last name, whatever, there are no changes in <pre> section in current viewmodel in Names array.

How can I make a truly observable array of observables? What's the most profitable way to add components like this? I've already done nested viewmodel observable, but it seems doesn't work.

1
I think the issue here is that knockout observableArray treats the array itself as obversable, notifying on events like push and remove, but will not notify if a single property of an item in the array changes. I'm pretty sure you will be left to implement that yourself (there's a "key point" in the docs that very breifly mention this - knockoutjs.com/documentation/observableArrays.html)Nick DeFazio
@NickDeFazio but in addName method, I add ko.observable, doesn't this enough?Yurii N.

1 Answers

1
votes

There a couple of issues with your code. The problem is not necessarily with the observable array - it is an issue with your use of a component. The purpose of components are listed here. Some things to note:

  1. observable array elements do not need to be made observable. So no need to do var vm = ko.observable(new AppViewModel()); Simply use var vm = new AppViewModel(); and push onto your array. However if you want to use a component then this isnt correct either! What are are doing is basically newing up AppViewModels twice! Can you see why? You do it once explicitly in the line above and then component does it once implicitly. Each time a component is created it is associated with AppViewModel - this is what happens when you register the component.
  2. component are associated with you can think of as a mini-viewModel with its own properties, functionality, computeds, etc. So in your case you probably dont need to use a component -> copy and paste the internals of your <script></script> tags into the <li><li> and it should just work.

Im not sure if this is the answer you are looking for, but this should do what you want. You can use components if you like but youd have to make more changes and tiny bit of unnecessary complexity. I took the liberty of changing your variable names. I think they might be more appropriate this way. Hope this helps!

Edit

I've edited the code snippet to work with a component. The problem with this specific implementation is that the component is now tightly coupled with the AppViewModel through the names observableArray. This might be okay in your case, but often this can lead to undesired behaviour, especially once your view models start getting larger and more complex. Of course you could pass in the unwrapped values of firstName and lastName in order to be better decoupled but then you'd be back at square one because <pre data-bind="text: ko.toJSON(names, null, 2)"></pre> will not register any changes made to the names observableArray.

 ko.components.register('my-comp', {
   viewModel: function(params) {
     var self = this;
     self.firstName = params.data.firstName
     self.lastName = params.data.lastName
     self.fullName = ko.pureComputed({
       read: function() {
         return self.firstName() + " " + self.lastName();
       },
       write: function(value) {
         var lastSpacePos = value.lastIndexOf(" ");
         if (lastSpacePos > 0) {
           self.firstName(value.substring(0, lastSpacePos));
           self.lastName(value.substring(lastSpacePos + 1));
         }
       },
       owner: self
     });

     self.capitalizeLastName = function() {
       var str = self.lastName();
       self.lastName(str.toUpperCase());
     };
   },
   template: {
     element: "fullNameTmpl"
   }
 });

var Name = function(){
  this.firstName = ko.observable("Bert");
  this.lastName = ko.observable("McBertington");
}


 function AppViewModel() {
   var self = this;
   self.names = ko.observableArray();
   self.addName = function() {
     self.names.push(new Name());
   }
 }


 ko.applyBindings(new AppViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

Names
<button type="button" class="btn btn-link" data-bind="click: addName">
  Add Dude
</button>

<ul class="list-group" id="namesList" data-bind="foreach: names">
  <my-comp params="data: $data"></my-comp>
  <!--<li class="list-group-item">
    <p>First name: <strong data-bind="text: firstName"></strong></p>
    <p>Last name: <strong data-bind="text: lastName"></strong></p>
    <p>First name: <input data-bind="textInput: firstName" /></p>
    <p>Last name: <input data-bind="textInput: lastName" /></p>
    <p>Full name: <input data-bind="textInput: fullName"></p>
    <button data-bind="click: capitalizeLastName">Go caps</button>
  </li>-->
</ul>


<pre data-bind="text: ko.toJSON(names, null, 2)"></pre>

<script type="text/html" id="fullNameTmpl">
  <li>
    <p>First name: <strong data-bind="text: firstName"></strong></p>
    <p>Last name: <strong data-bind="text: lastName"></strong></p>
    <p>First name: <input data-bind="textInput: firstName" /></p>
    <p>Last name: <input data-bind="textInput: lastName" /></p>
    <p>Full name: <input data-bind="textInput: fullName"></p>
    <button data-bind="click: capitalizeLastName">Go caps</button>
  </li>
</script>