Hi I am trying to get selected value of dropdown in Knockout js so that I can hide/ show other elements based on selection. Below is what I have tried.
What is happening that I am able to get right value on button click but not on dropdown selection change.
Below is my code. The button gives right value, but dropdown selection change event gives previous value & not the selected one.
JS
function ViewModel() {
var self = this;
self.optionValues= ko.observableArray(["Test1", "Test2", "Test3"]);
self.selectedValue = ko.observable();
self.save = function() {
alert(self.selectedValue());
}
}
ko.applyBindings(new ViewModel());
HTML
<select data-bind="event:{ change: save},options: optionValues, value: selectedValue"></select>
<button data-bind="click: save">Save</button>
Instead of the change event binding binding, you should subscribe directly on your selectedValue observable, and call your logic from there:
function ViewModel() {
var self = this;
self.optionValues = ko.observableArray(["Test1", "Test2", "Test3"]);
self.selectedValue = ko.observable();
self.selectedValue.subscribe(function(newValue) {
self.save();
});
self.save = function() {
alert(self.selectedValue());
}
}
ko.applyBindings(new ViewModel());
Html:
<select data-bind="options: optionValues, value: selectedValue"></select>
<button data-bind="click: save">Save</button>
Demo JSFiddle.
Related
Apparently, Bootstrap multiselect has a limitation when using jQuery multiselect() with Knockout.js, so that if a multiselect dropdown is modified by code during a Knockout event (a click event, in the following example), then the code isn't applied.
The following example demonstrate it:
First, click the button on the left. You'll see that although options are created, they are not selected. You'd need another click to make them selected.
Then, click the button on the right. You'll see that options are both created and selected. I used a 1000 ms timeout, but it works with only 1 ms timeout as well.
My question: Is there a better way than a timeout to make selectAll() work?
var selectorVM = function () {
var self = this;
self.available = ko.observableArray([]);
self.selected = ko.observableArray([]);
self.init = function () {
self.initOptions();
self.selectAll();
};
self.initWithTimeout = function () {
self.initOptions();
self.selectAllWithTimeout();
};
self.initOptions = function () {
self.available([]);
self.available([
{ name: "option 1", value: 1},
{ name: "option 2", value: 2}
]);
};
self.selectAll = function () {
var $selector = $("#selector");
$selector.multiselect('selectAll', false);
$selector.multiselect('updateButtonText');
};
self.selectAllWithTimeout = function () {
setTimeout(self.selectAll, 1000);
};
}
var selectorVM = new selectorVM();
ko.applyBindings(selectorVM);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.15/js/bootstrap-multiselect.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.15/css/bootstrap-multiselect.css" rel="stylesheet"/>
<div>
<button class="btn btn-default" data-bind="click: init">Click to init dropdown, no timeout</button>
<button class="btn btn-default" data-bind="click: initWithTimeout">Click to init dropdown, with timeout</button>
</div>
<div>
<select id="selector"
class="form-control"
multiple="multiple"
data-bind="options: available,
optionsText: 'name',
optionsValue: 'value',
selectedOptions: selected,
multiselect: { includeSelectAllOption: true }">
</select>
</div>
Your problem lies with code execution order. The built in KO options binding handler and the jQuery Multiselect plugin are both modifying the DOM, and the options handler probably executes after the multiselect function, so when you build the multiselect menu, there aren't yet any options for it to pick up. When you use setTimeout, even with a 1ms timeout, the function gets pushed to the end of the call stack and will execute when the current event loop has finished (see here for more details), so now it is executed after the options binding has finished. Ergo, it works.
However, that's not very pretty. It's never a good idea to perform DOM manipulation inside your view model. That's why custom binding handlers are a thing; to easily interact with DOM elements and keep your view models clean. Your code references a multiselect binding handler, but I don't see it in your post. If we create it (it's not complicated), we'll see we can make the menu work as expected. Your original issue was you were using the supplied binding handler as well as manipulating the DOM manually. You should just stick to the binding handler and it'll work fine. No timeouts needed.
With frameworks such as KO, as a general rule of thumb you should always only have to manipulate the data in your view model. KO should do the heavy lifting of updating the UI. Therefore, if you want to select all items, think about how this works: there's an observable array called selected where the ID's of the selected items are stored. So if you want to select all items, you only have to loop through all the available items, and push the ID's to the selected array. KO will take care of updating the UI for you. See the updated code snippet. I have created a separate button that calls the selectAll function, but you could of course just call selectAll in your init function if you wanted to.
var selectorVM = function () {
var self = this;
self.available = ko.observableArray([]);
self.selected = ko.observableArray([]);
self.init = function () {
self.initOptions();
};
self.initOptions = function () {
self.available([
{ name: "option 1", value: 1 },
{ name: "option 2", value: 2 }
]);
};
self.selectAll = function () {
self.available().forEach(function (opt) {
if (self.selected.indexOf(opt.value) === -1) {
self.selected.push(opt.value);
}
});
}
}
var selectorVM = new selectorVM();
ko.applyBindings(selectorVM);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.15/js/bootstrap-multiselect.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.15/css/bootstrap-multiselect.css" rel="stylesheet"/>
<div>
<button class="btn btn-default" data-bind="click: init">Click to init dropdown, no timeout</button>
<button class="btn btn-default" data-bind="click: selectAll">Select all</button>
</div>
<div>
<select id="selector"
class="form-control"
multiple="multiple"
data-bind="options: available,
optionsText: 'name',
optionsValue: 'value',
selectedOptions: selected,
multiselect: { includeSelectAllOption: true }">
</select>
</div>
<p>Selected options in observable array:</p>
<ul data-bind="foreach: selected"><li data-bind="text: $data"></li></ul>
Here's my code: http://codepen.io/kikibres/pen/mVYOaR
I’m trying to recode the select options to display the “X” button when it’s selected and when one click on the “X” button, it reset back to the original select value. Additionally, I wanted the "x" button to work only on their own dropdown menu instead of all dropdown menus.
I’m trying to make it more similar to http://www.mtommaneycentre.com.au/stores/. As you can see, when you click on an option in “A-Z” select field, an “X” button appears and when you click on the “X” button, it reset to the original “A-Z” select option….
How do one do that?
HTML
<div class="filtermenu">
<form class="controls" id="Filters">
<fieldset class="select-style">
<select>
<option value="">All</option>
<option value=".triangle">Triangle</option>
<option value=".square">Square</option>
<option value=".circle">Circle</option>
</select>
<button class="btn-clear">x</button>
</fieldset>
<fieldset class="select-style">
<select>
<option value="">All</option>
<option value=".blue">Blue</option>
<option value=".white">White</option>
<option value=".green">Green</option>
</select>
<button class="btn-clear">x</button>
</fieldset>
<fieldset>
<button class="filter" data-filter=".triangle">Triangle</button>
</fieldset>
<fieldset>
<input type="text" placeholder="Enter Name" val="" data-filter="" id="filter--text" />
</fieldset>
<button id="Reset">Clear Filters</button>
</form>
</div>
Javascript
The code that reset all options is
self.$reset.on('click', function(e){
e.preventDefault();
in
var dropdownFilter = {
// Declare any variables we will need as properties of the object
$filters: null,
$reset: null,
groups: [],
outputArray: [],
outputString: '',
// The "init" method will run on document ready and cache any jQuery objects we will need.
init: function(){
var self = this; // As a best practice, in each method we will asign "this" to the variable "self" so that it remains scope-agnostic. We will use it to refer to the parent "dropdownFilter" object so that we can share methods and properties between all parts of the object.
self.$filters = $('#Filters');
self.$reset = $('#Reset');
self.$container = $('#Container');
self.$filters.find('fieldset').each(function(){
var $this = $(this);
self.groups.push({
$buttons : $this.find('.filter'),
$inputsSelect : $this.find('select'),
$inputsText : $this.find('input[type="text"]'),
active : ''
});
});
self.bindHandlers();
},
// The "bindHandlers" method will listen for whenever a select is changed.
bindHandlers: function(){
var self = this;
// Handle select change
self.$filters.on('click', '.filter', function(e){
e.preventDefault();
var $button = $(this);
// If the button is active, remove the active class, else make active and deactivate others.
$button.hasClass('active') ?
$button.removeClass('active') :
$button.addClass('active').siblings('.filter').removeClass('active');
self.parseFilters();
});
// Handle dropdown change
self.$filters.on('change', function(){
self.parseFilters();
});
// Handle key up on inputs
self.$filters.on('keyup', 'input[type="text"]', function() {
var $input = $(this);
console.log($input.val());
$input.attr('data-filter', '[class*="'+$input.val().replace(/ /, '-')+'"]');
if ($input.val() == '')
$input.attr('data-filter', '');
console.log($input.attr('data-filter'));
self.parseFilters();
});
// Handle reset click
self.$reset.on('click', function(e){
e.preventDefault();
self.$filters.find('.filter').removeClass('active');
self.$filters.find('.show-all').addClass('active');
self.$filters.find('select').val('');
self.$filters.find('input[type="text"]').val('').attr('data-filter', '');
self.parseFilters();
});
},
// The parseFilters method pulls the value of each active select option
parseFilters: function(){
var self = this;
// loop through each filter group and grap the value from each one.
for(var i = 0, group; group = self.groups[i]; i++){
var activeButtons = group.$buttons.length ? group.$buttons.filter('.active').attr('data-filter') || '' : '';
var activeSelect = group.$inputsSelect.length ? group.$inputsSelect.val() || '' : '';
var activeText = group.$inputsText.length ? group.$inputsText.attr('data-filter') : '';
group.active = activeButtons+activeSelect+activeText;
console.log(group.active);
}
self.concatenate();
},
// The "concatenate" method will crawl through each group, concatenating filters as desired:
concatenate: function(){
var self = this;
self.outputString = ''; // Reset output string
for(var i = 0, group; group = self.groups[i]; i++){
self.outputString += group.active;
}
// If the output string is empty, show all rather than none:
!self.outputString.length && (self.outputString = 'all');
console.log(self.outputString);
// ^ we can check the console here to take a look at the filter string that is produced
// Send the output string to MixItUp via the 'filter' method:
if(self.$container.mixItUp('isLoaded')){
self.$container.mixItUp('filter', self.outputString);
}
}
};
// On document ready, initialise our code.
$(function(){
// Initialize dropdownFilter code
dropdownFilter.init();
// Instantiate MixItUp
$('#Container').mixItUp({
controls: {
enable: false // we won't be needing these
},
callbacks: {
onMixFail: function(){
alert('No items were found matching the selected filters.');
}
}
});
});
At the moment there isn't attached any event handler to your "x" buttons, so if you click on them, it just refreshes the page, that's why it resets all filters, because it reinitialize all the code.
You should add this code to your plugin
$('.btn-clear').on('click', function(event) {
event.preventDefault();
$(this).prev().val("").change();
});
The event.preventDefault() will prevent to run the default event when you click on the button, which is to refresh the page in this case.
The next line will points to the previous DOM element of the clicked element (which is the <select>) and changes to the option which has the value: "", and after it triggers a change() event to tell your written code that the select's value is changed, so it should reload the figures with new filters.
I saved it here: http://codepen.io/anon/pen/EPzWEQ?editors=1010
Of Course you should implement it to your plugin, I just wrote it to the bottom of the code to make it work.
EDIT:
If you want to make invisible the X buttons if there is no filter selected, just make a 'change' event handler on the select, and check if value is "" then hide the button, else show it. It should be look like this:
$('select').change(function() {
if ($(this).val() == "") {
$(this).next().hide();
} else {
$(this).next().show();
}
});
I have a knockout view model that is set up as an observable as my main view model. In the child element, it seems that I can't set up data-bind="click: the same way that I can when I am in the parent element.
My html:
<button id="myButton" type="button" class="btn btn-lg btn-primary" data-bind="click: test">Click Me</button>
In my main view model:
self.childElement = ko.observable(new childElementVm());
and in childElementVm
var childElementVm= function () {
var test = function(){
alert('this is a test');
}
}
what do I need to do differently to use data-bind="click: test" here?
To note, my applyBindings is fine (other knockout observables are functioning correctly) and the button is contained inside of a <div data-bind="with: childElement"
EDIT: here is a fiddle
Your test function is scoped to the childElementVmonly. Change your implementation to this:
var childElementVm= function () {
this.test = function(){
alert('this is a test');
}
}
or this:
var childElementVm= function () {
var self = this;
self.test = function(){
alert('this is a test');
}
}
Here is a working example
I want to call the onHover function but with no suceess. The only way I managed to do this is if the function is global, but this is no what I need.
What I am trying to do is onMouseOver of some element of the dropdown to take its value and do something with it in my viewmodel.
HTML:
<div>
<div data-bind="with: myInnerViewModel">
<input type="text" data-bind="kendoDropDownList: {data: myData, value: myValue,template:'<span onMouseOver = \'onHover(this)\' title=\'${data}\'>${data}</span>'}" />
<div>
</div>
JS:
var myViewModel = function () {
this.myInnerViewModel = {
myData : [1, 2 , 3],
myValue : ko.observable(1),
onHover : function(e){
alert(1);
}
};
};
ko.applyBindings(new myViewModel());
fiddler: http://jsfiddle.net/QZWPR/30/
The issue lies with location of your function. If you were to change
onMouseOver = \'onHover(this)\'
to
onMouseOver = \'myViewModel.myInnerViewModel.onHover(this)\'
then i expect you would see your alert.
OR
you need to use the event binding on your span
<span data-bind="event: { mouseover: onHover}">
I've got a very simple View Model:
var ViewModel = function() {
this.showRow = ko.observable(false);
this.toggleVisibility = function() {
if(this.showRow == true){
this.showRow = false;
}
else{
this.showRow = true;
}
alert('showRow is now '+this.showRow); //only here for testing
};
};
with equally simple markup:
Toggle
<br />
<table>
<tr data-bind="visible: showRow">Some Text</tr>
</table>
My problem is, that when the link is clicked, the alert box shows (displaying the correct value - true/false)
However, the visible binding on the tr element doesn't seem to work - either initially (the row should be invisible on load) nor when the value of showRow toggles.
jsFiddle of above- http://jsfiddle.net/alexjamesbrown/FgVxY/3/
You need to modify your html as follows:
<table>
<tr data-bind="visible: showRow"><td>Some Text</td></tr>
</table>
And JavaScript as follows:
var ViewModel = function() {
var self = this;
self.showRow = ko.observable(false);
self.toggleVisibility = function() {
self.showRow(!self.showRow());
alert('showRow is now ' + self.showRow());
};
};
ko.applyBindings(new ViewModel());
Syntax to set the value to observable property is: self.showRow(value);
If need to have tags inside of tags.
I've also modified your fiddle to simplify the javascript and follow newer code practices with regard to "this". See http://jsfiddle.net/FgVxY/4/