1

I am using knockout (v3.2.0) and jQuery (v1.12.4) which are referenced in my _Layout.cshtml page. I have a function called cloneActivity in the javascript section of the ManageActivities.cshtml page which should be triggered from the button markup pasted below.

<script type="text/javascript">
  $(function () {
    var employeeId = @ViewBag.UserInfo.UserId;

    function ActivityViewModel() {
      var self = this;
      self.ActivityHistoryId = ko.observable("");
      self.CompanyId = ko.observable("");
      self.UserId = ko.observable("");
      self.WorkFlowId = ko.observable("");
      self.ActivityName = ko.observable("");
      self.ActivityDescription = ko.observable("");
      self.Status = ko.observable("");

      var Activity = {
        ActivityHistoryId: self.ActivityHistoryId,
        CompanyId: self.CompanyId,
        UserId: self.UserId,
        WorkFlowId: self.WorkFlowId,
        ActivityName: self.ActivityName,
        ActivityDescription: self.ActivityDescription,
        Status: self.Status
      };

    self.Activity = ko.observable();
    self.ActivitiesArray = ko.observableArray();

    self.cloneActivity = function () {
      console.log('clone row clicked');
    };
  };

  var activityViewModel = new ActivityViewModel();
  ko.applyBindings(activityViewModel);
});
</script>

HTML button markup

<button class="btn btn-xs btn-success" title="clone work activity" name="cloneWork" data-toggle="modal" data-target="#addWorkInModal" data-bind="click: $root.cloneActivity"><i class="fa fa-clone"></i></button>

I don't see any errors and the function is not called. I have tried $parent, $parents[1], self, $root and nothing before the button click. IF I move the function outside of the ActivityViewModel and change the button to have an onclick="cloneActivity();" I can see the message in the console.

Not sure what I could be missing as I have this same concept working in other places within my application. Any help is greatly appreciated.

The markup for my table....

<div class="panel-body">
  <table id="master" class="table table-striped table-hover table-condensed" cellspacing="0">
    <thead>
      <tr>
        <th class="rpt_col_bg_head" style="width: 3%;"></th>
        <th class="rpt_col_bg_head" style="width: 20%;">Result</th>
        <th class="rpt_col_bg_detail" style="width: 20%;">Work Activity</th>
        <th class="rpt_col_bg_detail" style="width: 180px;">Effort(%)</th>
        <th class="rpt_col_bg_detail" style="width: 7%;">Status</th>
        <th class="rpt_col_bg_detail" style="width: 30%;">Were there any innovations</th>
        <th class="rpt_col_bg_detail text-center" style="width: 7%;">Action</th>
     </tr>
    </thead>
  </table>
</div>

The ajax call that returns the data from the server looks like this:

 var table = $('#master').DataTable({
   ajax : {
     type : "POST",
     url : "@Url.Action("GetAllActivities", "Activities")",
     data : { "UserId" : employeeId }
   },
   columns : [
     {  className : "details-control", orderable : false, defaultContent: "" },
     {  orderable : false, data : "ParentName" },
     {  orderable : false, data : "ActivityName" },
     {  orderable : false, data : "EffortHtml" },
     {  orderable : false, data : "Status" },
     {  orderable : false, data : "Innovation" },
     {  orderable : false, data : "ActionButtons" }
   ],
   columnDefs : [{
     targets: [6],  //disable search and sort on Actions column
     searchable : false,
     orderable : false
   }]
});  

I capture xhr to save the returned JSON to my view model that knockout is observing.

table.on('xhr', function () {
  var json = table.ajax.json();
  self.ActivitiesArray.push(json.data);  // Initialize the view-model
  console.log(ko.toJSON(self.ActivitiesArray));
});    

Here is the returned JSON with the markup for the buttons that have IDs attached to them.

[[{"ActivityHistoryId":1,"UserId":91,"WorkFlowId":4,"ActivityName":"Test Activity 1","ActivityDescription":"Description of Test Activity 1","Status":"<span class='badge badge-blue'>Not Started</span>","Effort":25,"EffortHtml":"<span id='originalEffort1'>25</span><button class='btn btn-xs text-primary' style='background: none; margin-bottom: 5px;' title='edit effort' onclick='showEdit(25);'><i class='fa fa-pencil'></i></button><section id='editEffort25' class='edit bg-warning edit-box' style='display: none;'><div style='margin-top: 5px;'><label class='text-muted'>Adjust Effort(%) </label><input type='number' min='1' max='200' class='effort' id='adjustedEffort1' style='width: 100%;'></div><small id='enterEffortMsg1' class='error-effort text-danger display: none;'></small><div style='margin-top: 5px;'><label class='text-muted'>Total Annual Effort(%) </label><input type='number' class='total_effort text-muted' id='totalEffort25' value='' disabled='' style='width: 100%;'></div><div class='total_after_adjust text-danger' style='margin-top: 15px;'></div><div id='totalAfterAdjustmentDiv1' class='total_after_adjust text-danger' style='margin - top: 15px; display: none;'><label id='totalAfterAdjustment1'><small></small><br></label></div><div style='margin-top: 5px;' class='text-center'><button onclick='checkEditEffort(1);' class='btn btn-info btn-xs' title='check total annual effort after adjust'>Check</button><button onclick='saveEditEffort(1);' class='btn btn-primary btn-xs' style='margin-left: 5px;'>Save</button><button onclick='showEdit(25);' class='btn btn-default btn-xs' style='margin-left: 5px;'>Cancel</button></div><div id='saveSuccess1' class='alert alert-success' style='display:none; margin-top: 10px; margin-bottom: 0;'><i class='fa fa-check'> Update Success!</i></div><div class='alert alert-danger' style='display:none; margin-top: 10px; margin-bottom: 0;'></div></section>","Innovation":false,"ParentId":2,"ParentName":"Test Result 1","ActionButtons":"<td><div class='text-center'><ul class='list-inline'><li><button class='btn btn-xs btn-primary' title='edit work activity' name='editWork' onclick='editActivity(1);'><i class='fa fa-pencil'></i></button></li><li><button class='btn btn-xs btn-danger' title='delete work activity' name='deleteWork' data-toggle='modal' data-target='#confirm_modal' onclick='deleteActivity(1);'><i class='fa fa-remove'></i></button></li><li><button class='btn btn-xs btn-success' title='clone work activity' name='cloneWork' data-bind='click: $root.cloneActivity'><i class='fa fa-clone'></i></button></li></ul></div></td>"},{"ActivityHistoryId":2,"UserId":91,"WorkFlowId":4,"ActivityName":"Test Activity 2","ActivityDescription":"Description of Test Activity 2","Status":"<span class='badge badge-blue'>Not Started</span>","Effort":9,"EffortHtml":"<span id='originalEffort2'>9</span><button class='btn btn-xs text-primary' style='background: none; margin-bottom: 5px;' title='edit effort' onclick='showEdit(9);'><i class='fa fa-pencil'></i></button><section id='editEffort9' class='edit bg-warning edit-box' style='display: none;'><div style='margin-top: 5px;'><label class='text-muted'>Adjust Effort(%) </label><input type='number' min='1' max='200' class='effort' id='adjustedEffort2' style='width: 100%;'></div><small id='enterEffortMsg2' class='error-effort text-danger display: none;'></small><div style='margin-top: 5px;'><label class='text-muted'>Total Annual Effort(%) </label><input type='number' class='total_effort text-muted' id='totalEffort9' value='' disabled='' style='width: 100%;'></div><div class='total_after_adjust text-danger' style='margin-top: 15px;'></div><div id='totalAfterAdjustmentDiv2' class='total_after_adjust text-danger' style='margin - top: 15px; display: none;'><label id='totalAfterAdjustment2'><small></small><br></label></div><div style='margin-top: 5px;' class='text-center'><button onclick='checkEditEffort(2);' class='btn btn-info btn-xs' title='check total annual effort after adjust'>Check</button><button onclick='saveEditEffort(2);' class='btn btn-primary btn-xs' style='margin-left: 5px;'>Save</button><button onclick='showEdit(9);' class='btn btn-default btn-xs' style='margin-left: 5px;'>Cancel</button></div><div id='saveSuccess2' class='alert alert-success' style='display:none; margin-top: 10px; margin-bottom: 0;'><i class='fa fa-check'> Update Success!</i></div><div class='alert alert-danger' style='display:none; margin-top: 10px; margin-bottom: 0;'></div></section>","Innovation":false,"ParentId":2,"ParentName":"Test Result 1","ActionButtons":"<td><div class='text-center'><ul class='list-inline'><li><button class='btn btn-xs btn-primary' title='edit work activity' name='editWork' onclick='editActivity(2);'><i class='fa fa-pencil'></i></button></li><li><button class='btn btn-xs btn-danger' title='delete work activity' name='deleteWork' data-toggle='modal' data-target='#confirm_modal' onclick='deleteActivity(2);'><i class='fa fa-remove'></i></button></li><li><button class='btn btn-xs btn-success' title='clone work activity' name='cloneWork' data-bind='click: $root.cloneActivity'><i class='fa fa-clone'></i></button></li></ul></div></td>"},{"ActivityHistoryId":3,"UserId":91,"WorkFlowId":4,"ActivityName":"Test Activity 3","ActivityDescription":"Description of Test Activity 3","Status":"<span class='badge badge-lightBlue'>In Progress</span>","Effort":12,"EffortHtml":"<span id='originalEffort3'>12</span><button class='btn btn-xs text-primary' style='background: none; margin-bottom: 5px;' title='edit effort' onclick='showEdit(12);'><i class='fa fa-pencil'></i></button><section id='editEffort12' class='edit bg-warning edit-box' style='display: none;'><div style='margin-top: 5px;'><label class='text-muted'>Adjust Effort(%) </label><input type='number' min='1' max='200' class='effort' id='adjustedEffort3' style='width: 100%;'></div><small id='enterEffortMsg3' class='error-effort text-danger display: none;'></small><div style='margin-top: 5px;'><label class='text-muted'>Total Annual Effort(%) </label><input type='number' class='total_effort text-muted' id='totalEffort12' value='' disabled='' style='width: 100%;'></div><div class='total_after_adjust text-danger' style='margin-top: 15px;'></div><div id='totalAfterAdjustmentDiv3' class='total_after_adjust text-danger' style='margin - top: 15px; display: none;'><label id='totalAfterAdjustment3'><small></small><br></label></div><div style='margin-top: 5px;' class='text-center'><button onclick='checkEditEffort(3);' class='btn btn-info btn-xs' title='check total annual effort after adjust'>Check</button><button onclick='saveEditEffort(3);' class='btn btn-primary btn-xs' style='margin-left: 5px;'>Save</button><button onclick='showEdit(12);' class='btn btn-default btn-xs' style='margin-left: 5px;'>Cancel</button></div><div id='saveSuccess3' class='alert alert-success' style='display:none; margin-top: 10px; margin-bottom: 0;'><i class='fa fa-check'> Update Success!</i></div><div class='alert alert-danger' style='display:none; margin-top: 10px; margin-bottom: 0;'></div></section>","Innovation":true,"ParentId":5,"ParentName":"Test Result 2","ActionButtons":"<td><div class='text-center'><ul class='list-inline'><li><button class='btn btn-xs btn-primary' title='edit work activity' name='editWork' onclick='editActivity(3);'><i class='fa fa-pencil'></i></button></li><li><button class='btn btn-xs btn-danger' title='delete work activity' name='deleteWork' data-toggle='modal' data-target='#confirm_modal' onclick='deleteActivity(3);'><i class='fa fa-remove'></i></button></li><li><button class='btn btn-xs btn-success' title='clone work activity' name='cloneWork' data-bind='click: $root.cloneActivity'><i class='fa fa-clone'></i></button></li></ul></div></td>"}]]  
Jason Spake
  • 4,224
  • 2
  • 14
  • 22
Brian Evans
  • 215
  • 3
  • 17
  • Did you make sure it's not the Bootstrap modal plugin that's preventing the `click`? In other words, did you try temporarily removing `data-toggle` and `data-target`? – haim770 Feb 16 '17 at 16:55
  • @haim770, I tried that but no change. – Brian Evans Feb 16 '17 at 16:57
  • What other knockout context manipulation is being done in your markup? In other words is the button you've shown within a "foreach" or a "with" binding? – Jason Spake Feb 16 '17 at 16:58
  • @JasonSpake, The button is generated from a server side call that returns JSON with markup in it for each row in the model. No real manipulation of the view model is being done. I am just placing the results of the ajax call in the observable array for use later on. – Brian Evans Feb 16 '17 at 17:02
  • @BrianEvans, If the ` – haim770 Feb 16 '17 at 17:05
  • Updated the question with finer level details. The button is created as part of the JSON that is returned from the ajax call that is used to populate the observable array. – Brian Evans Feb 16 '17 at 17:12
  • You'll have to call `ko.cleanNode($('#master')[0])` then `ko.applyBindings(self, $('#master')[0])` at the end of your `on('xhr')` function – haim770 Feb 16 '17 at 17:30
  • Well I can't find anything wrong with the code you've provided. I did my best to recreate your setup in a jsfiddle and it... works. I had to modify the ajax call of course and made some assumptions about the structure of the returned data, but otherwise it's pretty much the same. Even used the version of jQuery and knockout you specified. https://jsfiddle.net/jlspake/k650d75p/ – Jason Spake Feb 16 '17 at 20:38

2 Answers2

0

you need to call applyBindings once the table render complete.

you can use init.dt event instead of xhr

table.on('init.dt', function (e, settings, json) {
  self.ActivitiesArray.push(json.data);  // Initialize the view-model
  console.log(ko.toJSON(self.ActivitiesArray));

  // call bindings here
  ko.applyBindings(activityViewModel);
}); 

EDIT:

if your table depends on activityViewModel, then you can keep the applyBindings in main script function and clean the dom before reapplying

table.on('init.dt', function (e, settings, json) {
  self.ActivitiesArray.push(json.data);  // Initialize the view-model
  console.log(ko.toJSON(self.ActivitiesArray));

  // clean the dom since we already applied bindings
  ko.cleanNode(document.body);

  // re apply bindings here
  ko.applyBindings(activityViewModel);
}); 
Ja9ad335h
  • 4,405
  • 2
  • 16
  • 23
  • Re-calling `applyBindings()` without `cleanNode()` will result in error – haim770 Feb 16 '17 at 17:39
  • Yes, I tried all the above but nothing resulted in getting any further along. – Brian Evans Feb 16 '17 at 17:40
  • @haim770 i mean only call `applyBindings` once after `init` – Ja9ad335h Feb 16 '17 at 17:41
  • I am applying bindings at the end of document ready. If I call them in the table.on I will get an error stating multiple bindings cannot be applied. – Brian Evans Feb 16 '17 at 17:52
  • I tried the approach above and I get an error stating that I cannot apply multiple bindings to the same element. IF I remove the bindings at the end of the function the table is not rendered with any data. If I remove the applyBindings from the function above and leave in place the one I am using originally I see my data but the button still does not work. – Brian Evans Feb 16 '17 at 19:01
  • @Jag, I tried your edited approach. Problem is that if I use your exact code the table doesn't get any data. – Brian Evans Feb 17 '17 at 17:45
  • @Jag, the line: ko.cleanNode(document.body); actually causes the table to not work anymore. No if I prefix activityViewModel with self I get a different error: Unable to process binding "click: function (){return $root.cloneActivity }" Message: Cannot read property 'cloneActivity' of undefined – Brian Evans Feb 17 '17 at 17:49
  • This worked --> table.on('init.dt', function (e, settings, json) { var json = table.ajax.json(); self.ActivitiesArray.push(json.data); // Initialize the view-model ko.applyBindings(activityViewModel); }); – Brian Evans Feb 17 '17 at 18:20
  • And I just have $(function() { ///other stuff var activityViewModel = new ActivityViewModel(); }); – Brian Evans Feb 17 '17 at 18:23
  • I now see data-bind='click: $root.cloneActivity();' fire, but it automatically fires when the page loads before I clicked any button. It also works when I do click the button. – Brian Evans Feb 17 '17 at 18:24
0

You're not invoking the IIFE and you have preceded it with the jQuery $ symbol. It should have the form below. Or you can omit the jQuery declaration since you're not using it inside the IIFE.

(function($){
    // stuff
})(jQuery);

Your code is missing the last pair of parentheses to invoke the expression.


I wasn't sure how the $(function(){}); construct behaves but it turns out to be a shortcut for calling $(document).ready(function(){}); I don't use that shortcut and would structure the code like this:

_Layout.cshtml before the closing body tag:

// load jQuery and knockoutJS here
@RenderSection("scripts", required: false)

On your page:

@section scripts
{
    <script>
        (function($, ko) {

            // stuff

            $(document).ready(function() {
                var activityViewModel = new ActivityViewModel();
                ko.applyBindings(activityViewModel);
            });

        })(jQuery, ko);
    </script>
}
Community
  • 1
  • 1
Jamie Ide
  • 45,803
  • 16
  • 74
  • 115
  • I don't know about this. The page has no other issues; it is just the data-bind click that is not called and no error is displayed. Can you elaborate with a more completed example? – Brian Evans Feb 16 '17 at 18:53
  • When I make this change I get an error: ko is not defined. – Brian Evans Feb 16 '17 at 18:57
  • The error means that the IIFE is now being executed. It's likely that the problem now is that your script is being executed before the knockout library is loaded. – Jamie Ide Feb 16 '17 at 19:14