1

I have simple FullName viewcomponent with knockout viewmodel inside:

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="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>

<p>Full name: <strong data-bind="text: fullName"></strong></p>

<script>
function AppViewModel() {
    this.firstName = ko.observable("");
    this.lastName = ko.observable("");

    this.fullName = ko.computed(function() {
        return this.firstName() + " " + this.lastName();    
    }, this);
}

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

When I invoke this component, like @(await Component.InvokeAsync("FullName")), it works great.

However, when I'm trying to invoke this component multiple times:

@(await Component.InvokeAsync("FullName"))
@(await Component.InvokeAsync("FullName"))
@(await Component.InvokeAsync("FullName"))
//etc...

I have an error, that Knockout can't apply multiple same bindings.

So, how can I include multiple ViewComponents with Knockout viewmodels into one page?

Yurii N.
  • 5,455
  • 12
  • 42
  • 66
  • In knockout you can't apply binding again to the `same` element.if you really want to do it ,remove the bindings before you use 'applyBindings' again. `ko.cleanNode($element)`. – Matin Kajabadi Oct 07 '16 at 17:09

1 Answers1

2

In your code, you are calling applyBindings once per component you add, and Knockout wont let you bind something more than once unless you first remove the bindings(as per Matt.kaaj's comment).

That being said, I think you should look at scoping your bindings if you want to reuse your viewmodel in multiple places.

Full disclosure - I do not know much about asp.net and it's ViewComponents, so apologies if the syntax is incorrect, but it seems like they support passing in parameters. It looks like you could fix this by changing your component definition to something like:

<div id=${idThatIPassIn}>
<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="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>

<p>Full name: <strong data-bind="text: fullName"></strong></p>
</div>
<script>
function AppViewModel() {
    this.firstName = ko.observable("");
    this.lastName = ko.observable("");

    this.fullName = ko.computed(function() {
        return this.firstName() + " " + this.lastName();    
    }, this);
}

ko.applyBindings(new AppViewModel(), document.getElementById('${idThatIPassIn}'));
</script>

And then initialize with:

@(await Component.InvokeAsync("FullName"), new { idThatIPassIn = "name-1" })
@(await Component.InvokeAsync("FullName"), new { idThatIPassIn = "name-2" })
@(await Component.InvokeAsync("FullName"), new { idThatIPassIn = "name-3" })

This way, each time you tell knockout to apply bindings, the binding is contextualized to a single div that wraps your <p> elements, so you wont end up trying to re-bind something that's already been bound.

Alternatively, instead of passing in an ID, you could assign a random arbitrary ID and bind to that instead. In either case, scope your applyBindings call to an element.

Edit - You mentioned not wanting to bind by IDs. Instead try using a class, and jQuery's .each method. Something like:

<div class="name-block">
<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="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>

<p>Full name: <strong data-bind="text: fullName"></strong></p>
</div>

initialized as you currently have it:

@(await Component.InvokeAsync("FullName"))
@(await Component.InvokeAsync("FullName"))
@(await Component.InvokeAsync("FullName"))

Then in another separate .js include, add:

<script>
//I'm assuming that jQuery is available
$(document).ready(function(){

  function AppViewModel() {
      this.firstName = ko.observable("");
      this.lastName = ko.observable("");

      this.fullName = ko.computed(function() {
          return this.firstName() + " " + this.lastName();    
      }, this);
  }


  $(".name-block").each(function(index, obj){
      ko.applyBindings(new AppViewModel(), obj);
  });
});
</script>

This way, your viewmodel is only defined once, and your bindings are applied for each individual div that has the class "name-block".

I also wrapped the entire process in jQuery's document ready function, so you can safely assume that the DOM is ready to manipulate. Just make sure your script is included in your page as well, as now it's separated from the view component.

Community
  • 1
  • 1
Nick DeFazio
  • 2,412
  • 27
  • 31
  • I've been thinking about this way, but it requires a lot of auxiliary code, for creating unique ids, that's the way, which I want omit. – Yurii N. Oct 07 '16 at 21:32
  • So if I'm understanding you correctly, you don't want to manage unique IDs assigned to your divs? If that's the case, how about a class and then applying bindings to each class individually? – Nick DeFazio Oct 07 '16 at 21:46
  • Yes, you understand correctly. Could you please provide an example of applying bindings to each class? But if we classes, they're not individual, seems we'll have the same error with multiple viewmodels, won't we? – Yurii N. Oct 07 '16 at 22:18
  • @YuriyN. sure. I'm assuming you have jQuery available, as it will make things like iterating over classes and checking for dom readiness much easier. – Nick DeFazio Oct 08 '16 at 01:54
  • Excellent, that's what I need! Summing up, I need to define my `ViewComponent` with class div in it, then on the page, where this viewcomponent invokes, I include bindings javascript. Am I right? – Yurii N. Oct 08 '16 at 09:47