devanl
devanl

Reputation: 1322

Nested observable in KnockoutJS

I have a self describing definition as follows:

var my_data = {
  types: { typeA: {fieldX: { type: "string"}},
           typeB: {fieldY: { type: "string"}} },
  entries: [{ type: "typeA", fieldX: "foo" },
            { type: "typeB", fieldY: "bar" }]
  };

The idea being that 'types' describes the data presented for each index of 'entries'. This data is used to render a form for editing JSON data via a RESTful interface (an input for each type described field with bindings to the entry's field value).

I'm trying to set up a form in knockout which allows the 'entries' to be edited. I first create the HTML which creates a select box for determining type. To do this I map the types dictionary to an array:

function mapDictionaryToArray(dictionary) {
    var result = [];
    for (var key in dictionary) {
        if (dictionary.hasOwnProperty(key)) {
            result.push({ key: key, value: dictionary[key] });
        }
    }

    return result;
}

Then the KnockoutJS code to display the entries

<!-- ko foreach: entries -->
            <select data-bind="options: editTestViewModel.types,
                               optionsText: 'key',
                               optionsValue: 'value',
                               value: type,
                               event: { change: editTestViewModel.drawType }"></select>
            <div data-bind="attr: { id: $index() + 'typeFields' }" class="container"></div>
<!-- /ko -->

The model view:

function EditTestViewModel() {
    var self = this;

    self.entries = ko.observableArray();
    self.types = ko.observable();

    self.setTest = function(test) {
        self.test = test;
        self.types(mapDictionaryToArray(test.types()));
        self.entries(ko.mapping.fromJS(test.entries(), self.stateMapping)());

    };

    self.editTest = function() {
        $('#edit').modal('hide');
        testsViewModel.edit(self.test, {
            entries: self.entries() ,
            types: self.types()
        });
    };

    self.drawType = function(place) {
        home = $('#typeFields');
        home.children().remove();
        for (var field in place.type) {
            tag = $('<div class="input-group">').appendTo(home);
            $('<span class="input-group-addon">' + field + '</span>').appendTo(tag);
            $('<input type="text" class="form-control" data-bind="text: \'test\'">').appendTo(tag);
        }
    };

    self.stateMapping = {
        'type': {
            create: function(options) {
                return options.data
            }
        }
    }
}

var editTestViewModel = new EditTestViewModel();
ko.applyBindings(editTestViewModel, $('#edit')[0]);

The problem I am having is that the inserted data does not appear to be processed by KockoutJS. The type select functions and populates the relevant fields but, the field value is not populated (static "test" for now). Is KnockoutJS even supported for dynamically inserted content, and if so am I attempting to bind this correctly?

Upvotes: 0

Views: 906

Answers (1)

devanl
devanl

Reputation: 1322

Thanks to everyone for comments on the OP.

I rewrote to use templates. For some reason I was under the impression that observables could not be used for option.value storage. That appears to not be the case (https://stackoverflow.com/a/27873057/1440598). In the constructor I now set the type to be an observable for each entry.

Changing the select triggers the template field update: http://fiddle.jshell.net/jhwpn6dy/3/

HTML:

<div data-bind="text: data.test"></div>

<div data-bind="template: { name: 'entry-template', foreach: entries, as: 'entry' }"></div>

<script type="text/html" id="entry-template">
    <h3 data-bind = "text: entry.type">I'm an entry</h3>
    <select data-bind="options: $root.type_keys,
                       optionsText: 'key',
                       optionsValue: 'key',
                       value: entry.type"></select>
    <div data-bind="template: { name: 'field-template', foreach: $root.fieldList(entry), as: 'field' }"></div>
</script>

<script type="text/html" id="field-template">
    <div class="input-group">
      <span class="input-group-addon" data-bind="text: field"></span>
        <input type="text" class="form-control" data-bind="attr: { 'aria-label': field }, value: entry[field]"/>
    </div>
</script>

JS:

var data = {
    types: {
        typeA: {
            fieldX: {
                type: "string"
            }
        },
        typeB: {
            fieldY: {
                type: "string"
            }
        }
    },
    entries: [{
        type: "typeA",
        fieldX: "foo"
    }, {
        type: "typeB",
        fieldY: "bar"
    }],
    test: "Hello"
};

function MainViewModel(data_obj) {
    var self = this;

    self.data = data_obj;
    self.type_keys = mapDictionaryToArray(data_obj.types);    

    for (var entry in data_obj.entries) {
        data_obj.entries[entry].type = ko.observable(data_obj.entries[entry].type);
    }
    self.entries = ko.observableArray(data_obj.entries);
    console.log(self.entries());
}

MainViewModel.prototype.fieldList = function (entry) {
    var self = this;
    var keys = [];

    for (var key in self.data.types[entry.type()]) {
        if (entry.hasOwnProperty(key)) {
            keys.push(key);
        }
    }

    <!-- console.log(entry.type); -->
    return keys;
};

function mapDictionaryToArray(dictionary) {
    var result = [];
    for (var key in dictionary) {
        if (dictionary.hasOwnProperty(key)) {
            result.push({ key: key, value: dictionary[key] });
        }
    }

    return result;
}

var dataViewModel = new MainViewModel(data);

ko.applyBindings(dataViewModel);

Upvotes: 1

Related Questions