Сопоставление глубоко иерархических объектов с пользовательскими классами с использованием плагина отображения нокаутов

Используя плагин отображения нокаута (http://knockoutjs.com/documentation/plugins-mapping.html), вы можете отобразить глубоко иерархический объект?

Если у меня есть объект с несколькими уровнями:

var data = {
    name: 'Graham',
    children: [
        {
            name: 'Son of Graham',
            children: [
                {
                    name: 'Son of Son of Graham',
                    children: [
                        {
                            ... and on and on....
                        }
                    ]

                }
            ]
        }
    ]
}

Как мне сопоставить его с моими пользовательскими классами в javascript:

var mapping = {
    !! your genius solution goes here !!

    !! need to create a myCustomPerson object for Graham which has a child myCustomerPerson object 
    !! containing "Son of Graham" and that child object contains a child myCustomerPerson 
    !! object containing "Son of Son of Graham" and on and on....

}

var grahamModel = ko.mapping.fromJS(data, mapping);

function myCustomPerson(name, children)
{
     this.Name = ko.observable(name);
     this.Children = ko.observableArray(children);
}

Может ли плагин отображения рекурсивно отображать эти данные в иерархию моих пользовательских объектов?

Ответ 1

Что-то вроде этого (Живая копия на js скрипке):

CSS

.left {
    float: left;
}

.clear {
    clear: both;
}​

HTML:

<p>Current:&nbsp;
    <a href="#" data-bind="visible: (stack.length > 0), text: selectedNode().name, click: selectParentNode"></a>
    <span data-bind="visible: (stack.length <= 0), text: selectedNode().name"></span>
</p>
<p class="left">Children:&nbsp;</p>
<ul class="left" data-bind="template: {name: 'childList', foreach: selectedNode().children}"></ul>

<script type="text/html" id="childList">
    <li data-bind="click: function(){nodeViewModel.selectChildNode($data)}">
        <a href="#">A${name}</a>
    </li>
</script>

<br /><br />
<ul class="clear" data-bind="template: {name: 'backBtn'}"></ul>

<script type="text/html" id="backBtn">
    <a href="#" data-bind="visible: $data.selectedNode().back, click: function() { nodeViewModel.selectBackNode($data.selectedNode().back) }">Back</a>
</script>​

JavaScript:

var node = function(config, parent) {
    this.parent = parent;
    var _this = this;

    var mappingOptions = {
        children: {
            create: function(args) {
                return new node(args.data, _this);
            }
        }
    };

    ko.mapping.fromJS(config, mappingOptions, this);
};

var myModel = {
    node: {
        name: "Root",
        children: [
            {
            name: "Child 1",
            back: 1,
            children: [
                {
                name: "Child 1_1",
                back: 1,
                children: [
                    {
                    name: "Child 1_1_1",
                    back: 4,
                    children: [
                        ]},
                {
                    name: "Child 1_1_2",
                    back: 2,
                    children: [
                        ]},
                {
                    name: "Child 1_1_3",
                    back: 1,
                    children: [
                        ]}
                    ]}
            ]},
        {
            name: "Child 2",
            back: 1,
            children: [
                {
                name: "Child 2_1",
                back: 1,
                children: [
                    ]},
            {
                name: "Child 2_2",
                back: 1,
                children: [
                    ]}
            ]}
        ]
    }
};

var viewModel = {

    nodeData: new node(myModel.node, undefined),

    selectedNode: ko.observable(myModel.node),

    stack: [],

    selectBackNode: function(numBack) {

        if (this.stack.length >= numBack) {
            for (var i = 0; i < numBack - 1; i++) {
                this.stack.pop();
            }
        }
        else {
            for (var i = 0; i < this.stack.length; i++) {
                this.stack.pop();
            }
        }

        this.selectNode( this.stack.pop() );
    },

    selectParentNode: function() {
        if (this.stack.length > 0) {
            this.selectNode( this.stack.pop() );
        }
    },

    selectChildNode: function(node) {
        this.stack.push(this.selectedNode());
        this.selectNode(node);
    },

    selectNode: function(node) {
        this.selectedNode(node);
    }

};

window.nodeViewModel = viewModel;
ko.applyBindings(viewModel);​

Этот пример просто отображает бесконечно вложенный набор данных JSON, и я могу сказать, что от фактического использования этого точного кода в приложении, который отлично работает.

Некоторые дополнительные функции, такие как

selectBackNode и выберитеParentNode

позволяет вам перемещать назад дерево.

Во время навигации пример родительский ярлык становится ссылкой, позволяющей подняться на один уровень, а некоторые из листовых узлов имеют кнопку возврата, которая позволяет им перемещать назад дерево по заданному количеству уровней.

- EDIT -

Если ваши листовые узлы не имеют дочернего массива, вы можете столкнуться с проблемой, когда вводятся дополнительные данные, которых нет в модели.

Ответ 2

Из моего опыта я бы сказал, что у него не должно быть никаких проблем.

Я бы использовал следующую строку -

var grahamModel = ko.mapping.fromJS(data);

Затем установите точку останова на следующей строке при просмотре сгенерированного объекта в вашем отладчике (лучше всего работает хром или FF + Firebug). Таким образом, вы узнаете, будет ли ko.mapping генерировать viewmodel, который соответствует вашим потребностям.

Обычно он генерирует объект, где только конечные точки (переменные со значениями) являются ko.observables. Любое другое время данных, которое вы можете использовать для навигации по данным, например ... children: [..., отображается как обычные объекты javaScript.

Ответ 3

Если вы не хотите вложенных mappingOptions (создание объекта карты ko для каждого уровня node), вы можете воспользоваться тем фактом, что параметры сопоставления ko для создания дают вам доступ к родительскому объекту. Что-то вроде этого:

function Folder(parent,data) {
    var self = this;
    self.parent = parent;
    ko.mapping.fromJS(data, self.map, self);
}

Folder.prototype.map = {
    'folders': {
        create: function(options) {
            var folder = new Folder(options.parent,options.data);
            return folder;
        }
    }
}

var data = { name:"root", folders: [ {name:"child", folders: [] } ] };
var root = new Folder(null, data);

Таким образом, у вас есть только 1 копия карты, в прототипе своего класса (или может быть любой функцией). Если вы хотите, чтобы Folder.parent был также заметным, вы могли бы сделать data.parent = parent; внутри функции карты и не передавать в качестве параметра в свой конструктор папки или делать это внутри конструктора папки вместо self.parent = parent;

Ответ 4

Я использовал подход в этом , чтобы создать иерархию флажков, где узлы с дочерними элементами являются разборными, и когда вы проверяете/снимаете флажок родителя, его потомки получают check/unchecked.

Просмотр модели

var Category = function(data, parent) {
    var self = this;
    self.name = data.name;
    self.id = data.id;
    self.parent = parent;
    self.categoryChecked = ko.observable(false);
    ko.mapping.fromJS(data, self.map, self);
};

// This will add a "map" to our category view model
Category.prototype.map = {
    'sub_categories' : {
        create: function(options){
            var category = new Category(options.data, options.parent);
            category.parent.categoryChecked.subscribe(function(value){
                category.categoryChecked(value);
            });
            return category;
        }
    }
};  

HTML (просмотр)

    <div data-role="panel" id="left-panel" data-position="left" data-position-fixed="false" data-theme="b">
            <div data-role="collapsible-set" data-bind="template: {name: 'category_collapsible', foreach: sub_categories}" data-mini="true" id="categories" data-iscroll> </div>
        </div><!-- END left panel -->

        <script type="text/html" id="category_collapsible">
            <div class="category_collapsible" data-mini="true" data-content-theme="b" data-inset="true" data-iconpos="right">
                <h3>     
                    <input data-role="none" data-them="b" data-bind='checked: categoryChecked, jqmChecked: true, attr: {id: "category_checkbox_"+id}' class="chk_category" type="checkbox" />
                    <label data-bind='attr: {for: "category_checkbox_"+id}'><span data-bind="text: name"> </span></label>
                </h3>
                <ul data-role="listview" data-bind="template: {name: 'category_list', foreach: sub_categories}">

                </ul>
            </div>
        </script><!-- END category_collapsible template -->

        <script type="text/html" id="category_list">
            <!-- ko if: sub_categories().length==0 -->
                <li data-theme="c">
                    <input data-role="none" data-theme="c" data-bind='checked: categoryChecked, jqmChecked: true, attr: {id: "category_checkbox_"+id}' class="chk_category" type="checkbox"/>
                    <label data-corners="false" data-bind='attr: {for: "category_checkbox_"+id}'>
                        <span data-bind="text: name"> </span>
                    </label>        
                </li>
            <!-- /ko -->
            <!-- ko if: sub_categories().length>0 -->
                <li data-theme="c" data-bind="template: {name: 'category_collapsible', data: $data}"></li>
            <!-- /ko -->
        </script>