在没有点击事件的情况下在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>
我正在尝试构建一组父->子表单,其中父表单是一个包含多列值的 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>