Knockout: viewModel is not defined

6.8k Views Asked by At

When using this JSFiddle as a reference to build a grid with organizable contents I ran into a problem. An error message says Error: viewModel is not defined.

It should normally work as in the example, since my viewModel is defined on the first line in JS. It may have something to do with requesting viewModel from within the template.

When checking other answers, they all were too generic. I didn't find one that answers one that solves my problem.

The full error:

Uncaught ReferenceError: Unable to process binding "template: function (){return { name:'gridTmpl',foreach:gridItems,templateOptions:{ parentList:gridItems}} }"
Message: Unable to process binding "template: function (){return { name:'rowTmpl',foreach:rowItems,templateOptions:{ parentList:rowItems}} }"
Message: Unable to process binding "visible: function (){return $data !== viewModel.selectedRowItem() }"
Message: viewModel is not defined

var viewModel = {
  gridItems: ko.observableArray(
    [{
      "rowItems": [{
        "name": "Item 1"
      }, {
        "name": "Item 2"
      }, {
        "name": "Item 3"
      }]
    }, {
      "rowItems": [{
        "name": "Item 4"
      }]
    }, {
      "rowItems": [{
        "name": "Item 5"
      }, {
        "name": "Item 6"
      }]
    }]
  ),
  selectedRowItem: ko.observable(),
  selectRowItem: function(rowItem) {
    this.selectedRowItem(rowItem);
  }
};

//connect items with observableArrays
ko.bindingHandlers.sortableList = {
  init: function(element, valueAccessor, allBindingsAccessor, context) {
    $(element).data("sortList", valueAccessor()); //attach meta-data
    $(element).sortable({
      update: function(event, ui) {
        var item = ui.item.data("sortItem");
        if (item) {
          //identify parents
          var originalParent = ui.item.data("parentList");
          var newParent = ui.item.parent().data("sortList");
          //figure out its new position
          var position = ko.utils.arrayIndexOf(ui.item.parent().children(), ui.item[0]);
          if (position >= 0) {
            originalParent.remove(item);
            newParent.splice(position, 0, item);
          }

          ui.item.remove();
        }
      },
      connectWith: '.container'
    });
  }
};

//attach meta-data
ko.bindingHandlers.sortableItem = {
  init: function(element, valueAccessor) {
    var options = valueAccessor();
    $(element).data("sortItem", options.item);
    $(element).data("parentList", options.parentList);
  }
};

//control visibility, give element focus, and select the contents (in order)
ko.bindingHandlers.visibleAndSelect = {
  update: function(element, valueAccessor) {
    ko.bindingHandlers.visible.update(element, valueAccessor);
    if (valueAccessor()) {
      setTimeout(function() {
        $(element).focus().select();
      }, 0); //new RowItems are not in DOM yet
    }
  }
}

ko.applyBindings(viewModel);
.sortable {
  list-style-type: none;
  margin: 0;
  padding: 0;
  width: 60%;
}

.sortable li {
  margin: 0 3px 3px 3px;
  padding: 0.4em;
  padding-left: 1.5em;
  font-size: 1.4em;
  height: 18px;
  cursor: move;
}

.sortable li span {
  position: absolute;
  margin-left: -1.3em;
}

.sortable li.fixed {
  cursor: default;
  color: #959595;
  opacity: 0.5;
}

.sortable-grid {
  width: 100% !important;
}

.sortable-row {
  height: 100% !important;
  padding: 0 !important;
  margin: 0 !important;
  display: block !important;
}

.sortable-item {
  border: 1px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://code.jquery.com/ui/1.12.0-beta.1/themes/smoothness/jquery-ui.css" rel="stylesheet" />
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<ul class="sortable sortable-grid" data-bind="template: { name: 'gridTmpl', foreach: gridItems, templateOptions: { parentList: gridItems} }, sortableList: gridItems">
</ul>

<script id="gridTmpl" type="text/html">
  <li class="sortable-row">
    <table style="width:100%">
      <tbody>
        <tr class="sortable container" data-bind="template: { name: 'rowTmpl', foreach: rowItems, templateOptions: { parentList: rowItems} }, sortableList: rowItems">
        </tr>
      </tbody>
    </table>
  </li>
</script>

<script id="rowTmpl" type="text/html">
  <td class="item" data-bind="sortableItem: { item: $data, parentList: $item.parentList }">
    <a href="#" data-bind="text: name, click: function() { viewModel.selectRowItem($data); }, visible: $data !== viewModel.selectedRowItem()"></a>
    <input data-bind="value: name, visibleAndSelect: $data === viewModel.selectedRowItem()" />
  </td>
</script>

2

There are 2 best solutions below

2
Roy J On BEST ANSWER

viewModel isn't defined in your viewmodel. When you call ko.applyBindings(viewModel); the name viewModel isn't carried in to be used in your binding references; I think you want $root instead. You should be able to do:

  <td class="item" data-bind="sortableItem: { item: $data, parentList: $item.parentList }">
    <a href="#" data-bind="text: name, click: function() { $root.selectRowItem($data); }, visible: $data !== $root.selectedRowItem()"></a>
    <input data-bind="value: name, visibleAndSelect: $data === $root.selectedRowItem()" />
  </td>
4
connexo On

You need to instantiate your viewmodel when applying bindings, otherwise the constructor is not called. This is why it needs to be a function, not an object.

ko.applyBindings(new viewModel());

This is how your viewModel has to be changed:

var viewModel = function() {
    var self = this;

    self.gridItems= ko.observableArray(
        [{
          "rowItems": [{
            "name": "Item 1"
           }, {
            "name": "Item 2"
           }, {
            "name": "Item 3"
           }
          ]
       }, {
          "rowItems": [{
            "name": "Item 4"
          }]
       }, {
          "rowItems": [{
            "name": "Item 5"
          }, {
            "name": "Item 6"
          }]
       }]
    );
    self.selectedRowItem = ko.observable();
    self.selectRowItem = function(rowItem) {
        this.selectedRowItem(rowItem);
    };
};

Viewmodel properties are not meant to be changed or read from outside the viewmodel. To access your properties in your viewmodel, you cannot use viewmodel since it is not known in KO bindings context. You cann access those prefixing $root:

<script id="rowTmpl" type="text/html">
  <td class="item" data-bind="sortableItem: { item: $data, parentList: $data.parentList }">
    <a href="#" data-bind="text: name, click: function() { $root.selectRowItem($data); }, visible: $data !== $root.selectedRowItem()"></a>
    <input data-bind="value: name, visibleAndSelect: $data === $root.selectedRowItem()" />
  </td>
</script>