In rare cases like this, the use of Knockout's cleanNode
function may be necessary...
Discussion on init
and update
It is crucial to note that after invoking the init
function, Knockout will always proceed with calling the update
function. This implies that in your situation, the applyBindings
method will be invoked twice on the same element
, causing the WorkMethod
to execute twice upon clicking.
The suggested approach, as indicated in this answer by Philip, recommends excluding the applyBindings
call from the update
function. While this resolves most issues, there remains a concern: automatic updates when Title
, State
, or WorkMethod
undergo changes. If this is not problematic for your scenario, consider making these properties non-observable. But assuming they need to be updated...
Maintaining the current view state
Lack of "update support" in your binding arises due to:
In the init
function, you de-reference Title
, State
, and WorkMethod
before applying inner bindings. Consequently, your custom binding receives all update
calls, necessitating re-application of bindings to inner elements. (Refer to if
and with
bindings' source code for insights)
For text
and css
bindings, appropriate handling involves passing a reference to the respective observable rather than the inner value. This enables default bindings to manage updates within their own update
functions.
$(element).append(`<span data-bind="text: ${value}.Title"></span> `);
// ...
ko.applyBindingsToNode(element, {
css: data[valueAccessor()].State
// ...
Similarly, when dealing with the click
event binding, re-application becomes essential since it does not unwrap its valueAccessor... (Do you think this behavior should change?)
To prevent multiple onClick listeners, cleaning the node prior to re-applying bindings is imperative.
Demonstration with necessary enhancements
ko.bindingHandlers.actionButton = {
init: function(element, valueAccessor, allBindingsAccessor, data, context) {
var value = valueAccessor();
if (typeof value === 'object') {
throw (`${value.Title()} binding must be a string.`);
}
var options = allBindingsAccessor().abOptions || {};
$(element).attr('type', 'submit');
$(element).addClass('btn');
// Preserve the observable while allowing for it to be set from the binding's options
if (options.Title) {
data[value].Title(options.Title);
}
$(element).append(`<span data-bind="text: ${value}.Title"></span> `);
$(element).append(`<span class="glyphicon" data-bind="css: ${value}.Glyph"></span>`);
}, update: function(element, valueAccessor, allBindingsAccessor, data, context) {
ko.cleanNode(element);
ko.applyBindingsToNode(element, {
css: data[valueAccessor()].State,
click: data[valueAccessor()].WorkMethod()
});
}
};
var vm = {
Title: ko.observable("title"),
State: ko.observable(""),
WorkMethod: ko.observable(
function() {
console.log("click");
}
),
Glyph: ko.observable("")
};
ko.applyBindings({
"abSaveSchedule": vm,
changeMethod: () => vm.WorkMethod(() => console.log("click2")),
changeTitle: () => vm.Title("New title")
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button data-bind="actionButton: 'abSaveSchedule', abOptions: { Title: 'override title' }"></button>
<button data-bind="click: changeMethod">change work method</button>
<button data-bind="click: changeTitle">change title</button>
While functional, the existing structure may appear cluttered.
Refactored Approach
Here is a revised version accomplishing the same tasks but with enhanced readability:
ko.bindingHandlers.actionButton = {
init: function(element, valueAccessor, allBindingsAccessor, data, context) {
var propName = valueAccessor();
var options = allBindingsAccessor().abOptions || {};
var ctx = data[propName];
if (options.Title) {
data[propName].Title(options.Title);
}
ko.applyBindingsToNode(element, {
attr: {
"type": "submit",
"class": "btn"
},
css: ctx.State,
click: (...args) => ctx.WorkMethod()(...args),
template: {
data: ctx,
nodes: $(`<span data-bind="text:Title"></span>
<span class="glyphicon" data-bind="css: Glyph"></span>`)
}
});
return {
controlsDescendantBindings: true
};
}
};
var vm = {
Title: ko.observable("title"),
State: ko.observable(""),
WorkMethod: ko.observable(
function() {
console.log("click");
}
),
Glyph: ko.observable("")
};
ko.applyBindings({
"abSaveSchedule": vm,
changeMethod: () => vm.WorkMethod(() => console.log("click2")),
changeTitle: () => vm.Title("New title")
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button data-bind="actionButton: 'abSaveSchedule', abOptions: { Title: 'override title' }"></button>
<button data-bind="click: changeMethod">change work method</button>
<button data-bind="click: changeTitle">change title</button>