javascriptjqueryhtmlknockout.jsknockout-components

Knockout component - Apply Binding Dynamically from Child ViewModel


I have a knockout component which renders name of the Employees. In this component, I can pass callback function to change the format of the employee name displayed.

But I came into situation that I cannot change the component code. But I have to add Edit button to show modal popup to update the user information as shown in below image:

enter image description here

Some how I managed to display this Edit button using callback feature provided by component. But I am facing an issue with binding the click event of this button.

Issue is that: Even though I have defined callback function ShowModal, it is not getting called.

Here is my code:

Component Registration:

ko.components.register('emp-box', {
      viewModel: function(params) {
      var self = this;
      self.Name = ko.observable();
      self.Callback = params.formatCallback;

      if(self.Callback !== undefined)
      {
        self.Name(self.Callback(params.EmpName));
      }
      else
      {
        self.Name(params.EmpName.firstName + ', ' + params.EmpName.lastName);
      }

    },
    template: '<div class="name-container"><span data-bind="html: Name"></span></div>'
});

ViewModel

var vm = function() {
    var self = this;
    self.Emps = [{'Details': {firstName: 'First Name-A', lastName: 'Last Name-A'}},
                 {'Details': {firstName: 'First Name-B', lastName: 'Last Name-B'}}];

    // CALLBACK
    self.changeFormat = function(item)
    {
        return item.firstName + ' - ' + item.lastName + ' <span class="action" data-bind="click: ShowModal">Edit</span>';
    }

    self.ShowModal = function(item)
    {
        alert(1);
    }       
}

ko.applyBindings(new vm());

HTML:

<div data-bind='foreach: Emps'>
  <div data-bind='component: {
       name: "emp-box",
       params: {EmpName: Details, formatCallback: $root.changeFormat}
  }'></div>
</div>

Working Fiddle: Fiddle


Solution

  • As indicated in the comments, you are trying to make the view dynamic by adding more "knockout code" through the html binding. This cannot work.

    Before the html binding can even run, knockout must already have figured out the bindings, because how else would it know that it needs to run the html binding? And once the binding phase is done, it's done. Therefore you can't inject more knockout bindings that way.

    Knockout views are flexible, just add all the parts you want them to have and display them dynamically. In this case you want to have an "Edit" button that shows up on demand. That's easy with the if binding:

    <div class="name-container">
        <span data-bind="text: displayName"></span>
        <span class="action" data-bind="if: canEdit, click: onEdit">Edit</span>
    </div>
    

    Now the viewmodel that controls this needs a canEdit value and an onEdit value, boolean and function respectively. These can be controlled through params during component binding.

    When you make the displayName a computed and store the the name-formatting function in an observable, you can even switch the name format dynamically. Run the code sample below to see it in action.

    // emp-box.component.js ----------------------------------------------------
    function standardNameFormat(emp) {
        return ko.unwrap(emp.lastName) + ', ' + ko.unwrap(emp.firstName);
    }
    
    ko.components.register('emp-box', {
        viewModel: function (params) {
            this.emp = params.emp;
            this.displayName = ko.computed(function () {
                var formatter = ko.unwrap(params.formatter) || standardNameFormat;
                return formatter(params.emp);
            });
            this.canEdit = params.canEdit;
            this.onEdit = params.onEdit;
        },
        template: '<div class="name-container">\
            <span data-bind="text: displayName"></span>\
            <span class="action" data-bind="if: canEdit, click: onEdit">Edit</span>\
        </div>'
    });
    // -------------------------------------------------------------------------
    
    function EmployeeList(params) {
        var self = this;
        self.emps = ko.observableArray(params.emps);
        self.canEdit = ko.observable(false);
    
        self.firstnameLastname = function (emp) {
            return ko.unwrap(emp.firstName) + ' ' + ko.unwrap(emp.lastName);
        };
        
        self.nameFormat = ko.observable(self.firstnameLastname);
        self.showModal = function () {
            alert("You clicked on " + ko.unwrap(this.displayName));
        };
    }
    // -------------------------------------------------------------------------
    
    ko.applyBindings(new EmployeeList({
        emps: [
            {firstName: 'First Name-A', lastName: 'Last Name-A'},
            {firstName: 'First Name-B', lastName: 'Last Name-B'}
        ]
    }));
    .name-container{
      padding: 5px;
      margin: 2px;
      background-color: #cacaca;
      border: 1px solid #888;
      border-radius: 5px;
    }
    
    .name-container span {
      margin: 5px;
    }
    
    body {
      font-family: arial,sans-serif;
      font-size: 13px;
    }
    
    .action {
      cursor: pointer;
      color: blue;
      text-decoration: underline;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    
    <input type="checkbox" data-bind="checked: canEdit" id="chkEdit">
    <label for="chkEdit">Enable editing</label>
    
    <input type="radio" data-bind="value: null, checked: nameFormat" id="chkFmt1">
    <label for="chkFmt1">Default format</label>
    <input type="radio" data-bind="value: firstnameLastname, checked: nameFormat" id="chkFmt2">
    <label for="chkFmt2">Alternative format</label>
    
    <div data-bind='foreach: emps'>
      <div data-bind='component: {
           name: "emp-box",
           params: {emp: $data, formatter: $root.nameFormat, canEdit: $root.canEdit, onEdit: $root.showModal}
      }'></div>
    </div>