在没有点击事件的情况下在knockoutjs中设置活动记录

Set active record in knockoutjs without click-event

我正在尝试构建一组父->子表单,其中父表单是一个包含多列值的 table。 当通过单击事件或键盘导航激活父行时,子表单应使用相应的子记录进行更新。

表单中的每个值都显示为输入字段。

子记录本身可以是另一个父->子星座中的父记录。

但我完全迷失在这里,在过去的几个月里我尝试了不同的方法(中间有很大的休息)。

到目前为止,我已经使用 jQuery js:

设置了活动 table 行的样式
d.on('keydown', 'table.data tr td input:text', function (e) {
    var rowCount = $(this).closest('tbody').find('tr').length + 1;
    var cellCount = $(this).closest('tbody tr').find('td').length;
    // detect arrows pressing
    if (e.keyCode !== 38 && e.keyCode !== 40) // up & down
        return;
    e.preventDefault();
    var target;
    var cellAndRow = $(this).parents('td,tr');
    var cellIndex = cellAndRow[0].cellIndex;
    var rowIndex = cellAndRow[1].rowIndex;
    switch (e.keyCode) {
        case 40:
            rowIndex = rowIndex + 1;
            break;
            // down arrow                 
        case 38:
            rowIndex = rowIndex - 1;
            break;
    }
    target = $('table tr').eq(rowIndex).find('td').eq(cellIndex).find("input:text");
    if (target !== undefined) {
        target.focus();
    }
});
d.on('focus', 'table.data tr td input:text', function (e) {

    $(this).select();
    // Clear active state.
    JQD.util.clear_active();
    // Highlight row, ala Mac OS X.

    $(this).closest('tr').addClass('active');
    $(this).closest('td').addClass('active');

    var rowCount = $(this).closest('tbody').find('tr').length;
    var row = $(this).closest('tbody').find('tr.active');
    var rowIndex = row.index();
    if (rowCount > 0) {
        if (rowIndex === 0) {
            $(this).closest('.window_inner').find('button.prev').addClass('inactive');
        }
        else if ($(this).closest('.window_inner').find('button.prev').hasClass('inactive')) {
            $(this).closest('.window_inner').find('button.prev').removeClass('inactive');
        }
        if (rowCount - 1 === rowIndex) {
            $(this).closest('.window_inner').find('button.next').addClass('inactive');
        }
        else if ($(this).closest('.window_inner').find('button.next').hasClass('inactive')) {
            $(this).closest('.window_inner').find('button.next').removeClass('inactive');
        }
    } else {
        $(this).closest('.window_inner').find('button.prev').addClass('inactive');
        $(this).closest('.window_inner').find('button.next').addClass('inactive');
    }

    var id = $(this).closest('.window_inner').find('tr.active td span.id').text();
    var window_id = $(this).closest('.window').attr('id').replace('window_', '');
    window[window_id + '_mvm' ].vm().selectedId(id);

    $(this).closest('.window_inner').find('.window_bottom .left').text((rowIndex + 1) + '/' + rowCount);
});
d.on('blur', 'table.data tr td input:text', function (e) {
    JQD.util.clear_active();

    var window_id = $(this).closest('.window').attr('id');
    window[window_id.replace('window_', '') + '_mvm' ].vm().selectedId(0);

    $(this).closest('.window_inner').find('.window_bottom .left').text('');
});

除了设置活动行的样式外,我还尝试告诉 KO-viewmodel 记录是活动的,但它要求 ID 出现在 table 中,我可以仅在最顶层的父记录上使用它,因为 jQuery js 不知道该行所在的范围...

我对想法和不同的方法持开放态度,但我确实想使用 Knockout,因为对我来说,它不是一个框架,我可以在其中将视图模型保存在一个文件中并使用 KO 进行数据绑定,然后让我自己的框架会处理其他一切。

更新:

JS:

//wrapper for an observable that protects value until committed
ko.protectedObservable = function (initalValue) {
    //private variables
    var _temp = ko.observable(initalValue);
    var _actual = ko.observable(initalValue);

    var result = ko.dependentObservable({
        read: function () {
            return _actual();
        },
        write: function (newValue) {
            _temp(newValue);
        }
    });

    //commit the temporary value to our observable, if it is different
    result.commit = function () {
        if (_temp() !== _actual()) {
            _actual(_temp());
        }
    };

    //notify subscribers to update their value with the original
    result.reset = function () {
        _actual.valueHasMutated();
        var temp = _temp();
        var actual = _actual();
        _temp(_actual());
        var temp = _temp();
        var actual = _actual();
    };

    // notify subscriber if value has changed
    result.isDirty = function () {
        return _temp() !== _actual();
    };
    return result;
};

function Datatype(id, name, datatype, description) {
    var me = this;
    id = (id === 'undefined' ? null : id);
    name = (name === 'undefined' ? null : name);
    datatype = (datatype === 'undefined' ? null : datatype);
    description = (description === 'undefined' ? null : description);
    me.id = ko.observable(id);
    me.name = ko.protectedObservable(name);
    me.datatype = ko.protectedObservable(datatype);
    me.description = ko.protectedObservable(description);
    me.dirtyFlag = ko.computed(function () {
        return me.name.isDirty() || me.datatype.isDirty() || me.description.isDirty();
    });
    me.deleteFlag = ko.observable(false);
    me.remove = function () {
        me.deleteFlag(true);
    };
    me.commit = function () {
        me.name.commit();
        me.datatype.commit();
        me.description.commit();
        me.deleteFlag(false);
    };
    me.reset = function () {
        me.name.reset();
        me.name.notifySubscribers();
        me.datatype.reset();
        me.datatype.notifySubscribers();
        me.description.reset();
        me.description.notifySubscribers();
        me.deleteFlag(false);
    };
}

function FKSYS002_ViewModel(initialData, searchmode) {
    var self = this;
    self.searchmode = searchmode;
    self.datatypes = ko.observableArray();
    self.activeDatatype = ko.observable(null);
    self.setActiveDatatype = function(record) {
        alert(record.datatype());
        self.activeDatatype(ko.unwrap(record));
        return true;
    };
    self.isActiveDatatype = function(record) {
        return self.activeDatatype() === ko.unwrap(record);
    };
    self.selectedDatatype = ko.observable();

    self.selectedId = ko.observable(0);

    var temp_id = -1;
    self.add = function () {
        self.datatypes.push(new Datatype(temp_id));
        temp_id--;
    };
    self.remove = function () {
        if (self.selectedId() > 0) {
            var selectedItem = ko.utils.arrayFirst(self.datatypes(), function (item) {
                return item.id() === self.selectedId();
            });
            selectedItem.remove();
        }
    };
    self.save = function () {
        ko.utils.arrayForEach(self.datatypes(), function (item) {
            if (item.dirtyFlag()) {
                item.commit();
            }
        });
    };
    self.refetch = function () {
        self.fetchDatatypes();
    };
    self.executeSearch = function () {
        ko.utils.arrayForEach(self.datatypes(), function (item) {
            item.commit();
        });
    };

    self.fetchDatatypes = function (initialData) {
        if (typeof initialData !== 'undefined') {
            self.fillDatatypes(initialData);
        } else {
            URI = 'http://localhost/FormKit/App/API/datatypes';
            ajaxGet(URI).done(function (data) {
                self.fillDatatypes(data);
            });
        }
    };
    self.fillDatatypes = function (data) {
        self.datatypes.removeAll();
        for (var i = 0; i < data.length; i++) {
            self.datatypes.push(new Datatype(data[i].id, data[i].name, data[i].datatype, data[i].description));
        }
    };
    self.dirtyItems = ko.computed(function () {
        return ko.utils.arrayFilter(self.datatypes(), function (item) {
            return item.dirtyFlag();
        });
    }, self);
    self.isDirty = ko.computed(function () {
        return self.dirtyItems().length > 0;
    }, self);

    self.deletedItems = ko.computed(function () {
        return ko.utils.arrayFilter(self.datatypes(), function (item) {
            return item.deleteFlag();
        });
    }, self);
    self.containsDeleted = ko.computed(function () {
        return self.deletedItems().length > 0;
    }, self);
    if (searchmode === 'undefined' || !searchmode) {
        self.fetchDatatypes(initialData);
    } else if (searchmode) {
        self.datatypes.push(new Datatype());
    }
    ;
}

function FKSYS002_MasterViewModel() {
    var master = this;
    master.vm = ko.observable(null);
    master.svm = ko.observable(null);
    var init = '[{"id":"1","name":"String","datatype":"VARCHAR(4000)","description":""},{"id":"2","name":"Text","datatype":"TEXT","description":""},{"id":"3","name":"Number","datatype":"DECIMAL(15,4)","description":""},{"id":"4","name":"Boolean","datatype":"BIT(1)","description":""},{"id":"5","name":"Time","datatype":"TIME","description":""},{"id":"6","name":"Date","datatype":"DATE","description":""}]';

    master.FKSYS002_vm = new FKSYS002_ViewModel(init);
    master.FKSYS002_svm = new FKSYS002_ViewModel(init, true);
    master.initSearch = function () {
        master.vm(master.FKSYS002_svm);
    };
    master.executeSearch = function () {
        master.vm().executeSearch();
    };
    master.redoSearch = function () {
//                            master.vm(JSON.parse(master.svm()));
    };
    master.cancelSearch = function () {
        master.vm(master.FKSYS002_vm);
    };

    master.vm(master.FKSYS002_vm);
}

var FKSYS002_mvm = new FKSYS002_MasterViewModel;
ko.applyBindings(FKSYS002_mvm, $('#window_FKSYS002')[0]);

HTML:

    <div class="window_content">
        <table id="FKSYS002_table_001" class="data">
            <thead>
                <tr>
                    <th hidden>
                        id
                    </th>
                    <th>
                        Name
                    </th>
                    <th>
                        MySQL data type
                    </th>
                    <th>
                        Description
                    </th>
                </tr>
            </thead>
            <tbody data-bind="foreach: vm().datatypes">
                <tr data-bind="css: {deleted: deleteFlag(), active: $parent.isActiveDatatype($data)}, click: $parent.setActiveDatatype">
                    <td hidden>
                        <span class="id" data-bind="text: id"></span>
                    </td>
                    <td>
                        <input data-bind="value: name, attr:{readonly: deleteFlag()}, event: {focus: $parent.setActiveDatatype, keydown: $parent.setActiveDatatype($data)}" size="8" maxlength="30" />
                    </td>
                    <td>
                        <input data-bind="value: datatype, attr:{readonly: deleteFlag()}, event: {focus: $parent.setActiveDatatype, keydown: $parent.setActiveDatatype($data)}" size="15" maxlength="30" />
                    </td>
                    <td>
                        <input data-bind="value: description, attr:{readonly: deleteFlag()}, event: {focus: $parent.setActiveDatatype, keydown: $parent.setActiveDatatype($data)}" size="50" maxlength="250" />
                    </td>
                </tr>
            </tbody>
        </table>
    </div>

我可能会在跟踪活动记录的根视图模型上使用 属性,然后使用 css 绑定来确保活动记录具有 class:

(function() {
  "use strict";

  // A record constructor
  function Record(id) {
    this.id = ko.observable(id);
    this.val = ko.observable("Value for " + id);
    this.children = ko.observableArray();
  }

  // The root viewmodel
  var vm = {
    // The active record
    active: ko.observable(null),
    
    // Top record in our tree
    top: ko.observable(),
    
    // Function to determine if the given record is active
    isActive: function(record) {
      return vm.active() === ko.unwrap(record);
    },
    
    // Function to make the given record active
    makeActive: function(record) {
      vm.active(ko.unwrap(record));
      return true;
    }
  };
  
  // Add some records
  var r;
  
  // Top level record
  vm.top(new Record(0));

  // Give 'top' a child that has a child of its own
  r = new Record(1);
  r.children.push(ko.observable(new Record(11)));
  vm.top().children.push(ko.observable(r));

  // Give 'top' a child with two children
  r = new Record(2);
  r.children.push(ko.observable(new Record(21)));
  r.children.push(ko.observable(new Record(22)));
  vm.top().children.push(ko.observable(r));

  // Give it one more child, which has no children
  vm.top().children.push(ko.observable(new Record(3)));

  // Good to go
  ko.applyBindings(vm, document.body);
})();
.active > .header {
  background-color: yellow
}
.children {
  padding-left: 2em;
}
  <div class="record" data-bind="template: {name: 'record', data: top}, css: {'active': $root.isActive(top)}"></div>
<script type="text/html" id="record">
  <div class="header">I'm record <span data-bind="text: id"></span></div>
  <div><input type="text" data-bind="value: val, event: {focus: $root.makeActive, keydown: $root.isActive($data)}"></div>
  <div class="children" data-bind="foreach: children">
    <div data-bind="template: {name: 'record', data: $data}, css: {'active': $root.isActive($data)}"></div>
  </div>
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>