5

I'm using knockoutjs to create a treeview of divisions. Next to each node will be three buttons: 1)New child(applies to the node it's next to 2) Remove(This removes the node it's next to, and 3) Copy, which copies the node and all it's children and creates a new node under the parent.

I've got the New button down, and now I'm working on the remove button. I can't seem to get it to work, and instead of doing anything useful it just refreshes the entire page. Here's the code:

View:

<h2>Skill & Weight Divisions</h2>
        <span data-bind="text: tournamentname"></span><button data-bind="click: addDivision"><img src="new.png"/></button>
        <ul data-bind="template: { name: 'divisionTemplate', foreach: divisions }"></ul>

Template:

<script id="divisionTemplate" type="text/html">
   <li data-bind="style: {'background-color':color}">
       <input data-bind="value: name"/><button data-bind="click: addDivision"><img src="new.png"/></button><button data-bind="click: $parent.removeDivision"><img src="remove.png"/></button><button data-bind="click: $parent.copyDivision"><img src="copy.png"/></button>
       <ul data-bind="template: { 'if': children, name: 'divisionTemplate', foreach: children }"></ul>
    </li>       
</script>

View Model and appropriate helper function:

function division(id, name, filter, children) {
        this.id = ko.observable(id);
        this.name = ko.observable(name);
        this.filter = ko.observable(filter)
        if(children){
            this.children = ko.observableArray(children);   
        }else{
            this.children = ko.observableArray();   
        }
        this.addDivision = function(){
            this.children.push(new division("", "", ""));   
        }
        this.removeDivision = function(division){
            this.children.remove(division);
        }
        this.copyDivision = function(division){
            this.children.push(division);   
        }
        this.color = randColor();
    };
    function tournamentViewModel(){
        var self= this;
        self.tournamentname = ko.observable('NO NAME YET');
        self.districts = ko.observableArray([new district('Provo',1),new district('Salt Lake City',2),new district('St. George',3)]);
        self.district = ko.observable(self.districts()[0]);
        self.regions = ko.observableArray([new region('Utah',1),new region('Idaho',2)]);
        self.region = ko.observable(self.regions()[0]);
        self.location = ko.observable('WHEREVER YOU WANT');
        self.eventdate = ko.observable('');
        self.startTime = ko.observable('');
        self.image = ko.observable();
        self.flyer = ko.computed(function(){
            var flyerHTML = '<span style="text-align:center;padding:10px;"><h1>'+self.tournamentname()+'</h1><img src="'+self.image()+'"/><br/>';
            flyerHTML += 'District: ' + self.district().districtName + ' Region: ' + self.region().regionName+'<br><br>';
            flyerHTML += '<h2>WHEN: '+self.eventdate()+' '+self.startTime()+'</h2>';
            flyerHTML += '<h2>WHERE: '+self.location()+'</h2>';
            flyerHTML += '<img src="http://maps.googleapis.com/maps/api/staticmap?center='+encodeURI(self.location())+'&zoom=12&size=200x200&markers=color:blue%7Clabel:S%7C'+encodeURI(self.location())+'&maptype=roadmap&sensor=false"/>';
            return flyerHTML;
        }, self);
        self.clearImage = function(){
            self.image(''); 
        }
        self.tournamentID = ko.computed(function(){return 't_'+self.district()+'_'+self.region()+'_'+self.eventdate()}, self);
        self.pricingStructures = ko.observableArray([new pricingStructure(3,2.99), new pricingStructure(1,1.99)]);
        self.removePricingStructure = function(pricingStructure){
            self.pricingStructures.remove(pricingStructure); 
        }
        self.addPricingStructure = function(){
            self.pricingStructures.push(new pricingStructure("", ""));  
        }
        self.promoCodes = ko.observableArray();
        self.promoTypes = ['%','$'];
        self.removePromoCode = function(promoCode){
            self.promoCodes.remove(promoCode); 
        }
        self.addPromoCode = function(){
            self.promoCodes.push(new promoCode("", ""));    
        }
        self.divisions = ko.observableArray([new division(1, "Men","",[new division(2,"Gi"), new division(3,"No-Gi")])]);
        self.addDivision = function(){
            self.divisions.push(new division("", "", ""));  
        }

    }
    ko.applyBindings(new tournamentViewModel());

My main question in all of this is this: Is there a way to access an object's parent array in order to remove that very object from the array? Thanks in advance for the help!

EDIT: Here's a jsFiddle: http://jsfiddle.net/eqY7Z/ However it doesn't seem to be working at all there. If you guys can't get it going, I'll include the link to my site where it's being hosted so you can take a good look at it.

Frank B
  • 661
  • 1
  • 9
  • 20

2 Answers2

2

I took your idea and made a working fiddle, which behaves exactly how you described. I didn't want to try to sort yours out, sorry. It had a lot of stuff not directly related to your problem, and this solution is general enough that other people should be able to use it. If you need help adapting it, let me know.

One thing to make note of is the clone function. Your copy function isn't deep, and will result in multiple nodes pointing to the same object. If you were to update a nodes value, it would propagate to its clones. Knockout provides a handy deep copy + unwrap observables with ko.toJS. Super useful.

The JS:

var Node = function(name, children) {
    var self = this;
    self.name = ko.observable(name || 'NewNode');
    self.children = ko.observableArray(
    ko.utils.arrayMap(children || [], function(i) {
        return new Node(i.name, i.children);
    }));
    self.newChild = function() {
        self.children.push(new Node());
    };
    self.removeNode = function(node) {
        self.children.remove(node);
    };
    self.copyNode = function(node) {
        var cloneNode = ko.toJS(node);
        self.children.push(new Node(cloneNode.name, cloneNode.children));
    };
};

//Example data removed for brevity, see fiddle
ko.applyBindings(new Node(data.name, data.children));​

HTML:

<button data-bind="click: newChild">NewNode</button>
<ul data-bind="template: { name: 'treeTemplate', foreach: children}">
</ul>

<script id="treeTemplate" type="text/html">
    <li>
        <input data-bind="value: name" />
        <button data-bind="click: newChild">New Child</button>
        <button data-bind="click: $parent.removeNode">Remove Node</button>
        <button data-bind="click: $parent.copyNode">Copy Node</button>
        <ul data-bind="template: { name: 'treeTemplate', foreach: children}"></ul>
    </li>
</script>
​
Kyeotic
  • 19,697
  • 10
  • 71
  • 128
  • This looks great and I'll try it out later tonight. Just to be clear, as I know you wrote this as a general answer, in my specific case the nodes would be what I'm calling divisions, correct? – Frank B Jul 13 '12 at 16:35
  • Yes. Node is a common term for an item in a tree. – Kyeotic Jul 13 '12 at 16:36
  • Also thanks so much for tackling the copy function too. I was going to do that after I defeated the remove function. Thanks for saving me a ton of time and teaching me some good knockout javascript! – Frank B Jul 13 '12 at 16:36
  • I accepted because the fiddle looks great. I'm sure it'll work in my code, and if not that's my fault not yours, so I figured no reason for you to wait for your points. – Frank B Jul 13 '12 at 17:11
  • @FrankB lol, thanks. Seriously though, if you need help adapting it (since that was pasrt of your question), let me know. I'd be glad to help. – Kyeotic Jul 13 '12 at 17:29
  • The only help I may need is adapting it to where it's part of the overall view model, instead of it's own. Any ideas? – Frank B Jul 14 '12 at 03:41
  • I'm having problems making this a part of my overall viewmodel. Any suggestions? – Frank B Jul 15 '12 at 00:49
  • So you know I got it to work, I can show you in a fiddle how I did it if you'd really like to know. – Frank B Jul 20 '12 at 19:42
  • I haven't forgotten about you. I've just been super busy. I'll have it up some point this week. – Frank B Aug 13 '12 at 22:00
1

I was able to create a working version of your code in the following jsFiddle: http://jsfiddle.net/3eQNf/. It seemed to come down to 2 main issues:

  1. You had a context issue with the use of the "this" keyword in your division class. Adding the self variable solved that problem.

  2. You needed to add a single root-level division and bind to its children. This makes all the recursion work as expected. Doing this also removed the need for the addDivision method off of your tournamentViewModel

Also, FYI, I needed to add stubs for your district, region, and pricingStructure classes since those were not included in your sample code above. Hope this helps.

Paulito
  • 55
  • 1
  • 5
  • Thanks for letting me know what was wrong. I thought I had cleared out the district, region, etc. code. Oh well. I did notice though that I can't copy a top level division with the fiddle you provided, and copying actually just creates text boxes that point to already existing objects. I know my question wasn't about copying though, just wanted to point that out for you. – Frank B Jul 13 '12 at 19:24