raben
raben

Reputation: 3110

Dynamically add node to a list with knockoutjs

I have a nested HTML list and I need to dynamically load new item when I click to expand one node. I found and modified this snippet but when I try to push new nodes in my list I got an error:

Uncaught ReferenceError: Unable to parse bindings.
Bindings value: visible: expanded, click: toggle
Message: toggle is not defined

HTML

<!DOCTYPE html>
<html>

<head>
  <link data-require="font-awesome@*" data-semver="3.0.2" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/3.0.2/css/font-awesome.min.css" />
  <script data-require="jquery@*" data-semver="2.0.3" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
  <script data-require="knockout@*" data-semver="2.3.0" src="http://knockoutjs.com/downloads/knockout-2.3.0.js"></script>
  <script data-require="knockout.mapping@*" data-semver="2.3.5" src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.3.5/knockout.mapping.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
  <script type="text/html" id="tree-node">
    <li>
      <span class="node-toggle" data-bind="visible: expanded, click: toggle">&ndash;</span>
      <span class="node-toggle" data-bind="visible: collapsed, click: toggle">+</span>
      <span class="node-label" data-bind="text: name, click: $root.selected"></span>

      <div data-bind="if: expanded">
        <ul data-bind="template: {name: 'tree-node', foreach: children}"></ul>
      </div>
    </li>
  </script>
</head>

<body>
  <h1>Example Tree</h1>
  <p>Click on the plus sign to expand a node, click on the label to select it.</p>
  <ul data-bind="template: {name: 'tree-node', data: root}"></ul>
  <div data-bind="if: selected">
    <div data-bind="with: selected">
      Selected Node: <span data-bind="text: name"></span>
    </div>
  </div>
</body>

</html>

JS

// Code goes here
$(function() {
  function TreeNode(values) {
      var self = this;
      ko.mapping.fromJS(values, { children: { create: createNode }}, this);
      this.expanded = ko.observable(false);
      this.collapsed = ko.computed(function() {
      return !self.expanded();
    })
  }



  TreeNode.prototype.toggle = function () {
      this.expanded(!this.expanded());
      if (this.expanded() && !this.children().length){
        // This throw an error
        this.children.push(TreeNode(
          {id: "xxxx", name: "Node xxxx", children: []}
          ));
      } 
  };

  function createNode(options) {
      return new TreeNode(options.data);
  }

  var root = new TreeNode({ id: "1", name: "Root", children: [
      { id: "1.1", name: "Node 1", children: [
          {id: "1.1.1", name: "Node 1.1", children: []},
          {id: "1.1.2", name: "Node 1.2", children: []}
      ]},
      { id: "1.2", name: "Node 2", children: []}
  ]});

  var viewModel = {
      root: root,
      selected: ko.observable()
  };

  ko.applyBindings(viewModel, $('html')[0]);
});

CSS

/* Styles go here */

ul, li {
  list-style: none;
}

.node-label {
  cursor: pointer;
}
.node-toggle {
  display: inline-block;
  width: 1em;
  cursor: pointer;
}

Ideally the solution should work for infinite nesting levels. Can anybody give me a hint?

Upvotes: 0

Views: 464

Answers (1)

Roy J
Roy J

Reputation: 43899

TreeNode is a constructor, but you are calling it without new in the line

this.children.push(TreeNode(
  {id: "xxxx", name: "Node xxxx", children: []}
  ));

This is just one of those gotchas you have to watch out for. If you use strict mode, then any use of this in a constructor you didn't call with new would be undefined instead of the global object. That could help.

Upvotes: 1

Related Questions