Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automatically trim whitespace from all observable values

Tags:

knockout.js

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.

like image 964
Jason Avatar asked May 24 '12 21:05

Jason


5 Answers

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.

like image 117
Joe Avatar answered Oct 04 '22 16:10

Joe


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

like image 43
Justin Avatar answered Oct 04 '22 18:10

Justin


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

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

like image 26
Jeremy Roberts Avatar answered Oct 04 '22 18:10

Jeremy Roberts


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
    });
};
like image 31
scott-pascoe Avatar answered Oct 04 '22 17:10

scott-pascoe


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">
like image 34
user3297291 Avatar answered Oct 04 '22 18:10

user3297291