I have a ViewModel in Knockout that is derived mainly from the mapping plugin (ie, dynamically). This works fine. However, now my client wants me to make sure that all inputs have whitespace trimmed off before submitting to the server. Obviously, the trimming code is very simple, but being relatively new to Knockout, I'm not sure exactly where to put this code. I read about extenders, but that seems pretty verbose and repetitive to go back and add that to each observable. Plus I'm not even sure I can do that to dynamically generated observables (a la, the mapping plugin).
Is there any central mechanism I can extend/override where I can inject some trimming code every time an observable changes? Basically I'm trying to avoid hours spent going through all of our forms and adding special binding syntax in the HTML if I don't have to.
Thanks.
I had the same problem. I wrote an extension so you can call trimmed
in your view-model without having to change your bindings. For example:
var vm = {
myValue: ko.observable('').trimmed()
}
The extension:
ko.subscribable.fn.trimmed = function() {
return ko.computed({
read: function() {
return this().trim();
},
write: function(value) {
this(value.trim());
this.valueHasMutated();
},
owner: this
});
};
Code is on JSFiddle with examples.
Just in case anyone comes across this problem with newer versions of Knockout, the current top-ranked answer will not work correctly.
Here's an updated fiddle and code to show the changes needed:
ko.subscribable.fn.trimmed = function() {
return ko.computed({
read: function() {
return this().trim();
},
write: function(value) {
this(value.trim());
this.valueHasMutated();
},
owner: this
}).extend({ notify: 'always' });
};
If anyone knows why the extend
is now needed, please let me know. It took me forever to figure out why it wasn't working correctly in Knockout 3.1.0
You could write a custom binding that trims the observable. Something similar to this
http://jsfiddle.net/belthasar/fRjdq/
Using Joe's solution as a starting point, We implemented it just a little differently.
Notice:
ko.observable()
has nothing in the parenthesestrimmed
read function simply returns this()
and doesn't get any null or undefined exceptions.Model code:
var vm = {
myValue: ko.observable().trimmed()
}
The extension:
ko.subscribable.fn.trimmed = function() {
return ko.computed({
read: function() {
return this();
},
write: function(value) {
this(value.trim());
this.valueHasMutated();
},
owner: this
});
};
You can create a custom binding that calls the value
binding internally, or you can overwrite the value
binding to auto-trim before it actually binds (not-recommended).
The basic idea:
value
bindingcomputed
read
and write
from the computed instead of from the original observableko.bindingHandlers.trimmedValue = {
init: function(element, valueAccessor, allBindings) {
const ogValue = valueAccessor();
let newVa = valueAccessor;
// If this is a type="text" element and the data-bound value is observable,
// we create a new value accessor that returns an in-between layer to do
// our trimming
if (element.type === "text" && ko.isObservable(ogValue)) {
const trimmedValue = ko.observable().extend({"trim": true});
// Write to the model whenever we change
trimmedValue.subscribe(ogValue);
// Update when the model changes
ogValue.subscribe(trimmedValue);
// Initialize with model value
trimmedValue(ogValue());
// From now on, work with the trimmedValue
newVa = () => trimmedValue;
}
// Note: you can also use `ko.applyBindingsToNode`
return ko.bindingHandlers.value.init(element, newVa, allBindings)
}
}
// Our observable to check our results with
var myObs = ko.observable("test ");
myObs.subscribe(function(newValue) {
console.log("Change: \"" + newValue + "\"");
});
// The extender that does the actual trim
ko.extenders.trim = function(target, option) {
return ko.computed({
read: target,
write: function(val) {
target(
val && typeof val.trim === "function"
? val.trim()
: val
);
// This makes sure the trimming always resets the input UI
if (val !== target.peek()) {
target.valueHasMutated();
}
}
}).extend({notify: "always"});
};
ko.applyBindings({
myObs: myObs
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h4><code>type="text" trimmedValue</code></h4>
<input type="text" data-bind="trimmedValue: myObs">
valueHasMutated
s in your modelThe tricky part is to determine what updates you want to receive in your model... The example below will not trigger valueHasMutated
nor mutate your model's observable. However, if you change your model value to an untrimmed string, the binding handler will reset it instantly. E.g.: myObs(" test ")
will trigger
Change: " test "
, andChange: "test"
If you only need trimming from the UI to the model, and don't mind some extra updates, you can use:
ko.bindingHandlers.value.init = function(element, valueAccessor, allBindings) {
const ogValue = valueAccessor();
const newVa = (element.type === "text" && ko.isObservable(ogValue))
? () => ogValue.extend({"trim": true})
: valueAccessor;
return ogValueInit(element, newVa, allBindings)
};
value
bindingTo use this behaviour as standard behaviour (again, not recommended), you can do:
const ogValueInit = ko.bindingHandlers.value.init;
ko.bindingHandlers.value.init = function( /*... */ ) {
// ...
return ogValueInit( /* ... */);
};
const ogValueInit = ko.bindingHandlers.value.init;
ko.bindingHandlers.value.init = function(element, valueAccessor, allBindings) {
const ogValue = valueAccessor();
let newVa = valueAccessor;
// If this is a type="text" element and the data-bound value is observable,
// we create a new value accessor that returns an in-between layer to do
// our trimming
if (element.type === "text" && ko.isObservable(ogValue)) {
const trimmedValue = ko.observable().extend({"trim": true});
// Write to the model whenever we change
trimmedValue.subscribe(ogValue);
// Update when the model changes
ogValue.subscribe(trimmedValue);
// Initialize with model value
trimmedValue(ogValue());
// From now on, work with the trimmedValue
newVa = () => trimmedValue;
}
return ogValueInit(element, newVa, allBindings)
};
// Our observable to check our results with
var myObs = ko.observable("test ");
myObs.subscribe(function(newValue) {
console.log("Change: \"" + newValue + "\"");
});
// The extender that does the actual trim
ko.extenders.trim = function(target, option) {
return ko.computed({
read: target,
write: function(val) {
target(
val && typeof val.trim === "function"
? val.trim()
: val
);
// This makes sure the trimming always resets the input UI
if (val !== target.peek()) {
target.valueHasMutated();
}
}
}).extend({notify: "always"});
};
ko.applyBindings({
myObs: myObs
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h4><code>type="text" value</code></h4>
<input type="text" data-bind="value: myObs">
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With