Knockout computed and input validation - javascript

I am fairly new to knockout and am trying to figure out how to put two pieces that I understand together.
I need:
Items that are dependent on each other.
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.

Related

Knockout computed property fires on loading

I'm starting with knockout and my computed observable seems to fire always when the viewmodel is instantiated and i don't know why.
I've reduced the problem to the absurd just for testing: the computed property just prints a message in the console and it is not binded to any element at the DOM. Here it is:
(function() {
function HomeViewModel() {
var self = this;
(...)
self.FullName = ko.computed(function () {
console.log("INSIDE");
});
(...)
};
ko.applyBindings(new HomeViewModel());
})();
How can it be avoided?
Update:
Here is the full code of the ViewModel just for your better understanding:
function HomeViewModel() {
var self = this;
self.teachers = ko.observableArray([]);
self.students = ko.observableArray([]);
self.FilterByName = ko.observable('');
self.FilterByLastName = ko.observable('');
self.FilteredTeachers = ko.observableArray([]);
self.FilteredStudents = ko.observableArray([]);
self.FilteredUsersComputed = ko.computed(function () {
var filteredTeachers = self.teachers().filter(function (user) {
return (user.name.toUpperCase().includes(self.FilterByName().toUpperCase()) &&
user.lastName.toUpperCase().includes(self.FilterByLastName().toUpperCase())
);
});
self.FilteredTeachers(filteredTeachers);
var filteredStudents = self.students().filter(function (user) {
return (user.name.toUpperCase().includes(self.FilterByName().toUpperCase()) &&
user.lastName.toUpperCase().includes(self.FilterByLastName().toUpperCase())
);
});
self.FilteredStudents(filteredStudents);
$("#LLAdminBodyMain").fadeIn();
}).extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
self.FilteredUsersComputed.subscribe(function () {
setTimeout(function () { $("#LLAdminBodyMain").fadeOut(); }, 200);
}, null, "beforeChange");
$.getJSON("/api/User/Teacher", function (data) {
self.teachers(data);
});
$.getJSON("/api/User/Student", function (data) {
self.students(data);
});
}
ko.applyBindings(new HomeViewModel());
})();
I need it to not be executed on load because on load the self.students and self.teachers arrays are not jet populated.
NOTE: Just want to highlight that in both codes (the absurd and full), the computed property is executed on loading (or when the ViewModel is first instantiated).
There are two main mistakes in your approach.
You have a separate observable for filtered users. That's not necessary. The ko.computed will fill that role, there is no need to store the computed results anywhere. (Computeds are cached, they store their own values internally. Calling a computed repeatedly does not re-calculate its value.)
You are interacting with the DOM from your view model. This should generally be avoided as it couples the viewmodel to the view. The viewmodel should be able operate without any knowledge of how it is rendered.
Minor points / improvement suggestions:
Don't rate-limit your filter result. Rate-limit the observable that contains the filter string.
Don't call your computed properties ...Computed - that's of no concern to your view, there is no reason to point it out. For all practical purposes inside your view, computeds and observables are exactly the same thing.
If teachers and students are the same thing, i.e. user objects to be displayed in the same list, why have them in two separate lists? Would it not make more sense to have a single list in your viewmodel, so you don't need to filter twice?
Observables are functions. This means
$.getJSON("...", function (data) { someObservable(data) });
can be shortened to
$.getJSON("...", someObservable);.
Here is a better viewmodel:
function HomeViewModel() {
var self = this;
self.teachers = ko.observableArray([]);
self.students = ko.observableArray([]);
self.filterByName = ko.observable().extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
self.filterByLastName = ko.observable().extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
function filterUsers(userList) {
var name = self.filterByName().toUpperCase(),
lastName = self.filterByLastName().toUpperCase(),
allUsers = userList();
if (!name && !lastName) return allUsers;
return allUsers.filter(function (user) {
return (!name || user.name.toUpperCase().includes(name)) &&
(!lastName || user.lastName.toUpperCase().includes(lastName));
});
}
self.filteredTeachers = ko.computed(function () {
return filterUsers(self.teachers);
});
self.filteredStudents = ko.computed(function () {
return filterUsers(self.students);
});
self.filteredUsers = ko.computed(function () {
return self.filteredTeachers().concat(self.filteredStudents());
// maybe sort the result?
});
$.getJSON("/api/User/Teacher", self.teachers);
$.getJSON("/api/User/Student", self.students);
}
With this it does not matter anymore that the computeds are calculated immediately. You can bind your view to filteredTeachers, filteredStudents or filteredUsers and the view will always reflect the state of affairs.
When it comes to making user interface elements react to viewmodel state changes, whether the reaction is "change HTML" or "fade in/fade out" makes no difference. It's not the viewmodel's job. It is always the task of bindings.
If there is no "stock" binding that does what you want, make a new one. This one is straight from the examples in the documentation:
// Here's a custom Knockout binding that makes elements shown/hidden via jQuery's fadeIn()/fadeOut() methods
// Could be stored in a separate utility library
ko.bindingHandlers.fadeVisible = {
init: function(element, valueAccessor) {
// Initially set the element to be instantly visible/hidden depending on the value
var value = valueAccessor();
$(element).toggle(ko.unwrap(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable
},
update: function(element, valueAccessor) {
// Whenever the value subsequently changes, slowly fade the element in or out
var value = valueAccessor();
ko.unwrap(value) ? $(element).fadeIn() : $(element).fadeOut();
}
};
It fades in/out the bound element depending the bound value. It's practical that the empty array [] evaluates to false, so you can do this in the view:
<div data-bind="fadeVisible: filteredUsers">
<!-- show filteredUsers... --->
</div>
A custom binding that fades an element before and after the bound value changes would look like follows.
We subscribe to value during the binding's init phase.
There is no update phase in the binding, everything it needs to do is accomplished by the subscriptions.
When the DOM element goes away (for example, because a higher-up if or foreach binding triggers) then our binding cleans up the subscriptions, too.
Let's call it fadeDuringChange:
ko.bindingHandlers.fadeDuringChange = {
init: function(element, valueAccessor) {
var value = valueAccessor();
var beforeChangeSubscription = value.subscribe(function () {
$(element).delay(200).fadeOut();
}, null, "beforeChange");
var afterChangeSubscription = value.subscribe(function () {
$(element).fadeIn();
});
// dispose of subscriptions when the DOM node goes away
// see http://knockoutjs.com/documentation/custom-bindings-disposal.html
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
// see http://knockoutjs.com/documentation/observables.html#explicitly-subscribing-to-observables
beforeChangeSubscription.dispose();
afterChangeSubscription.dispose();
});
}
};
Usage is the same as above:
<div data-bind="fadeDuringChange: filteredUsers">
<!-- show filteredUsers... --->
</div>

bacon.js: Strange behaviour with holdWhen and onValue

Take a look at the working CodePen here: http://codepen.io/djskinner/pen/JdpwyY
// Animation start events push here
var startBus = new Bacon.Bus();
// Animation end events push here
var endBus = new Bacon.Bus();
// Balance updates push here
var balanceBus = new Bacon.Bus();
// A Property that determines if animating or not
var isAnimating = Bacon.update(false,
[startBus], function() { return true; },
[endBus], function() { return false; }
);
// Only update the displayBalance when not animating
var displayBalance = Bacon.update(0,
[balanceBus.holdWhen(isAnimating)], function(previous, x) {
return x;
}
);
setTimeout(function() {
var streamTemplate = Bacon.combineTemplate({
balance: displayBalance
});
// Uncommenting this block changes the way the system behaves
// streamTemplate.onValue(function(initialState) {
// console.log(initialState);
//})();
// Print the displayBalance
streamTemplate.onValue(function(v) {
console.log(v.balance);
});
});
Pressing the balance button generates a new random number. A Property is created that uses holdWhen to restrict balance updates coming through until the isAnimating Property becomes false.
If I was interested in getting the initial state of streamTemplate, I might get the value and immediately unsubscribe:
streamTemplate.onValue(function(initialState) {
console.log(initialState);
})();
However, once I do this the displayBalance Property behaves differently and I no longer receive updates.
Why would this seemingly inert change make such a drastic different to the system? Surely the behaviour of the system shouldn't be dependent on whether someone has subscribe and unsubscribed to the streamTemplate at some point in the past?
This behaviour has been confirmed as a bug that has been fixed in 0.7.67.
See here for details.

Knockout append observables w/ characters

I have observables in my KnockoutJS app and their values are being fetched from an array that's contained globally in the app, eg:
self.observable = ko.observable(App[0].Observable_Value);
For ease, let's say Observable_Value = 10.
This is working as you'd expect it to and the <input> that self.observable is binded to has the correct value in it by default; 10.
What I want to do now is to append the observable with a % in the <input> so the displayed output in the input field will be 10%.
I simply want to append the input value with a % because I need the observable to be readable as a number and not a string to prevent NaN errors later on in the app. The app relies heavily on numbers.
I've tried doing this as a computed function with read/write & parseFloat but to no success.
Any ideas?
I think that the best option in this case would be a custom binding handler.
I simply want to append the input value with a % because I need the observable to be readable as a number and not a string
That is precisely what a binding handler could do - preserve the original value of the observable, but change the way that it is displayed.
I've tried doing this as a computed function
Although a computed function could do the trick, it's usually unecessary unless you want to actually work with the return value from the computed. In this case, since you just want to change the visual display, you don't need a another variable representing the same value.
Here is a very basic one that just puts a % sign in front of the observable's value. See fiddle
ko.bindingHandlers.percent = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var value = ko.unwrap(valueAccessor());
$(element).text('%' + value);
}
};
You can add a read/write computable that will add the percent for display, then remove it when setting the underlying value (fiddle):
ko.observable.fn.formatAsPercent = function() {
var base = this;
return ko.computed({
read: function() {
return base() + "%";
},
write: function(newValue) {
base(parseInt(newValue.replace('%', '')));
}
});
};
function ViewModel() {
var self = this;
self.number = ko.observable(10); // actual number
self.percent= self.number.formatAsPercent(); // used for binding to show %
}
var my = { vm : new ViewModel() };
ko.applyBindings(my.vm);

Knockout computed vs. subscription, timing issues

Just found out, that in KnockoutJS subscription functions are evaluated before dependent computables and need someone who can commit that, because I can't find anything about Knockouts timing in the docs or discussion forums.
That means: If I have a model like this...
var itemModel = function (i) {
var self = this;
self.Id = ko.observable(i.Id);
self.Title = ko.observable(i.Title);
self.State = ko.observable(i.State);
};
var appModel = function () {
var self = this;
self.Items = ko.observableArray() // <-- some code initializes an Array of itemModels here
self.indexOfSelectedItem = ko.observable();
self.selectedItem = ko.computed(function () {
if (self.indexOfSelectedItem() === undefined) {
return null;
}
return self.Items()[self.indexOfSelectedItem()];
});
};
where I want to keep track of the selected array item with an observable index field, and I subscribe to this index field like this...
appModel.indexOfSelectedItem.subscribe(function () {
// Do something with appModel.selectedItem()
alert(ko.toJSON(appModel.selectedItem()));
}
...the subscription function is evaluated before the computed is reevaluated with the new index value, so I will get the selectedItem() that corresponds to the last selected Index and not the actual selected Index.
Two questions:
Is that right?
Then why should I make use of ko.computed() if a simple function gets me the current selected Item every time I call it, while ko.computed gets evaluated at anytime where everything is done already and I dont need it anymore?
By default all computeds in Knockout are evaluated in an eager fashion, not lazily (i.e., not when you first access them).
As soon as one dependency changes, all all subscriptions are notified and all connected computeds are re-evaluated. You can change that behavior to "lazy" by specifying the deferEvaluation option in a computed observable, but you cannot do this for a subscription.
Hoewever, I think there is no need to depend on the index of the selected item. In fact, that's even bad design because you are not really intested in the numerical value of the index, but rather in the item it represents.
You could reverse the dependencies by creating a writeable computed observable that gives you the index of the currently selected item (for diplay purposes) and allows to change it as well (for convenience).
function AppModel() {
var self = this;
self.Items = ko.observableArray();
self.selectedItem = ko.observable();
self.indexOfSelectedItem = ko.computed({
read: function () {
var i,
allItems = self.Items(),
selectedItem = self.selectedItem();
for (i = 0; i < allItems.length; i++) {
if (allItems[i] === selectedItem) {
return i;
}
}
return -1;
},
write: function (i) {
var allItems = self.Items();
self.selectedItem(allItems[i]);
}
});
}
Knockout favors storing/handling the actual values instead of just indexes to values, so it would probably not be difficult to make the necessary changes to your view. Just make everything that previously wrote to indexOfSelectedItem now write to selectedItem directly. Dependencies on selectedItem will continue to work normally.
In a well-designed Knockout application you will rarely ever have the need to handle the index of an array item. I'd recommend removing the write part of the computed once everything works.
See: http://jsfiddle.net/4hLLn/

Knockout.js modify value before ko.observable() write

I've got a working viewmodel with numerous variables.
I use autoNumeric (http://www.decorplanit.com/plugin/) for text formatting in textbox. I'd like to use the input field's observed data in a computed observable, but if the observable textfield with the formatting gets modified, the formatting also gets saved in the variable.
How can I only observe the value of the input field without the formatting?
I think the best way to this could be a getter/setter to the observable, and remove the formatting when the value is set. I couldn't find a solution in knockout's documentation to write get/set methods for ko.observable(), and ko.computed() can not store a value.
I don't want to use hidden fields, or extra variables.
Is this possible without it?
Solution, as seen on http://knockoutjs.com/documentation/extenders.html
ko.extenders.numeric = function(target, precision) {
//create a writeable computed observable to intercept writes to our observable
var result = ko.computed({
read: target, //always return the original observables value
write: function(newValue) {
var current = target(),
roundingMultiplier = Math.pow(10, precision),
newValueAsNum = isNaN(newValue) ? 0 : parseFloat(+newValue),
valueToWrite = Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier;
//only write if it changed
if (valueToWrite !== current) {
target(valueToWrite);
} else {
//if the rounded value is the same, but a different value was written, force a notification for the current field
if (newValue !== current) {
target.notifySubscribers(valueToWrite);
}
}
}
});
//initialize with current value to make sure it is rounded appropriately
result(target());
//return the new computed observable
return result;
};
And later on
function AppViewModel(one, two) {
this.myNumberOne = ko.observable(one).extend({ numeric: 0 });
this.myNumberTwo = ko.observable(two).extend({ numeric: 2 });
}
You can use ko.computed() for this. You can specify a write option, see Writeable computed observables
Example (taken from knockout documentation):
function MyViewModel() {
this.price = ko.observable(25.99);
this.formattedPrice = ko.computed({
read: function () {
return '$' + this.price().toFixed(2);
},
write: function (value) {
// Strip out unwanted characters, parse as float, then write the raw data back to the underlying "price" observable
value = parseFloat(value.replace(/[^\.\d]/g, ""));
this.price(isNaN(value) ? 0 : value); // Write to underlying storage
},
owner: this
});
}
ko.applyBindings(new MyViewModel());

Categories

Resources