Knockout multiselect dropdown with lazy loading - javascript

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 () :/

Related

How to create an autocomplete combobox with KnockoutJS and JQuery UI

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);
});
};
}

Knockout js unable to remove object from observableArray

I've got two ways that I'm filling in an observableArray, one for testing purposes and one for the way I intend on using this array.
The first way I'm defining these objects and pushing them in one at a time, the second way I'm reading a JSON stream and pushing them in with a loop.
Here's my code for this shuttle-menu I'm using.
var StateModel = function() {
var self = this;
// initialize containers
self.leftStateBox = ko.observableArray();
self.rightStateBox = ko.observableArray();
// selected ids
self.selectedLeftStateBox = ko.observableArray();
self.selectedRightStateBox = ko.observableArray();
self.moveLeft = function () {
var sel = self.selectedRightStateBox();
for (var i = 0; i < sel.length; i++) {
var selCat = sel[i];
var result = self.rightStateBox.remove(function (item) {
return item.id == selCat;
});
if (result && result.length > 0) {
self.leftStateBox.push(result[0]);
}
}
self.selectedRightStateBox.removeAll();
}
self.moveRight = function () {
var sel = self.selectedLeftStateBox();
for (var i = 0; i < sel.length; i++) {
var selCat = sel[i];
var result = self.leftStateBox.remove(function (item) {
return item.id == selCat;
});
if (result && result.length > 0) {
self.rightStateBox.push(result[0]);
}
}
self.selectedLeftStateBox.removeAll();
}
self.leftStateBox.push({
id: "CAA"
, name: 'State 1'
});
self.leftStateBox.push({
id: "VAA"
, name: 'State 2'
});
self.leftStateBox.push({
id: "BAA"
, name: 'State 3'
});
self.loadStates = function() {
var self = this;
$.getJSON("${baseAppUrl}/public/company/" + companyId + "/json/searchStates/list",
function (searchStatesData) {
var states = JSON.parse(searchStatesData).searchStates;
for(var i = 0; i < states.length; i++) {
self.leftStateBox.push(new State(states[i]));
}
});
};
self.loadStates();
}
var State = function (state) {
var self = this;
self.name = ko.observable(state.name);
self.id = ko.observable(state.id);
}
$(function () {
ko.applyBindings(new StateModel(), document.getElementById("statesBox"));
});
Here's my view section:
'<div id="statesBox">
<div>
Available States:
<select multiple='multiple' data-bind="options: leftStateBox, optionsText: 'name', optionsValue: 'id', selectedOptions: selectedLeftStateBox"></select>
</div>
<div>
<p><button data-bind="click: moveRight">Add Selected</button></p>
<p><button data-bind="click: moveLeft">Remove Selected</button></p>
</div>
<div>
Selected States:
<select multiple='multiple' data-bind="options: rightStateBox, optionsText: 'name', optionsValue: 'id', selectedOptions: selectedRightStateBox"></select>
</div>
<br /><br />
</div>'
When I try to shuttle things back and forth on the list it works for the three I manually entered in but it doesn't work for the ones imported through the JSON call. They all show up on the list though and seem to have the same information, I structured the manually created objects after how the JSON objects look. When I trace the JS function moveRight, the remove works for the manually created objects but fails on the imported ones. I'm not really sure what I'm doing wrong at this point, has anyone seen something like this?
I grabbed the shuttle menu code from this post
The items you're adding have an important difference.
self.leftStateBox.push({
id: "BAA"
, name: 'State 3'
});
...
var State = function (state) {
var self = this;
self.name = ko.observable(state.name);
self.id = ko.observable(state.id);
}
The first have non-observable property values and the second have observable property values. In general, you should only make things observable if they need to be so (are you ever going to want to change the name or id of an item?).
var State = function (state) {
var self = this;
self.name = state.name;
self.id = state.id;
}
If a property is always observable, you can just "unwrap" it directly: item.id(). If it may sometimes be observable, you can use ko.unwrap(item.id).
var result = self.rightStateBox.remove(function (item) {
return ko.unwrap(item.id) == selCat;
});
Manipulating arrays is very easy by using underscore js
you can easily remove an item in KO observable array by the following code.
self.rightStateBox(_.without(self.rightStateBox(), toRemove));
toRemove is the object to remove from the array.

JavaScript equivalent to angularjs ng-if

If I want to remove/add element on DOM I just use ng-if and the code under it does not compile into to DOM, can I do the same using pure js? I don't want the HTML code inside my js code.
Hiding it using CSS:
<div id = "infoPage" style="display: none;">
Will still insert the element to the DOM.
EDIT
The condition for showing or not is based on a flag like:
var show = false; //or true
You can try something like this:
Idea:
Create an object that holds reference of currentElement and its parent (so you know where to add).
Create a clone of current element as you want to add same element after its removed.
Create a property using Object.defineProperty. This way you can have your own setter and you can observe changes over it.
To toggle element, check
If value is true, you have to add element. But check if same element is already available or not to avoid duplication.
If false, remove element.
var CustomNGIf = function(element, callback, propertyName) {
var _value = null;
// Create copies of elements do that you can store/use it in future
this.parent = element.parentNode;
this.element = element;
this.clone = null;
// Create a property that is supposed to be watched
Object.defineProperty(this, propertyName, {
get: function() {
return _value;
},
set: function(value) {
// If same value is passed, do nothing.
if (_value === value) return;
_value = !!value;
this.handleChange(_value);
}
});
this.handleChange = function(value) {
this.clone = this.element.cloneNode(true);
if (_value) {
var index = Array.from(this.parent.children).indexOf(this.element);
// Check if element is already existing or not.
// This can happen if some code breaks before deleting node.
if (index >= 0) return;
this.element = this.clone.cloneNode(true);
this.parent.appendChild(this.element);
} else {
this.element.remove();
}
// For any special handling
callback && callback();
}
}
var div = document.getElementById('infoPage');
const propName = 'value';
var obj = new CustomNGIf(div, function() {
console.log("change")
}, propName);
var count = 0;
var interval = setInterval(function() {
obj[propName] = count++ % 2;
if (count >= 10) {
window.clearInterval(interval);
}
}, 2000)
<div class='content'>
<div id="infoPage"> test </div>
</div>

Save and restore Sortable positions from two lists

I'm using RubaXa's excellent Sortable JS library to allow drag-and-drop rearranging of divs on a Bootstrap-based dashboard. Since the divs are all in 2 columns (left and right), I have the columns defined with ids of "leftColumn" and "rightColumn".
In order to allow dragging between columns, I set up both sortables with the same group, like this:
Sortable.create(leftColumn, {
group: 'dash_sections',
});
Sortable.create(rightColumn, {
group: 'dash_sections',
});
Now I am trying to load and save the order from both lists (the entire group). I placed data-id fields in each of the div tags, and I'm trying to use the following code to save and restore the order of everything.
Sortable.create(rightColumn, {
group: 'dash_sections',
store: {
get: function (sortable) {
var order = localStorage.getItem(sortable.options.group);
return order ? order.split('|') : [];
},
set: function (sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.group, order.join('|'));
}
}
});
However, I'm only saving and restoring the order for that column, not the entire group. I eventually want to have the group's order stored in a single string in the database. How do I go about saving and restoring the entire group's order?
Update:
I put similar code in both sortable.create functions, using "leftcol" and "rightcol" instead of sortable.options.group. This properly saves the order of each sortable as long as you don't drag between columns. I'm still looking for a way to save the order even when dragging between columns.
Here's how I implemented a similar functionality
Introduced a category flag to sortable options:
var leftColumnOptions = {
group: "dash_sections",
category: "left_column",
store: setupStore()
};
var rightColumnOptions = {
group: "dash_sections",
category: "right_column",
store: setupStore()
}
setupStore function checks for localStorage availability and applies get and set
function setupStore() {
if (localStorageAvailable) { // basic localStorage check: (typeof (localStorage) !== "undefined")
return {
get: getValue,
set: setValue
};
}
return {};
}
getValue and setValue retreive and store item ids based on category name defined in options above
function getValue(sortable) {
var order = localStorage.getItem(sortable.options.category);
return order ? order.split('|') : [];
}
function setValue(sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.category, order.join('|'));
}
It is a good idea to check for stored order information in localStorage before initializing Sortable, I'm using lodash for convenience
function applyState($section, categoryName) {
if (localStorageAvailable) {
var order = localStorage.getItem(categoryName);
var itemIds = order ? order.split('|') : [];
var $items = _.map(itemIds, function(itemId, index) {
return $("[data-id='" + itemId + "'");
});
$section.append($items);
}
}
usage would be:
applyState($(".js-left-column"), "left_column");
applyState($(".js-right-column"), "right_column");
// initialize sortable
Entire code:
HTML:
<div class="js-two-column-sortable js-left-column" data-category="left_column">
<!-- elements -->
</div>
<div class="js-two-column-sortable js-right-column" data-category="right_column">
<!-- elements -->
</div>
JS:
var localStorageAvailable = (typeof localStorage !== "undefined");
function getValue(sortable) {
var order = localStorage.getItem(sortable.options.category);
return order ? order.split('|') : [];
}
function setValue(sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.category, order.join('|'));
}
function setupStore() {
if (localStorageAvailable) {
return {
get: getValue,
set: setValue
};
}
return {};
}
function onAdd(evt) {
setValue(this);
}
function applyState($section, categoryName) {
if (localStorageAvailable) {
var order = localStorage.getItem(categoryName);
var itemIds = order ? order.split('|') : [];
var $items = _.map(itemIds, function(itemId, index) {
return $("[data-id='" + itemId + "'");
});
$section.append($items);
}
}
var options = {
group: "two-column-sortable",
store: setupStore(),
onAdd: onAdd
};
function init() {
$(".js-two-column-sortable").each(function(index, section) {
var $section = $(section);
var category = $section.attr("data-category");
var sortableOptions = _.extend({}, options, { category: category });
applyState($section, category);
$section.data("twoColumnSortable", new Sortable(section, sortableOptions));
});
}
init();

Knockout Custom binding for Timepart of date

I'm new to Knockout. I have a Viewmodel like this:
var Booking = function(data) {
var self = this;
self.BookingID = ko.observable(data.BookingID);
self.BookingCustomerOrderID = ko.observable(data.BookingCustomerOrderID);
self.BookingName = ko.observable(data.BookingName);
self.BookingTreatmentGroupID = ko.observable(data.BookingTreatmentGroupID);
self.BookingStartTime = ko.observable(data.BookingStratTime);
self.BookingEndTime = ko.observable(data.BookingEndTime);
In my form I'd like to let the user change time of the StartTime and EndTime. Can I make a custom binding to just the time part leaving the datepart from the input field and make this update the model?
To just display the value this works fine, but this does not update the viewmodel.
ko.bindingHandlers.timeVal = {
update: function (element, valueAccessor) {
var value = valueAccessor();
var date = moment(value());
var strDate = date.format('HH:mm');
$(element).val(strDate);
}
};
Looking at the documentation, you can register an event handler onto the element in your init method, and use that to update your observable. Something like this:
ko.bindingHandlers.hasFocus = {
init: function(element, valueAccessor) {
$(element).change(function() {
if(this.val() != valueAccessor())
{
this.val(valueAccessor());
}
});
},
update: function(element, valueAccessor) {
var value = valueAccessor();
var date = moment(value());
var strDate = date.format('HH:mm');
$(element).val(strDate);
}
};

Categories