I have set up the following select for changing semesters on page. When the select detects a change, the changeSemesters function is fired, which runs an AJAX that replaces the current data on the page with data specific to the selected semester.
View
<select data-bind="options: semestersArr, optionsText: 'name', optionsValue: 'id', value: selectedSemester, event: { change: changeSemesters }"></select>
ViewModel
this.selectedSemester = ko.observable();
//runs on page load
var getSemesters = function() {
var self = this, current;
return $.get('api/semesters', function(data) {
self.semestersArr(ko.utils.arrayMap(data.semesters, function(semester) {
if (semester.current) current = semester.id;
return new Model.Semester(semester);
}));
self.selectedSemester(current);
});
};
var changeSemesters = function() {
// run ajax to get new data
};
The problem is that the change event in the select fires the changeSemester function when the page loads and sets the default value. Is there a way to avoid that without the use of a button?
Generally what you want to do in these scenarios is to use a manual subscription, so that you can react to the observable changing rather than the change event. Observables will only notify when their value actually changes.
So, you would do:
this.selectedSemester.subscribe(changeSemesters, this);
If selectedSemester is still changing as a result of getting bound, then you can initialize it to the default value.
this.selectedSemester = ko.observable(someDefaultValue);
Related
Let's say I have this:
ko.bindingHandlers.test= {
update: function (element, valueAccessor) {
alert("Test");
}
};
The alert fires every time an observable is changed, but also initally when the binding is first evaluated. How can I make the alert fire on every change except initially?
Here's one way: keep an array of elements that the update populates with its element if it's not there (which is the first time it runs) and otherwise does whatever action. Since you've got a custom binding handler, you can just hang the array off of that.
ko.bindingHandlers.test = {
update: function(element, valueAccessor) {
var seenElements = ko.bindingHandlers.test.seen,
val = valueAccessor()();
if (seenElements.indexOf(element) >= 0) {
alert("Test");
} else {
seenElements.push(element);
}
}
};
ko.bindingHandlers.test.seen = [];
var vm = {
alertOn: ko.observable(0),
raiseAlert: function() {
vm.alertOn.notifySubscribers();
}
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="test:alertOn"></div>
<button data-bind="click:raiseAlert">Update</button>
How can I make the alert fire on every change except initially?
Knockout will call the update callback initially when the binding is applied to an element and track any dependencies (observables/computeds) that you access. When any of these dependencies change, the update callback will be called once again.
I don't think it is possible to fire only by changes and not in initially.
You can create a workaround to fit with your scenario by adding change event tied up with your element inside init.
init: function(element, valueAccessor) {
// element should be the element you want to attach change event.
$(element).on('change', function (value)
{
// do your stuff.
});
}
I want to load my page based on default item selected in the combobox and reload on changing selected item (knockout).
Currently what I am doing is loading the form on click of a button. Here is my code. Please suggest me as I am very new to knockout.
html:
<select data-bind="options : employeeList, optionsText : 'name', value : selectedEmployeeList"></select>
</span>
<button class="toolbar-button" data-bind="click : load, disable :loading">Load employee</button>
js:
var filterVM = function () {
this.employeeList = ko.observableArray();
this.selectedEmployeeList = ko.observable();
};
dataService.getEmployeeLists(this.viewData.EmployeeId).then(loadEmployeeLists).then(enableControls);
filterVM.prototype.load = function () {
var selectedEmployeeList = this.selectedEmployeeList();
var self = this;
if (selectedEmployeeList) {
disableControls();
dataService.getEmployeeByEmployeeList(this.viewData.employeeId, selectedEmployeeList.id)
.done(loadPEmployeeSpread);
}
};
You can call your load function on change of selected value selectedEmployeeList
Add following code in constructor
this.selectedEmployeeList .subscribe(function(newValue) {
//This function is going to get called on every change of the selectedEmployeeList
//Add here your code to load the page
}, this);
subscribe :- is facility provided by knockout that it tracks every change made in the subscribed variable and calls function on every change.
I use select2 and knockoutJs with this simple binding:
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var options = ko.toJS(valueAccessor()) || {};
setTimeout(function () {
$(element).select2(options);
}, 0);
}
};
Markup:
<select class="select2" style="width:100%" data-bind="optionsCaption: '',options: $root.items,optionsText: 'description',optionsValue: 'id', value: $root.selectedItem,select2: { placeholder: 'Select an item...',allowClear: true }"></select>
it works! Now I enabled the allowClear option in Select2 to clear dropdown to a placeholder value like Select an item....
If I click the x icon dropdown properly sets the placeholder but knockout don't update observable binded value!
I think I've to change custombinding adding something like this:
setTimeout(function () {
$(element).select2(options).on("select2-removed", function (e) {
ko.bindingHandlers.value.update(element, function () { return ''; });
});
...
but it won't work!
There are couple of issues.
1) the update in bindinghandler is to update DOM based on value change, you should never define an update callback with the ability to mutate your model.
The right way is, when define a new bindinghandler, use init callback to hook up all change events with the model, the update callback is merely a DOM drawing routine.
If your init provided DOM drawing (such as provided by select2), you don't need to define an update callback.
Hence the line ko.bindingHandlers.value.update(element, function () { return ''; }); only redraw the DOM, it doesn't do what you want.
2) the select2 binding you created has some holes.
first, value binding doesn't know the existence of select2 binding, that's where you struggled.
second, your select2 binding has to wait for other binding (the options binding) to finish DOM creation, what's why you use setTimeout. But ko provided a way to define sequence of the bindings, just look the source code of ko value binding, it's defined as 'after': ['options', 'foreach']
third, your select2 doesn't respond to outside change. For instance, if you have another UI to update $root.selectedItem (a normal select list), the change raised by that UI would not sync back to your select2.
The solution
Define select2 binding based on existing value binding (just found out it's not needed), and hook up all change events.
we don't need "select2-removed" event, it's all about "change" event.
select2 provided all drawing, we don't need update callback.
use a flag shouldIgnore to break the loop between value subscriber and select2 change event handler.
http://jsfiddle.net/huocp/8N3zX/6/
http://jsfiddle.net/huocp/8N3zX/9/
ko.bindingHandlers.valueSelect2 = {
'after': ['options'],
'init': function(element, valueAccessor, allBindings) {
// kind of extend value binding
// ko.bindingHandlers.value.init(element, valueAccessor, allBindings);
var options = allBindings.get('select2Options') || {};
$(element).select2(options);
var value = valueAccessor();
// init val
$(element).val(ko.unwrap(value)).trigger("change");
var changeListener;
if (ko.isObservable(value)) {
var shouldIgnore = false;
changeListener = value.subscribe(function(newVal) {
if (! shouldIgnore) {
shouldIgnore = true;
// this is just select2 syntax
$(element).val(newVal).trigger("change");
shouldIgnore = false;
}
});
// this demo only works on single select.
$(element).on("change", function(e) {
if (! shouldIgnore) {
shouldIgnore = true;
if (e.val == '') {
// select2 use empty string for unselected value
// it could cause problem when you really want '' as a valid option
value(undefined);
} else {
value(e.val);
}
shouldIgnore = false;
}
});
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
if (changeListener) changeListener.dispose();
$(element).select2("destory");
});
}
};
I have a select control bound to a Knockout observable array:
<select data-bind="event: { change: selectedProductOfferingChange }, options: $parent.productTypes, optionsText: 'text', optionsCaption: '-- Select --', value: selectedProductType, enable: !isReadOnly()"></select>
When the selection is changed, I want to run some code, perhaps make an AJAX call. If the change is not allowed, I want to cancel the change and display a modal dialog. I can't subscribe to the property as that will fire after the change has taken place. I would need the new value to determine if the change should be cancelled or not.
I tried the following in the viewmodel but the change is not cancelled though the property (selectedProductOffering) on the viewmodel is not updated:
self.selectedProductOfferingChange = function (data, event) {
event.stopImmediatePropagation();
return false;
};
Could I use the "beforeChange" option with subscribe?
self.selectedProductType.subscribe(function (previous) {
}, self, "beforeChange");
Can the change be cancelled here?
After some thought, here's what I came up with:
self.selectedProductOfferingChange = function (data, e) {
// Do any checking here
if (confirm("OK to make this change ?")) { return; }
// This stops the viewmodel property from being updated
e.stopImmediatePropagation();
// Since the viewmodel property hasn't changed, force the view to update
self.selectedProductType.valueHasMutated();
};
The problem with this code is that it doesn't give you access to the new value. A computed will solve this issue:
<select data-bind="options: $parent.productTypes, optionsText: 'text', optionsCaption: '-- Select --', value: computedSelectedProductType, enable: !isReadOnly()"></select>
self.computedSelectedProductType = ko.computed({
read: function () {
return self.selectedProductType();
},
write: function (value) {
// Do any checks here. If you want to revert to the previous
// value, don't call the following but do call:
// self.selectedProductType.valueHasMutated()
self.selectedProductType(value);
},
owner: self
});
I have a checkbox that I would like to trigger a simple 'select all' functionality. The problem is that I can't figure out how to connect the checkbox's action to an action in my controller so that I can actually update the records.
App.LanguagesController = Ember.ArrayController.extend({
actions: {
toggleAllVisibility: function() {
var newVisibility = !this.get('allAreVisible');
var needingVisibilityChange = this.filterBy('visible', !newVisibility);
needingVisibilityChange.setEach('visible', newVisibility);
}
},
allAreVisible: function(param) {
return this.filterBy('visible', false).get('length') === 0;
}.property('#each.visible'),
});
In my template, I have the following input helper
{{input type='checkbox' checked=allAreVisible}}
This properly updates the checkbox when I change the elements manually (i.e. if all of them are selected, then checkbox updates), but no actions fire when I toggle the checkbox.
It looks like in older versions of Ember.js I could simply add an action parameter to the input helper but that doesn't work anymore. I'm assuming I need to setup something that observes when the computed property attempts to change, but I couldn't find anything in the docs or other help.
I also tried extending checkbox to send the click event:
App.AllLanguagesCheckbox = Ember.Checkbox.extend(Ember.ViewTargetActionSupport, {
click: function() {
this.triggerAction({
action: 'toggleAllVisibility'
});
}
});
And then loaded that in my template with
{{view App.AllLanguagesCheckbox checkedBinding=allAreVisible}}
That allows the checkbox to trigger the action, but does not update based on the computed property in the controller.
I feel like I'm missing something obvious here.
EDIT
Based on kingpin2k's answer below, here's the working controller code:
App.LanguagesController = Ember.ArrayController.extend({
toggleAllVisibility: function() {
var newVisibility = !this.get('controller').get('allAreVisible');
var needingVisibilityChange = this.get('controller').filterBy('visible', !newVisibility);
needingVisibilityChange.setEach('visible', newVisibility);
},
allAreVisible: function(param) {
return this.filterBy('visible', false).get('length') === 0;
}.property('#each.visible'),
});
It's not called with the normal scope so you have to explicitly go through the controller to get the model array, but it works as expected now.
Here's the accompanying input helper:
{{input type='checkbox' checked=allAreVisible change=toggleAllVisibility}}
The problem is your checkbox is connected to a computed property, the computation should derive the value (aka you shouldn't be setting it), which is what would be happening when someone tries to check.
_allAreVisible:false,
allAreVisible: function(param) {
if(this.filterBy('visible', false).get('length') === 0){
// set to true;
// else set to false
}.observes('#each.visible'),
http://emberjs.jsbin.com/abODIKoj/1/edit