2

Here's the script: jsFiddle (see below for the script inline).

P (Page) is a container for S (Section) objects, and S is a container for F (Field) objects. When you click on "Add Section" you will get a new Section. Similarly you would get a new Field in the selected Section (currentSec) when "Add Field to Section" is clicked.

I don't know if KO has a syntax for calling the addField() function directly on the Section object -that's what I'm looking for. Evidently the current data-bind for the "Add Field" button is currently incorrect.

I know I can move addField() to the Page object and have it use currentSec to do its thing, but I'm wondering if I can keep my structure and still achieve the same result. I highly prefer to stick to my OOP best practices.

HTML:

<div data-bind="foreach: secs">
    <section class="section" data-bind="click: $parent.currentSec, attr: {id: id}">
        <div data-bind="text: $data.id"/>
        <ul data-bind="foreach: $data.fields">
            <li data-bind="attr: {id: id}">New Field</li>
        </ul>
    </section>
</div>
<button data-bind="click: addSection">Add Section</button>

<div data-bind="with: currentSec">
    <button data-bind="click: addField">Add Field to Section</button>
</div>

JS:

function P() {
    this.id = 'pageId';
    this.secs = ko.observableArray();
    this.currentSec = ko.observable();
}
P.prototype.addSection = function() {
    this.secs.push(new S("section" + this.secs().length));
}

function S(sid) {
    this.id = sid;
    this.fields = ko.observableArray();
    this.currentField = ko.observable();
}
S.prototype.addField = function() {
    this.fields.push(new F("field" + this.fields().length));
}

function F(fid) {
    this.id = fid;
}

ko.applyBindings(new P());
Mossi
  • 997
  • 5
  • 15
  • 28

1 Answers1

2

Yes, this is possible. Check this fiddle. It basically runs on this:

<!-- ko with: currentSec -->
<button data-bind="click: addField">Add Field to Section</button>
<!-- /ko -->

It sets a context using the with binding for the button of the current section, allowing the click binding to work properly.

UX-wise it may make more sense though to have the "Add Field" button appear somewhere inside the section itself though. In the section you have the correct context anyways.


I did need to tweak a few things to your code to get things working (I'm not sure how much of it is really needed, but refactoring these items helped me get to the mentioned fiddle):

  • I tend to store this in a variable self inside the constructor function, as to avoid trouble with this inside inner functions such as addSection.
  • Somewhat related, I moved the functions to inside the constructor function itself, so they have access self through the constructor function closure.
  • I've added some "active" css (it took me a bit to see that clicking a section allowed me to "select" it as current.
  • I've made it so that addSection sets the current section to the new one (but that's optional of course).
  • The div was self-closing, but this tends to cause trouble; so I added a </div> closing tag.

None of this is all too important to the question at hand. For completeness, here's the full working repro:

<div id="page0" data-bind="foreach: secs">
    <section class="section" data-bind="click: $parent.currentSec, attr: {id: id}, css: { active: $parent.currentSec() == $data }">
        <div data-bind="text: $parent.id"></div>
        <ul class="connected" data-bind="foreach: fields">
            <li data-bind="attr: {id: id}, click: $parent.currentField">New Field</li>
        </ul>
    </section>
</div>
<button data-bind="click: addSection">Add Section</button>
<button data-bind="click: currentSec().addField">Add Field to Section</button>

With this JS:

function P() {
    var self = this;
    self.id = 'pageId';
    self.secs = ko.observableArray();
    self.currentSec = ko.observable();
    self.addSection = function() {
        var newSection = new S("section" + self.secs().length);
        self.currentSec(newSection);
        self.secs.push(newSection);
    }
}

function S(sid) {
    var self = this;
    self.id = sid;
    self.fields = ko.observableArray();
    self.currentField = ko.observable();
    self.addField = function() {
        self.fields.push(new F("field" + self.fields().length));
    };
}

function F(fid) {
    var self = this;
    self.id = fid;
}

var pvm = new P();
ko.applyBindings(pvm);
Jeroen
  • 60,696
  • 40
  • 206
  • 339
  • That's nuts how KO can interpret comments as virtual DOM elements. After reading your reply I dug through the docs a bit more and found the section that explains this. Sometimes you don't understand something until you end up having to use it! BUT, do you know why my updated script would not work? I'm using a
    : http://jsfiddle.net/pXpAd/10/
    – Mossi May 06 '14 at 08:27
  • 1
    @user1936026 Your code is working fine with `
    ` http://jsfiddle.net/yQb5q/ you have just again selfcolsed your div: `
    `
    – nemesv May 06 '14 at 08:47
  • hmm looks like you are right. I don't understand why self-closing the div causes `addField()` to not work though. Thanks by the way! – Mossi May 06 '14 at 18:39
  • See e.g. [this](http://stackoverflow.com/a/3558200/419956) for more info. A short-short version: in html4 a self closing div isn't valid. In xhtml it is, but that only works if you have the exact right *mime type* (just doctype stuff doesn't cut it). In html5 `
    ` equals `
    `, and I'm unsure what that means in browsers internally and for KO specifically, but I can imagine it creates trouble for things like the `text` binding which may try to access inner text nodes that aren't there for empty elements. (Only way to be sure would be to delve into the source, I guess...)
    – Jeroen May 06 '14 at 20:12