18

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.

Jason
  • 51,583
  • 38
  • 133
  • 185
  • You mention they only need to be trimmed before you submit. Unmap you observables using ko.mapping.toJS(model) and then write a helper to recurse through each property and trim it. – madcapnmckay May 24 '12 at 22:27
  • hm. that's fair. maybe i will try that. although i wish there was a way to run some code every time the observable updates. – Jason May 24 '12 at 22:30
  • FWIW, because i'm running a validator on input change, i'm just running a trim function at the beginning of that. – Jason May 24 '12 at 23:05
  • lots of good answers but still nothing that works across the board without going through the code and updating each observable on every page. Did you find a way to do it automatically? – rdans Jul 27 '17 at 13:13

6 Answers6

21

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.

Joe
  • 16,328
  • 12
  • 61
  • 75
  • Is this functionally the same as adding it to the the ko.extenders namespace? – Matthew Nichols Nov 26 '12 at 14:09
  • @MatthewNichols yes. ko.extenders is actually implemented with ko.subscribable. The difference being, you need an argument with ko.extenders, e.g. `.extend({trimmed: true})` or `.extend({trimmed: null})`. This way is also cleaner by letting you call the function directly from the observable. See http://freshbrewedcode.com/joshbush/2011/12/27/knockout-js-observable-extensions/ – Joe Nov 26 '12 at 16:25
  • This will only work in browsers that support `.trim()`, correct? (i.e. no IE7 or IE8 support) – John Dec 13 '12 at 15:02
  • @johnthexiii correct. Options: use `jQuery.trim(s)` or `s.replace(/^\s+|\s+$/g, '')` in place of `s.trim()`. Alternatively, you could add your own polyfill to make it work to support these browsers. See http://stackoverflow.com/questions/498970/how-do-i-trim-a-string-in-javascript – Joe Dec 13 '12 at 16:57
  • 1
    @johnthexiii just added legacy browser support to the JSFiddle code. – Joe Dec 13 '12 at 17:16
11

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

Justin
  • 166
  • 1
  • 7
  • 2
    [3.0.0 changed how computed observables notify changes](http://knockoutjs.com/upgrade-notes/v3.0.0.html#computed-properties-now-notify-only-when-their-value-changes) so they match regular observables. Usually this is a good thing (fewer unnecessary updates), but causes a problem here when you need to revert an incorrect value. – Michael Best Aug 23 '14 at 22:38
4

You could write a custom binding that trims the observable. Something similar to this

http://jsfiddle.net/belthasar/fRjdq/

Jeremy Roberts
  • 751
  • 1
  • 5
  • 13
  • 1
    while this probably works, i was looking for a solution that wouldn't require i edit all the bindings in my HTML.. – Jason May 24 '12 at 21:45
  • Could you post your mapping code? Are you using mappingOptions? – Jeremy Roberts May 24 '12 at 21:47
  • my mapping code is just `ko.mapping.fromJS(data, {}, viewmodel)` in the `.done()` function of an ajax call – Jason May 24 '12 at 21:49
  • From the documentation you can add mapping options. You may have access to the data from there. It has been a while since I messed with the mapping options. var mapping = { 'children': { key: function(data) { return ko.utils.unwrapObservable(data.id); } } } var viewModel = ko.mapping.fromJS(data, mapping); – Jeremy Roberts May 24 '12 at 21:51
  • the mapping options looks interesting, especially the [update](http://knockoutjs.com/documentation/plugins-mapping.html#customizing_object_updating_using_update) option. However, it looks like I'd have to create an update option for each observable manually. Can I apply these options AFTER I've mapped? If so I could map, loop through and apply the options, and then remap, but I don't know if that's possible? – Jason May 24 '12 at 22:04
3

Using Joe's solution as a starting point, We implemented it just a little differently.

Notice:

  • The ko.observable() has nothing in the parentheses
  • The new trimmed 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
    });
};
scott-pascoe
  • 1,463
  • 1
  • 13
  • 31
3

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:

  • Intercept the value binding
  • Wrap the passed observable in a computed
  • Make the binding read and write from the computed instead of from the original observable
  • When new input arrives, trim it before we write it
  • When the model value changes, trim it and update both model & UI if needed

ko.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">

If you don't care about some unneeded valueHasMutateds in your model

The 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

  1. Change: " test ", and
  2. Change: "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)
};

Overwriting the default value binding

To 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">
user3297291
  • 22,592
  • 4
  • 29
  • 45
  • thanks. this looks promising however is preventing me from entering any spaces whatsoever inside a text field. As soon as I press the space key it immediately trims the space off before i have finished typing in the field – rdans Jul 28 '17 at 12:24
  • Are you using the `textInput` binding instead of `value`? It trims on the `change` event, which should only occur when you remove focus from the `input`. If you want live updates you'll need some extra code... – user3297291 Jul 28 '17 at 14:41
0

An alternative approach that works fine for us - trim when the field is edited:

$(document.body).on('blur', 'input, textarea', function () { this.value = this.value.trim(); $(this).trigger('change'); });

The trigger of the 'change' event ensures KO picks up the change (tested with KO v2).

psdie
  • 61
  • 5