0

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>
Peter G.
  • 7,816
  • 20
  • 80
  • 154
  • 1
    In your error message you have `viewmodel` while binding `viewModel`. Can you search your code for `viewmodel` ? – Nikolay Ermakov Jan 19 '16 at 12:26
  • Sorry, this was a mistake while copying the error. The original error message says `viewModel`. – Peter G. Jan 19 '16 at 12:35
  • 1
    As an aside, your viewmodel method `selectRowItem: function(rowItem) {this.selectedRowItem(rowItem);}` is superfluous. Just use `click: $root.selectedRowItem`. – Tomalak Jan 19 '16 at 13:46

2 Answers2

2

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>
Roy J
  • 42,522
  • 10
  • 78
  • 102
  • Yes, that is acceptable solution. I tried to follow the example exactly, but somehow it didn't work with reference to `viewModel`. – Peter G. Jan 19 '16 at 13:00
  • There is no need to do `click: function() { $root.selectRowItem($data); }`. A simple `click: $root.selectedRowItem` is enough. – Tomalak Jan 19 '16 at 13:48
1

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>
connexo
  • 53,704
  • 14
  • 91
  • 128
  • This would normally help, but in this scenario it gives another error `Uncaught TypeError: viewModel is not a function`. The reference solution that I'm using has `ko.applyBindings(viewModel);` and it works normally. – Peter G. Jan 19 '16 at 12:46
  • Here is updated JSFiddle with the changes, error remains same in this case too https://jsfiddle.net/piglin/uun74v1j/ – Peter G. Jan 19 '16 at 12:56
  • He's not using a constructor. He's just creating an object, which is perfectly valid. – Roy J Jan 19 '16 at 12:57
  • 1
    https://jsfiddle.net/uun74v1j/1/ as stated by @Roy J, you cannot access `viewmodel` in your bindings. Instead use `$root`. – connexo Jan 19 '16 at 13:01