Reject Invalid Values in Knockout Observables

Wouldn't it be nice if we could restrict the value written to a Knockout observable? Some values might mess up your model completely while others just don't make sense. So how can we create a conditioned observable that rejects invalid values? It turns out that conditioning an observable is not so hard.

This approach is deliberately strict: after normalization, it rejects every value that does not satisfy the condition.

  1. Intro
  2. A small example: Sudoku
  3. The Cell
  4. Conditioned Observable
    1. Inner workings
    2. Implementation
    3. Conditioning in action
  5. Summary
  6. Changelog
  7. Comments

A small example: Sudoku

Let's think of a Sudoku cell. Its value must be a number from 1 through 9. Its valid values also depend on the numbers already used in the same row and column (orange) and the box the cell is in (purple).

Not allowed due to row and column: 1, 2, 3, 4, 6, 7, 8, 9. Not allowed due to box: 2, 3, 6, 8. Allowed: 5.
The restrictions for a Sudoku cell visualized:
Not allowed due to row and column: 1, 2, 3, 4, 6, 7, 8, 9.
Not allowed due to box: 2, 3, 6, 8. Allowed: 5.

The Cell

We can model this Sudoku cell as a class with options and a value. Options can be removed from the cell. This is useful when an option is no longer possible because a related cell has changed. When a value is set, the cell should check whether that value is still an option.

class Cell {

  constructor() {
    this.options = ko.observableArray([1,2,3,4,5,6,7,8,9]);
    this.value = ko.observable(null);
  }

}

How can we restrict that number?

Conditioned Observable

Let's extend Knockout with a conditionedObservable that takes an initial value, a condition, and an optional normalizer.

Inner workings

The basic idea comes from this post by Ryan Niemeyer. Building the conditioned observable works as follows:

  1. Create a hidden observable that stores the value.
  2. Return a writable computed observable based on the hidden observable.
  3. Optionally normalize the incoming value.
  4. Validate the value before accepting and storing it.

Implementation

Here's the implementation:

ko.conditionedObservable = function(
    initialValue,
    condition,
    normalize
){
    let obi = ko.observable(initialValue);
    let computer = ko.computed({
        owner: this,
        read: () => obi(),
        write: (newValue) => {

            //unwrap and normalize the value
            let value = ko.unwrap(newValue);
            let normalizedValue = normalize
                ? normalize(value)
                : value;

            //check condition
            if(condition(normalizedValue)){

                //set it to the observable
                obi(normalizedValue);
            }
            else {
                //reset the value
                computer.notifySubscribers(computer());
            }
        }
    });

    return computer;
};

Looks pretty simple. Yet it is very effective.

Conditioning in action

Let's add it to the Cell class. The observable is now conditioned to allow only a null value or values that are still available in the options array. The normalizer converts numeric strings to numbers before they are validated and stored.

class Cell {

  constructor() {
    this.options = ko.observableArray([1,2,3,4,5,6,7,8,9]);
    this.value = ko.conditionedObservable(
      null,
      (value) => value == null || this.options().indexOf(value) > -1,
      (value) => value == null || value === "" ? null : Number(value)
    );
  }

}

Notice how the arrow function captures this, allowing it to access this.options.

Summary

Knockout is a versatile MVVM framework that you can extend with features like conditioned observables. With a few lines of code, you can add constraints to your observables. Conditioning made easy.

Changelog

  • Converted the examples from TypeScript to JavaScript, added input normalization, corrected rejected-value notifications and Sudoku constraints, and updated outdated links.
  • Initial article.
expand_less brightness_auto