I've been trying to create an autocomplete dropdown based on the accepted response in this post but the autocomplete dropdown simply isn't showing up. It could be because the response is 9 years old or perhaps I'm doing something wrong. I have tried all of the suggestions that I've come across. Is there an updated way to create this combobox using jquery version 1.12.3, jquery-ui version 1.12.1, and knockoutjs version 3.4.1?
To me is seems like the bindings aren't really taking place because I could rename the custom binding to "jqAuto1" instead of "jqAuto" and there would be no errors, even though "jqAuto1" isn't defined anywhere. Why isn't that being picked up?
Here is my code. Note that the JS script is in a separate, parent solution from the CSHTML and TS files. The browser still finds and executes the JS script.
CSHTML
<input class="form-control form-control-xs" data-bind="value: companyName, jqAuto: { autoFocus: true }, jqAutoSource: myComp, jqAutoValue: mySelectedGuid, jqAutoSourceLabel: 'displayName', jqAutoSourceInputValue: 'coname', jqAutoSourceValue: 'id'" placeholder="Type Company Name and select from list" />
TS
// For list of Companies
class Comp {
_id: KnockoutObservable<string>;
_coname: KnockoutObservable<string>;
_coid: KnockoutObservable<string>;
constructor(id: string, coname: string, coid: string) {
this._id = ko.observable(id);
this._coname = ko.observable(coname);
this._coid = ko.observable(coid);
}
}
myComp: KnockoutObservableArray<Comp>;
mySelectedGuid: KnockoutObservable<string>;
displayName: KnockoutComputed<string>;
...
this.myComp = ko.observableArray([
new Comp("1", "Company 1", "CO1"),
new Comp("2", "Company 2", "CO2"),
new Comp("3", "Company 3", "CO3"),
new Comp("4", "Company 4", "CO4"),
new Comp("5", "Company 5", "CO5")
]);
this.companyName = ko.validatedObservable<string>("");
this.displayName = ko.computed(function () {
return this.myComp.coname + " [" + this.myComp.coid + "]";
}, this);
this.mySelectedGuid = ko.observable("5");
JS
Pretty much what's in the linked post
(function () {
var global = this || (0, eval)('this'),
document = global['document'],
moduleName = 'knockout-binding-jqauto',
dependencies = ['knockout', 'jquery'];
var moduleDance = function (factory) {
// Module systems magic dance.
if (typeof define === "function" && define["amd"]) {
define(moduleName, dependencies.concat('exports'), factory);
} else {
// using explicit <script> tags with no loader
global.CPU = global.CPU || {};
factory(global.ko, global.Globalize);
}
};
var factory = function (ko, $) {
ko.bindingHandlers.jqauto = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var options = valueAccessor() || {},
allBindings = allBindingsAccessor(),
unwrap = ko.utils.unwrapObservable,
modelValue = allBindings.jqAutoValue,
source = allBindings.jqAutoSource,
valueProp = allBindings.jqAutoSourceValue,
inputValueProp = allBindings.jqAutoSourceInputValue || valueProp,
labelProp = allBindings.jqAutoSourceLabel || valueProp;
//function that is shared by both select and change event handlers
function writeValueToModel(valueToWrite) {
if (ko.isWriteableObservable(modelValue)) {
modelValue(valueToWrite);
} else { //write to non-observable
if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['jqAutoValue'])
allBindings['_ko_property_writers']['jqAutoValue'](valueToWrite);
}
}
//on a selection write the proper value to the model
options.select = function (event, ui) {
writeValueToModel(ui.item ? ui.item.actualValue : null);
};
//on a change, make sure that it is a valid value or clear out the model value
options.change = function (event, ui) {
var currentValue = $(element).val();
alert(currentValue);
var matchingItem = ko.utils.arrayFirst(unwrap(source), function (item) {
return unwrap(inputValueProp ? item[inputValueProp] : item) === currentValue;
});
if (!matchingItem) {
writeValueToModel(null);
}
}
//handle the choices being updated in a DO, to decouple value updates from source (options) updates
var mappedSource = ko.dependentObservable(function () {
mapped = ko.utils.arrayMap(unwrap(source), function (item) {
var result = {};
result.label = labelProp ? unwrap(item[labelProp]) : unwrap(item).toString(); //show in pop-up choices
result.value = inputValueProp ? unwrap(item[inputValueProp]) : unwrap(item).toString(); //show in input box
result.actualValue = valueProp ? unwrap(item[valueProp]) : item; //store in model
return result;
});
return mapped;
}, null, { disposeWhenNodeIsRemoved: element });
//whenever the items that make up the source are updated, make sure that autocomplete knows it
mappedSource.subscribe(function (newValue) {
$(element).autocomplete("option", "source", newValue);
});
options.source = mappedSource();
//initialize autocomplete
$(element).autocomplete(options);
},
update: function (element, valueAccessor, allBindings, viewModel) {
//update value based on a model change
var allBindings = allBindingsAccessor(),
unwrap = ko.utils.unwrapObservable,
modelValue = unwrap(allBindings.jqAutoValue) || '',
valueProp = allBindings.jqAutoSourceValue,
inputValueProp = allBindings.jqAutoSourceInputValue || valueProp;
//if we are writing a different property to the input than we are writing to the model, then locate the object
if (valueProp && inputValueProp !== valueProp) {
var source = unwrap(allBindings.jqAutoSource) || [];
var modelValue = ko.utils.arrayFirst(source, function (item) {
return unwrap(item[valueProp]) === modelValue;
}) || {}; //probably don't need the || {}, but just protect against a bad value
}
//update the element with the value that should be shown in the input
$(element).val(modelValue && inputValueProp !== valueProp ? unwrap(modelValue[inputValueProp]) : modelValue.toString());
}
}
};
moduleDance(factory);
})();
I have not fully understood your question. But knockout is not relevant to UIComplete. Please see a simple example using UI complete.
async function autocomplete() {
const sthings= await getSthings(); //gets json array, or ajax call, this is a promise
$("#sthHighlightSearch").autocomplete({
source: sthings
});
//This is an extension method for autocomplete
//Should filter the list with starts with characters written in the autocomplete
$.ui.autocomplete.filter = function (array, term) {
var matcher = new RegExp("^" + $.ui.autocomplete.escapeRegex(term), "i");
return $.grep(array, function (value) {
return matcher.test(value.label || value.value || value);
});
};
}
Related
I have set Editors.Text or edit.
{id: "label", name: "name", field: "label",editor: Editors.Text,width: 80},
This enables editing for field on browser.
But how can I catch when editing is finished??
I am checking the event list of slickgrid.
However can't find the appropriate event.
How can I catch the event after editing columns??
It appears that there is no generic event for this - probably not a bad idea to add one. I suspect it is expected that you would write a custom editor and add the event directly to that rather than adding it to the grid.
I assume you want to update some related data or UI when the editing is complete?
[Edit]
The editor has events encapsulated in it for doing this - the grid uses a plugin model with loadValue and applyValue to read/write the data source. I'll post an example of my personal text editor here, as it may help. Note that I have written a data provider for my personal grid to allow it to interface to several custom data objects - this isn't in the standard one you should be using, which is here.
function TextEditor(args) {
var $input;
var defaultValue;
var scope = this;
this.init = function () {
$input = $("<INPUT type=text class='editor-text' />")
.appendTo(args.container)
.on("keydown.nav", function (e) {
if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {
e.stopImmediatePropagation();
}
})
.focus()
.select();
$input.width(args.container.clientWidth); /* mod */
};
this.destroy = function () {
$input.remove();
};
this.focus = function () {
$input.focus();
};
this.getValue = function () {
return $input.val();
};
this.setValue = function (val) {
$input.val(val);
};
this.loadValue = function () {
defaultValue = args.grid.getDataProvider().getValueByColName(args.rowIndex, args.column.field);
defaultValue = defaultValue || "";
$input.val(defaultValue);
$input[0].defaultValue = defaultValue;
$input.select();
};
this.serializeValue = function () {
return $input.val();
};
this.applyValue = function (state) {
args.grid.getDataProvider().setValueByColName(args.rowIndex, args.column.field, state);
};
this.isValueChanged = function () {
return (!($input.val() == "" && defaultValue == null)) && ($input.val() != defaultValue);
};
this.validate = function () {
if (args.column.validator) {
var validationResults = args.column.validator($input.val());
if (!validationResults.valid) {
return validationResults;
}
}
return {
valid: true,
msg: null
};
};
this.init();
}
After much research and trail and error, I haven't come up with a solution yet. Please help! The SearchCustomer method in the code has comments on the scenarios that work and don't work.
Situation
I use knockoutjs with the mapping plugin. I take a view model which contains a Workorder from the server and it contains some properties about it along with a Customer model underneath it and a Contact model underneath Customer.
On the workorder screen the user can search for a customer which pops up a modal search window. They select that customer and the customer's id and customer model comes back to the workorder. I update the workorder's customerID no problem, but when I try to update the customer data (including contact) I get the Function Expected error.
Code
function WorkorderViewModel(data) {
var self = this;
data = data || {};
mapping = {
'Workorder': {
create: function (options) {
return new Workorder(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
self.ViewCustomer = function () {
self.Workorder.Customer.View();
}
self.SearchCustomer = function () {
self.Workorder.Customer.Search(function (customerID, customer) {
self.Workorder.CustomerID(customerID); //Works
self.Workorder.Customer(customer) //Function Expected, I feel this should work! Please help!
self.Workorder.Customer = new Customer(customer, self.Workorder); //No Error doesn't update screen
self.Workorder.Customer.Contact.FirstName(customer.Contact.FirstName); //Works, updates screen, but I don't want to do this for every property.
self.Workorder.SaveAll(); //Works, reload page and customer data is correct. Not looking to reload webpage everytime though.
})
}
}
function Workorder(data, parent) {
var self = this;
data = data || {};
mapping = {
'Customer': {
create: function (options) {
return new Customer(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
}
function Customer(data, parent) {
var self = this;
data = data || {};
mapping = {
'Contact': {
create: function (options) {
return new Contact(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
}
function Contact(data, parent) {
var self = this;
data = data || {};
mapping = {};
ko.mapping.fromJS(data, mapping, self);
self.AddedOn = ko.observable(moment(data.AddedOn).year() == 1 ? '' : moment(data.AddedOn).format('MM/DD/YYYY'));
self.FullName = ko.computed(function () {
var fullName = '';
if (self.FirstName() != null && self.FirstName() != '') {
fullName = self.FirstName();
}
if (self.MiddleName() != null && self.MiddleName() != '') {
fullName += ' ' + self.MiddleName();
}
if (self.LastName() != null && self.LastName() != '') {
fullName += ' ' + self.LastName();
}
return fullName;
})
}
Thanks Everyone!
Since self.Workorder.Customer is originally populated using ko.mapping, when you want to repopulate it, you should use ko.mapping again, like:
ko.mapping.fromJS(customer, self.Workorder.Customer)
Try changing:
self.Workorder.Customer(customer);
to:
self.Workorder.Customer = customer;
My guess is that the Customer property of the Workorder is not an observable.
I am having difficulties with this handler I have partially gotten from here and partially hacked together. I am still getting my bearings on the handlers so I am assuming my issues is stemming from a lack of understanding.
I am using this handler in a template that is displayed inside a ko "if" statement. When the template is being included/excluded on and off options are duplicating. This is because of the unwrap(valueAccessor()).push(item) line. I have tried just building the array independantly and then setting the valueAccessors value to the array directly but the ui does not respond, it only works by pushing the items.
How can I get around this? Am I doing the binding correctly or is there a better more appropriate way? I have marked the line of code with a comment to indicate where my problem is happening.
multiSelectCheck: {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
// This will be called when the binding is first applied to an element
// Set up any initial state, event handlers, etc. here
var bindings = allBindingsAccessor();
var ddOptions = unwrap(valueAccessor);
var selectedOptions = unwrap(bindings.selectedOptions);
var options = unwrap(bindings.multiselectOptions) || [];
var optionsCaption = unwrap(bindings.optionsCaption);
var displaySelected = unwrap(bindings.selectedList) || 5;
var loadingCaption = unwrap(bindings.loadingCaption);
var delimiter = unwrap(bindings.splitSelectedBy) || ',';
var setInitial = unwrap(bindings.setInitialValue);
// display loader in dropdown
ko.computed(function () {
if (unwrap(bindings.loading)) {
var spinnerClass = 'fa fa-spinner fa-spin fa-lg';
var spinner = loadingCaption || '<span><i class="' + spinnerClass + '"></i> Loading...</span>';
// set text to loading
$(element).multiselect({ selectedList: 0, noneSelectedText: spinner, selectedText: spinner }).multiselect('disable');
$(element).multiselect('refresh');
}
}, null, { disposeWhenNodeIsRemoved: element });
// internal options
var internal = { selectedList: displaySelected, noneSelectedText: 'Select options', selectedText: '# selected' }
// merge options with provided options
options = $.extend(internal, options);
// pass the original optionsCaption to the similar widget option
if (optionsCaption) {
options.noneSelectedText = unwrap(optionsCaption);
}
// remove this and use the widget's
bindings.optionsCaption = '';
// populate intitial values if available
if (ddOptions && !ddOptions.length && setInitial) {
if (selectedOptions) {
// create array from value
if (typeof selectedOptions == 'string') {
selectedOptions = selectedOptions.split(delimiter);
selectedOptions = selectedOptions.filter(function (e) { return !!e; }); // filter empty nodes
bindings.selectedOptions(selectedOptions);
}
// add options objects to array of available options
for (var i = 0; i < selectedOptions.length; i++) {
var item = { Value: selectedOptions[i], Text: '' };
//console.log(item);
//#### THIS IS THE LINE OF CODE IN QUESTION ####
unwrap(valueAccessor()).push(item);
}
}
}
// apply multiselect plugin
var elm = $(element).multiselect(options).multiselectfilter({ filterOnly: true, autoReset: true });
// refresh the plugin
$(element).multiselect('refresh');
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
elm.multiselectfilter('destroy').multiselect("destroy");
$(element).remove();
});
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
// This will be called once when the binding is first applied to an element,
// and again whenever the associated observable changes value.
// Update the DOM element based on the supplied values here.
var bindings = allBindingsAccessor();
var selectOptions = unwrap(bindings.multiSelectCheck);
var selectedOptions = unwrap(bindings.selectedOptions);
var delimiter = unwrap(bindings.splitSelectedBy) || ',';
var displaySelected = unwrap(bindings.selectedList) || 5;
// remove this and use the widget's
bindings.optionsCaption = '';
// handle delimited values
if (typeof selectedOptions == 'string') {
selectedOptions = selectedOptions.split(delimiter);
selectedOptions = selectedOptions.filter(function (e) { return !!e; }); // filter empty nodes
bindings.selectedOptions(selectedOptions);
}
ko.bindingHandlers.options.update(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
var data = unwrap(valueAccessor());
var showFilter = (data && data.length > 10) ? 'enable' : 'disable';
setTimeout(function () {
var $element = $(element);
$element.multiselect({ selectedList: displaySelected, noneSelectedText: 'Select options', selectedText: '# selected' }).multiselect('enable').multiselect('refresh').multiselectfilter('refresh');
$element.multiselectfilter(showFilter);
$element.multiselect('refresh');
}, 0);
}
}
Try to create temporary array with items and update valueAccessor observable array directly:
var items = [];
for (var i = 0; i < selectedOptions.length; i++) {
items.push({ Value: selectedOptions[i], Text: '' });
}
// items
valueAccessor(items);
So my problem ended up being that I thought I had the array value, but I ended up having the observable instead. This caused my array length check to always pass so it kept adding the initial values.
I had to change the init line
var ddOptions = unwrap(valueAccessor);
To
var ddOptions = unwrap(valueAccessor()); // added () :/
Here goes my View model, which helps to load the items to drop down. Items are getting loaded but when I inspect the element "value" attribute is empty. How can I get selected value?
$(function () {
tss.Department = function (selectedItem) {
var self = this;
self.id = ko.observable();
self.description = ko.observable();
self.isSelected = ko.computed(function () {
return selectedItem() === self;
});
self.stateHasChanged = ko.observable(false);
};
tss.vm = (function () {
var metadata = {
pageTitle: "My App"
},
selectedDepartment = ko.observable(),
departments = ko.observableArray([]),
sortFunction = function (a, b) {
return a.description().toLowerCase() > b.description().toLowerCase() ? 1 : -1;
},
selectDepartment = function (p) {
selectedDepartment(p);
},
loadDepartments = function () {
tss.departmentDataService.getDepartments(tss.vm.loadDepartmentsCallback);
},
loadDepartmentsCallback = function (json) {
$.each(json, function (i, p) {
departments.push(new tss.Department(selectedDepartment)
.id(p.DepartmentId)
.description(p.Description)
);
});
departments.sort(sortFunction);
};
return {
metadata: metadata,
departments: departments,
selectDepartment: selectDepartment,
loadDepartmentsCallback: loadDepartmentsCallback,
loadDepartments: loadDepartments,
choices: choices,
selectedChoice: selectedChoice
};
})();
tss.vm.loadDepartments();
ko.applyBindings(tss.vm);
});
Here is my HTML
<select data-bind="options:departments, value:selectDepartment,
optionsText: 'description', optionsCaption:'Select a product ...'">
</select>
Also sorting is not happening. departmentDataService used to call external data. which has both "id" and "description"
I also tried setting value as 'Id', but did not work.
You should not use an additional function selectDepartment to pass the value to the observable, but instead directly bind the observable to the value property of the select-box:
<select data-bind="options:departments, value:selectedDepartment, ...
(remember to export the selectedDepartment observable)
The value property is not only used to communicate the current value from view to viewmodel, but also vice versa: to set the selected option. Binding to a function that provides only "write" functionality is therefore not sufficient.
If you need to react to changes of the selected department, you can subscribe to the observable (this is explained in the official docs).
So I bind my Knockout template as follows:
First ajax, get data then I pass the data can call a function named bindKo:
function bindKo(data) {
var length = data.length;
var insertRecord = {};
if (length > 0) {
insertRecord = data[data.length - 1]; //last record is an empty PremlimViewModel for insert
insertRecord.Add = true;
data.splice(data.length - 1, 1); //remove that blank insert record
}
function prelims(data) {
var self = this;
var model = ko.mapping.fromJS(data, { copy: ["_destroy"] }, self);
self.BidPriceFormatted = ko.computed({
read: function () {
var bidPrice = this.BidPrice();
if (bidPrice) {
if (!isNaN(bidPrice)) {
var input = '<input type="text" value="' + bidPrice + '"/>';
return $(input).currency({ decimals: 0 }).val();
}
}
},
write: function (value) {
value = value.replace(/\D/g, '');
this.BidPrice(value);
},
owner: this
});
return model;
}
var mapping = {
create: function (options) {
return new prelims(options.data);
}
};
function viewModel(prelimData) {
var self = this;
self.prelims = ko.mapping.fromJS(prelimData, mapping);
self.remove = function (prelim) {
self.prelims.destroy(prelim);
};
self.addOption = function () {
var clone = jQuery.extend(true, {}, insertRecord);
self.prelims.push(ko.mapping.fromJS(clone));
};
}
ViewModel = new viewModel(data);
ko.applyBindings(ViewModel);
}
I have a template defined where you can add and remove records, and user does just that:
<script type="text/html" id="PrelimsTemplate">
<!--Template Goodness-->
</script>
Then, ajax call, records updated in datanbase, latest results returned and I do:
ko.mapping.fromJS(newestData, ViewModel)
But this does not work because my ViewModel is complex.
So I would just like to reBind the template entirely. Make is disappear and reappear with latest data.
Wrap your template in a container than you can hook onto with jQuery.
When you need to trash it use ko.cleanNode and jQuery .empty()
emptyTemplate: function(){
ko.cleanNode($('#template-container')[0]);
$('#template-container').empty();
}
Load your template back up
fillTemplate: function(){
$('#template-container').html('<div data-bind="template: {name:\'templateId\', data: $data}"></div>');
ko.applyBindings(data,$('#template-container')[0])
},
See my fiddle