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.
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 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:
- Create a hidden observable that stores the value.
- Return a writable computed observable based on the hidden observable.
- Optionally normalize the incoming value.
- 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.