简体   繁体   中英

Knockout computed and input validation

I am fairly new to knockout and am trying to figure out how to put two pieces that I understand together.

I need:

  1. Items that are dependent on each other.
  2. Input value validation on the items.

Example:

  • I have startTime in seconds, duration in seconds, and stopTime that is calculated from startTime + duration
  • startTime cannot be changed
  • duration and stopTime are tied to input fields
  • stopTime is displayed and entered in HH:MM:SS format
  • If the user changes stopTime , duration should be calculated and automatically updated
  • If the user changes duration , stopTime should be calculated and automatically updated

I can make them update each other (assume Sec2HMS and HMS2Sec are defined elsewhere, and convert between HH:MM:SS and seconds):

this.startTime = 120; // Start at 120 seconds
this.duration = ko.observable(0);

// This dependency works by itself.
this.stopTimeFormatted = ko.computed({
    read: function () {
        return Sec2HMS(this.startTime + parseInt(this.duration()), true);
    },
    write: function (value) {
        var stopTimeSeconds = HMS2Sec(value);
        if (!isNaN(stopTimeSeconds)) {
            this.duration(stopTimeSeconds - this.startTime);
        } else {
            this.duration(0);
        }
    },
    owner: this
});

Or, I can use extenders or fn to validate the input as is shown in the knockout docs:

ko.subscribable.fn.HMSValidate = function (errorMessage) {
    //add some sub-observables to our observable
    var observable = this;
    observable.hasError = ko.observable();
    observable.errorMessage = ko.observable();

    function validate(newValue) {
        var isInvalid = isNaN(HMS2Sec(newValue));
        observable.hasError(isInvalid ? true : false);
        observable.errorMessage(isInvalid ? errorMessage : null);
    }

    //initial validation
    validate(observable());

    //validate whenever the value changes
    observable.subscribe(validate);

    //return the original observable
    return observable;
};
this.startTime = 120; // Start at 120 seconds
this.duration = ko.observable(0);
this.stopTimeHMS = ko.observable("00:00:00").HMSValidate("HH:MM:SS please");

But how do I get them working together? If I add the HMSValidate to the computed in the first block it doesn't work because by the time HMSValidate 's validate function gets the value it's already been changed.

I have made it work in the first block by adding another observable that keeps track of the "raw" value passed into the computed and then adding another computed that uses that value to decide if it's an error state or not, but that doesn't feel very elegant.

Is there a better way?

http://jsfiddle.net/cygnl7/njNaS/2/

I came back to this after a week of wrapping up issues that I didn't have a workaround for (code cleanup time!), and this is what I have.

I ended up with the idea that I mentioned in the end of the question, but encapsulating it in the fn itself.

ko.subscribable.fn.hmsValidate = function (errorMessage) {
    var origObservable = this;
    var rawValue = ko.observable(origObservable()); // Used for error checking without changing our main observable.
    if (!origObservable.hmsFormatValidator) {
        // Handy place to store the validator observable
        origObservable.hmsFormatValidator = ko.computed({
            read: function () {
                // Something else could have updated our observable, so keep our rawValue in sync.
                rawValue(origObservable());
                return origObservable();
            },
            write: function (newValue) {
                rawValue(newValue);
                if (newValue != origObservable() && !isNaN(HMS2Sec(newValue))) {
                    origObservable(newValue);
                }
            }
        });
        origObservable.hmsFormatValidator.hasError = ko.computed(function () {
            return isNaN(HMS2Sec(rawValue()));
        }, this);
        origObservable.hmsFormatValidator.errorMessage = ko.computed(function () {
            return errorMessage;
        }, this);
    }

    return origObservable.hmsFormatValidator;
};

What this does is creates another computed observable that acts as a front/filter to the original observable. That observable has some other sub-observables, hasError and errorMessage , attached to it for the error states. The rawValue keeps track of the value as it was entered so that we can detect whether it was a good value or not. This handles the validation half of my requirements.

As for making two values dependent on each other, the original code in my question works. To make it validated, I add hmsValidate to it, like so:

this.stopTimeFormatted = ko.computed({
    read: function () {
        return Sec2HMS(this.startTime + parseInt(this.duration()), true);
    },
    write: function (value) {
        this.duration(HMS2Sec(value) - this.startTime);
    },
    owner: this
}).hmsValidate("HH:MM:SS please");

See it in action here: http://jsfiddle.net/cygnl7/tNV5S/1/

It's worth noting that the validation inside of write is no longer necessary since the value will only ever be written by hmsValidate if it validated properly.

This still feels a little inelegant to me since I'm checking isNaN a couple of times and having to track the original value (especially in the read()), so if someone comes up with another way to do this, I'm all ears.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM