Update element text using custom binding - javascript

I'm making my first custom binding. I would like to be able to specify what text that appears on an element based on a resource file. Something like this:
var exampleResource = {
hello: 'world'
};
ko.bindingHandlers.resource = {
init: function (element, valueAccessor) {
var value = valueAccessor();
ko.bindingHandlers.text.update(element, function() {
return exampleResource[value] || '';
});
}
};
<span data-bind="resource: 'hello'"></span>
Should I use ko.bindingHandlers.text as above?
Since the resource variable isn't observable, is there any point of adding the update callback for the binding? If I understand it correctly it will only get called if an observable is passed as the value?

You'd need an update if you want to support the input for your binding handler to be dynamic. In your example you don't do that, but you could. Here's an example:
var exampleResource = {
hello: 'world',
goodbye: 'drowl'
};
ko.bindingHandlers.resource = {
update: function (element, valueAccessor) {
var key = ko.utils.unwrapObservable(valueAccessor());
ko.bindingHandlers.text.update(element, function() {
return exampleResource[key] || key;
});
}
};
ko.applyBindings({ myObs: ko.observable('goodbye') });
span { font-weight: bold; color: red; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
Static: <span data-bind="resource: 'hello'"></span>
<hr>
Dynamic: <span data-bind="resource: myObs"></span>
- based on: <select data-bind="value: myObs, options: ['hello', 'goodbye']"></select>
If you don't need this dynamicness you could stick to your old solution. However, in that case I'd question the added value of KnockoutJS for resources in general :-)

Related

Knockout JS - bind css class to an element

I want to achieve dynamic class biding, which should assign and reassign correct class based on variable, putted in this field. The issue I got, when I induce function setRed(), and then setWhite(), both color classes are binded, and of course 1st CSS class is considered.
I've got an element which binding looks like that:
<div data-bind='dxNumberBox: dxCustomCalc'></div>
So far I made elementAttr class ko.observable();
self.dxCustomCalc = {
displayExpr: 'Name',
keyExpr: 'Id',
value: self.customCalc,
//onValueChanged: function (e) {
// self.childFormName(e.component.option('selectedItem'));
//},
disabled: ko.computed(function () {
return self.readOnly;
}),
elementAttr: {
/* class: "is-valid-nok"*/ /* - red*/
/*class: "is-valid-onlyok" */ /* -white */
/*class: "is-required-empty",*/ /* - yellow */
class: ko.observable(),
}
};
And went through element:
function setRed() {
self.dxCustomCalc.elementAttr.class("is-valid-nok");
console.log("color changed to red")
}
function setWhite(){
self.dxCustomCalc.elementAttr.class("is-valid-onlyok");
console.log("color changed to white")
}
Functions are executed based on value in field. For example, If value matches, function setRed() is fired. Then, if the value changes and the condition is met, function setWhite() is fired.
The result I got, after executing both functions on subscription to element is:
<div data-bind="dxNumberBox: dxCustomCalc" class="dx-numberbox is-valid-nok is-valid-onlyok">
The result I want to achieve, after executing both functions is:
<div data-bind="dxNumberBox: dxCustomCalc" class="dx-numberbox is-valid-onlyok">
I'd use the class binding to set a CSS class based on an observable.
You could do use it directly
<div data-bind="dxNumberBox: dxCustomCalc, class: dxCustomCalc.cssClass">
or you could apply the class binding as part of your dxCustomCalc custom binding, using ko.applyBindingsToNode():
ko.bindingHandlers.dxNumberBox = {
init: function(elem, valueAccessor, allBindings, viewModel) {
const value = ko.unwrap(valueAccessor());
ko.applyBindingsToNode(elem, {class: value.cssClass}, viewModel);
}
};
function DxNumberBox() {
this.dxCustomCalc = {
cssClass: ko.observable("is-required-empty")
};
this.setRed = () => this.dxCustomCalc.cssClass("is-valid-nok");
this.setWhite = () => this.dxCustomCalc.cssClass("is-valid-onlyok");
this.setEmpty = () => this.dxCustomCalc.cssClass("is-required-empty");
}
const vm = new DxNumberBox();
ko.applyBindings(vm);
.is-valid-nok {
background-color: red;
}
.is-valid-onlyok {
background-color: white;
}
.is-required-empty {
border: 1px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js"></script>
<button data-bind="click: setRed">Red</button>
<button data-bind="click: setWhite">White</button>
<button data-bind="click: setEmpty">Empty</button>
<div data-bind="dxNumberBox: dxCustomCalc">
Profit Information
</div>
<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
Functions are executed based on value in field.
Having extra methods like .setRed() is clunky and unnecessary. Turn cssClass into a computed observable that calculates a class name based on the state of the viewmodel, e.g.
cssClass: ko.pureComputed(() => {
var value = self.value().trim();
if (value == '') return 'is-required-empty';
if (value < 0) return 'is-valid-nok';
return 'is-valid-onlyok';
});

formatter.js overrides knockout value

I'm using knockout.js for binding values to view.
When modal is shown i initialize formatter. Here is sample:
<input type="text" id="propertyName" class="form-control" name="name" required="" data-bind="value: Name">
$("#exampleFormModal").on("shown.bs.modal", function () {
self.InitFormatter();
});
self.InitFormatter = function () {
$('#propertyName').formatter({
'pattern': '{{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}}',
'persistent': true
});
}
The problem is that there is empty values in value: Name
Using knockout with a library that does any kind of DOM manipulation - including value updates on elements - requires a custom binding handler, so that knockout can a) initialize that library properly and b) pass any updates between viewmodel and view.
Writing a custom binding handler for formatter.js is tricky, because formatter.js takes very tight control of all value-related events (keyboard, paste) that happen on an input element - without exposing any events of its own.
In other words, it's easy to set up, but it's hard to be notified when a value changes. But that is exactly what's necessary to keep the viewmodel up-to-date.
To be able to do it anyway, we must hook into one of the internal functions of formatter - the _processKey method. This method is called whenever the value of an input changes, so it's the perfect spot to set up a little "snitch" that tells knockout when the value changes.
Disclaimer This is a hack. It will break whenever the formatter.js internals change. With the current version 0.1.5 however, it seems to work rather well.
This way we can bind our view like this:
<input data-bind="formatter: {
value: someObservable,
pattern: '{{9999}}-{{9999}},
persistent: true
}">
and knockout can fill in the input value whenever someObservable changes, and thanks to the hook into _processKey it also can update someObservable whenever the input value changes.
The full implementation of the binding handler follows (it has no jQuery dependency):
// ko-formatter.js
/* global ko, Formatter */
ko.bindingHandlers.formatter = {
init: function (element, valueAccessor) {
var options = ko.unwrap(valueAccessor()) || {},
instance = new Formatter(element, ko.toJS(options)),
_processKey = Formatter.prototype._processKey,
valueSubs, patternSubs, patternsSubs;
if (ko.isWritableObservable(options.value)) {
// capture initial element value
options.value(element.value);
// shadow the internal _processKey method so we see value changes
instance._processKey = function () {
_processKey.apply(this, arguments);
options.value(element.value);
};
// catch the 'cut' event that formatter.js originally ignores
ko.utils.registerEventHandler(element, 'input', function () {
options.value(element.value);
});
// subscribe to options.value to achieve two-way binding
valueSubs = options.value.subscribe(function (newValue) {
// back out if observable and element values are equal
if (newValue === element.value) return;
// otherwise reset element and "type in" new observable value
element.value = '';
_processKey.call(instance, newValue, false, true);
// write formatted value back into observable
if (element.value !== newValue) options.value(element.value);
});
}
// support updating "pattern" option through knockout
if (ko.isObservable(options.pattern)) {
patternSubs = options.pattern.subscribe(function (newPattern) {
instance.resetPattern(newPattern);
});
}
// support updating "patterns" option through knockout
if (ko.isObservable(options.patterns)) {
patternsSubs = options.patterns.subscribe(function (newPatterns) {
instance.opts.patterns = newPatterns;
instance.resetPattern();
});
}
// clean up after ourselves
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
if (valueSubs) valueSubs.dispose();
if (patternSubs) patternSubs.dispose();
if (patternsSubs) patternsSubs.dispose();
});
}
// this binding has no "update" part, it's not necessary
};
This also supports making the pattern observable, so you can change the pattern for an input field dynamically.
Live demo (expand to run):
// ko-formatter.js
/* global ko, Formatter */
ko.bindingHandlers.formatter = {
init: function (element, valueAccessor) {
var options = ko.unwrap(valueAccessor()) || {},
instance = new Formatter(element, ko.toJS(options)),
_processKey = Formatter.prototype._processKey,
valueSubs, patternSubs, patternsSubs;
if (ko.isWritableObservable(options.value)) {
// capture initial element value
options.value(element.value);
// shadow the internal _processKey method so we see value changes
instance._processKey = function () {
_processKey.apply(this, arguments);
options.value(element.value);
};
// catch the 'cut' event that formatter.js originally ignores
ko.utils.registerEventHandler(element, 'input', function () {
options.value(element.value);
});
// subscribe to options.value to achieve two-way binding
valueSubs = options.value.subscribe(function (newValue) {
// back out if observable and element values are equal
if (newValue === element.value) return;
// otherwise reset element and "type" new observable value
element.value = '';
_processKey.call(instance, newValue, false, true);
// write formatted value back into observable
if (element.value !== newValue) options.value(element.value);
});
}
// support updating "pattern" option through knockout
if (ko.isObservable(options.pattern)) {
patternSubs = options.pattern.subscribe(function (newPattern) {
instance.resetPattern(newPattern);
});
}
// support updating "patterns" option through knockout
if (ko.isObservable(options.patterns)) {
patternsSubs = options.patterns.subscribe(function (newPatterns) {
instance.opts.patterns = newPatterns;
instance.resetPattern();
});
}
// clean up after ourselves
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
if (valueSubs) valueSubs.dispose();
if (patternSubs) patternSubs.dispose();
if (patternsSubs) patternsSubs.dispose();
});
}
// this binding has no "update" part, it's not necessary
};
// viewmodel implementation
ko.applyBindings({
inputPattern: ko.observable('{{9999}}-{{9999}}-{{9999}}-{{9999}}'),
inputValue: ko.observable(),
setValidValue: function () {
var dummy = this.inputPattern().replace(/\{\{([a9*]+)\}\}/g, function ($0, $1) {
return $1.replace(/\*/g, "x");
});
this.inputValue(dummy);
},
setInvalidValue: function () {
this.inputValue('invalid value');
}
});
input {
width: 20em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/formatter.js/0.1.5/formatter.min.js"></script>
View:<br>
<input data-bind="formatter: {
value: inputValue,
pattern: inputPattern,
persistent: true
}">
<input data-bind="value: inputPattern"><br>
<button data-bind="click: setValidValue">Set valid value</button>
<button data-bind="click: setInvalidValue">Set invalid value</button>
<hr>
Viewmodel:<br>
<pre data-bind="text: ko.toJSON($root, null ,2)"></pre>

show underlying value of input on focus knockout custom binding

I have created (please excuse the mess - just hacking it together at the moment!) the following binding handler in knockout
The desired functionality is that if a field is numeric then it will be formatted to 'X' decimal places (default 2) when displaying but the underlying value is whatever is entered.
All works fine, but I want to add the functionality that when an input is focused then it shows the actual value and I just have no idea how to do this.
i.e.
user enters 1.1121 into an input
when they leave the input it formats to 1.11
if they go back into the input (focus) then it displays 1.1121 for editing
It is point 3 that I have no idea how to achieve as at the moment it shows 1.11 and then over-writes on blur??
Can anyone point me in the right direction - basically where do I access the underlying value on focus and replace the displayed text with the underlying value??
for brevity I have removed some other 'decoration' code that wraps the inputs as they are not relelvant.
Thanks in advance.
ko.bindingHandlers.specialInput = {
init: function (element, valueAccessor, allBindingsAccessor) {
var value = valueAccessor();
var decimals = allBindingsAccessor().decimals || 2;
var formatString = "";
var interceptor = ko.computed({
read: function () {
if(isNumeric(ko.unwrap(value))){
//to do if time - replace this with a function that will accept any number of decimals
if(decimals == 0){
formatString = "0,0";
}
if(decimals == 1){
formatString = "0,0.0";
}
if(decimals == 2){
formatString = "0,0.00";
}
if(decimals == 3){
formatString = "0,0.000";
}
if(decimals == 4){
formatString = "0,0.0000";
}
if(decimals == 5){
formatString = "0,0.00000";
}
return numeral(ko.unwrap(value)).format(formatString);
}else{
return ko.unwrap(value);
}
},
write: function (newValue) {
if ($.trim(newValue) == '')
value("");
else
if(isNumeric(newValue)){
value(numeral().unformat(newValue));
value.valueHasMutated();
}else{
value(newValue);
value.valueHasMutated();
}
}
}).extend({notify: 'always'});
if (element.tagName.toLowerCase() == 'input') {
ko.applyBindingsToNode(element, {
value: interceptor
});
} else {
ko.applyBindingsToNode(element, {
text: interceptor
});
}
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var value = ko.unwrap(valueAccessor());
return ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
}
};
function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
I wouldn't implement this with an interceptor computed.
I'd rather do the following:
In the init register event handlers for focus and blur in the init:
on the focus handler, you have to show the underlying value in the input
on the blur handler, you have to store the number in the textbox, and show the rounded value in the input
In the update you have to store the real value, and show it in the textbox. Most probably the textbox won't have the focus when you update the value, so you should be safe showing the rounded value. However, if you think that in some cases it can be focused, you can do something like this to test it to decide how to show the value: Using jQuery to test if an input has focus
pseudocode
ko.bindingHandlers.yourBindingName = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var $element = $(element);
$element.on('focus', function() {
// Show raw value:
$element.val(/* raw value */);
});
$element.on('blur', function() {
// Update the observable with the value in the input
ko.unwrap(valueAccessor())( /* get raw value from input */);
// Show the rounded value
$element.val(/* rounded value */);
});
// Update will be called after init when the binding is first applied,
// so you don't have to worry about what it's initially shown
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// When the value changes, show it in the input
$.element.val(/* if focused show raw, if not, show roundede*/);
}
};
If you are sure that you are only going to use it with input and writable observables, this code is safe. If you have doubts about it, you should add many checks like cheking the kind of element to use jQuery .val() or .text(), check if the binding expression is a writeable observable to update its value, and so on.
NOTE: thereis something which is overseen many times: disposing of objects that we no longer need when the element is destroyed (that can happen for example with 'if', 'whit' or 'template'). In this case I think you don't need to do that, but, please, see this: Custom disposal logic if you think you need to destroy something.
There's nothing wrong with using jQuery in a binding handler, but you could also do this pretty simply with existing binding handlers. It's the sort of thing that could be rolled into a component to be re-used (although I didn't do that in my example).
You just need a backing store for your value, and the computed returns either the backing store or the formatted backing store, depending on whether the input has focus (using the hasFocus binding).
vm = (function() {
var editing = ko.observable(false),
backingStore = 0,
value = ko.computed({
read: function() {
return editing() ? backingStore : (+backingStore).toFixed(2);
},
write: function(newValue) {
backingStore = newValue;
}
});
return {
editing: editing,
value: value
}
}());
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input data-bind="hasFocus: editing, value: value" />
<input />

knockoutjs Binding Handler only firing when ko.unwrap(valueAccessor()); is in update: part

I have two identical Binding Handlers and one of them is not firing, because it has no ko.unwrap(valueAccessor()); in the "update:" part of it.
Best way to see what is going on, is to look at my jsfiddle. By hitting the button the first span toggles the background, but the second not.
When you uncomment line 26 "ko.unwrap(valueAccessor());" the 2nd update function is called and the other span toggles gray/white.
HTML:
<button id='ChangeValue'>item++</button>
<hr/>
<span>Update fired viewModel.item = </span>
<span data-bind='text: $data.item(), bind1: $data.item'></span>
<br/>
<span>Update not fired viewModel.item = </span>
<span data-bind='text: $data.item(), bind2: $data.item'></span>
JS/jQuery/knockoutjs:
$(document).ready(function() {
$('#ChangeValue').click(function() {
viewModel.item(viewModel.item() + 1);
});
var viewModel = {
item: ko.observable(1)
};
ko.bindingHandlers.bind1 = {
init: function(element, valueAccessor) {
},
update: function(element, valueAccessor) {
ko.unwrap(valueAccessor());
if (element.style.background == "gray") {
element.style.background = "white";
} else {
element.style.background = "gray";
}
}
};
ko.bindingHandlers.bind2 = {
init: function(element, valueAccessor) {
},
update: function(element, valueAccessor) {
// if uncomment following line "update" is called
//ko.unwrap(valueAccessor());
if (element.style.background == "gray") {
element.style.background = "white";
} else {
element.style.background = "gray";
}
}
};
ko.applyBindings(viewModel);
});
Is this the expected behavior? I have a complex update functionality in my real code and was wondering why some of my binding handlers are not firing and after a long time of research I did extract this example.
I think it has something to do with the garbage collector in the browser (tested in chrome/IE 11) or knockout.
So my questions:
Is it enought to add "ko.unwrap(valueAccessor());" in my update part of the custom binding to be sure it will be processed?
Is there any documentation about that? Because I did not find something at knockout.js
Yes, this is the expected behavior. The update method is called whenever the binding is applied, and while executing, knockout registers/tracks the observables that are accessed in update method. So in your case, the bind2 binding is not accessing the valueAccessor value, therefore not triggering any update. So if you have
var value = valueAccessor();
var valueUnwrapped = ko.unwrap(value);
This enables knockout to trigger update on any change in valueAccessor() observable.
If you access the value of any observable in update function, the change in that observable will trigger the update function.
It is documented well here.
Using: http://knockoutjs.com/downloads/knockout-2.1.0.js
http://jsfiddle.net/fgav467w/1/
ko.bindingHandlers.bind1 = {
init: function(element, valueAccessor) {
},
update: function(element, valueAccessor) {
if (element.style.background == "gray") {
element.style.background = "white";
} else {
element.style.background = "gray";
}
}
};
The 3.0.0 releases removed a lot of extra "work" which Dandy describes. The 2.0.0 releases like the one I use in the fiddle did not. They will work as you intend but the overall performance will not be as good. But you have options at least.

Animate on changing the value of an observable in Knockout

I want to add animation when working with observable not observableArray which is already available in knockout documents.
Consider a <div data-bind="text: fullname"/> which fullname is an observable. I want the content of <div> element animate and slides down when the value of fullname changes. Something like:
<div data-bind="text: fullname, transition: slideDown"/>
Also, I'm using Durandal if it helps.
Question: How can I assign transition animations to observables?
The beforeRemove and afterAdd are just wrappers around the subscribe function.
ko.bindingHandlers['transition'] = {
init: function(element, valueAccessor, allBindings) {
var transition = valueAccessor(),
target = allBindings.get('text'),
subscriptions = [
target.subscribe(target, onChange),
target.subscribe(target, onBeforeChange, 'beforeChange')
];
function onChange() {
// handle transition when target updates
}
function onBeforeChange() {
// handle transition BEFORE target updates
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
subscription[0].dispose();
subscription[1].dispose();
});
}
};
You can create a custom binding handler which does the animation for you:
ko.bindingHandlers.textSlide = {
init: function(element, valueAccessor) {
$(element).text(ko.unwrap(valueAccessor()));
},
update: function(element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
if (value != $(element).text()) {
$(element).slideUp(function() {
$(this).text(value).slideDown();
});
}
}
};
See http://jsfiddle.net/ro0u9Lmb/

Categories