Automatically trim whitespace from all observable

2019-03-24 07:53发布

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.

标签: knockout.js
5条回答
相关推荐>>
2楼-- · 2019-03-24 08:37

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

查看更多
地球回转人心会变
3楼-- · 2019-03-24 08:39

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.

查看更多
走好不送
4楼-- · 2019-03-24 08:39

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

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

查看更多
神经病院院长
5楼-- · 2019-03-24 08:44

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

查看更多
Root(大扎)
6楼-- · 2019-03-24 08:57

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
    });
};
查看更多
登录 后发表回答