KnockoutJS Validation with dynamic observables - javascript

I am using this plugin https://github.com/ericmbarnard/Knockout-Validation and i am trying to validate an object that is loaded dynamically.
Javascript:
function VM() {
var self = this;
// This is a static observable, just to ensure that basic validation works fine.
self.static = ko.observable();
self.static.extend({required: true});
// This is the observable that will be updated to my model instance.
self.person = ko.observable({});
// This is an handler for manual trigger.
// I'm not even sure this is needed.
self.a = function(){
self.errors.showAllMessages();
self.staticErrors.showAllMessages();
}
// Here i'm loading current person from somewhere, i.e. a rest service.
self.load = function() {
// Update observable
self.person(new Model());
// Define validation rules
self.person().name.extend({required: true});
self.person().email.extend({required: true});
// Set person data
self.person().name('Long');
self.person().email('John');
// Set validators
self.errors = ko.validation.group(self.person);
self.staticErrors = ko.validation.group(self.static);
}
}
// Just a test model.
function Model() {
this.name = ko.observable();
this.email = ko.observable();
}
ko.validation.init();
var vm = new VM();
ko.applyBindings(vm);
Markup
<ul>
<li>1. Hit "Load"</li>
<li>2. Hit "Show errors", or maunally change input data.</li>
</ul>
<button data-bind='click: load'>Load</button>
<br/>
<h1>This is working properly.</h1>
<input type='text' data-bind='value: static' />
<br/>
<h1>This is not working.</h1>
<input type='text' data-bind='value: person().name' />
<input type='text' data-bind='value: person().email' />
<br/>
<button data-bind='click: a'>Show errors</button>
Fiddle
http://jsfiddle.net/qGzfr/
How do I make this work?

The validation plugin only gets applied in your bindings only if by the time when the binding is parsed by Knockout your properties are validate.
In different words: you cannot add validation to a property after the property was bound on the UI.
In your example you are using an empty object in self.person = ko.observable({}); as a default value, so when Knockout executes the data-bind='value: person().name' expression you don't have a name property so the validation won't work even if you later add the name property to your object.
In your example you can solve this with changing your Model constructor to include the validation rules:
function Model() {
this.name = ko.observable().extend({required: true});
this.email = ko.observable().extend({required: true});
}
And use an empty Model object as the default person:
self.person = ko.observable(new Model());
And when calling Load don't replace the person object but update its properties:
self.load = function() {
// Set person data
self.person().name('Long');
self.person().email('John');
}
Demo JSFiddle.
Note: Knockout does not always handles well if you replace whole object like self.person(new Model()); so it is anyway a better practice to only update the properties and not throw away the whole object.
A different solution would be to use the with binding because inside the with binding KO will reevaluate the bindings if the bound property changes.
So change your view:
<!-- ko with: person -->
<input type='text' data-bind='value: name' />
<input type='text' data-bind='value: email' />
<!-- /ko -->
In this case you need to use null as the default person:
self.person = ko.observable();
And in your Load you need to add the validation before assigning your person property so by the time KO applies the bindings your properties have the validation:
self.load = function() {
var model = new Model()
model.name.extend({required: true});
model.email.extend({required: true});
self.person(model);
// Set person data
self.person().name('Long');
self.person().email('John');
}
Demo JSFiddle.

I was able to make it work, this are the changes required:
<head>
<script type="text/javascript" src ="knockout-2.3.0.js"></script>
<script type="text/javascript" src ="knockout.validation.min.js"></script>
</head>
<body>
<!-- no changes -->
<script>
function VM() { ... }
function Model() { ... }
// ko.validation.init();
var vm = new VM();
ko.applyBindings(vm);
</script>
</body>
What was done?
Include KnockoutJS and the validation plugin.
Bind after the elements have been added. Remeber that HTML pages are parsed from top to bottom.
How could you tell? In the console this errors appeared:
Cannot read property 'nodetype' of null
and
Cannot call method 'group' of undefined

Related

Unable to view data on an oservable

I have a View model, which has a loaddata function. It has no constructor. I want it to call the loadData method IF the ID field has a value.
That field is obtained via:
self.TemplateId = ko.observable($("#InputTemplateId").val());
Then, at the end of my ViewModel, I have a bit of code that checks that, and calls my load function:
if (!self.CreateMode()) {
self.loadData();
}
My load method makes a call to my .Net WebAPI method, which returns a slighly complex structure. The structure is a class, with a few fields, and an Array/List. The items in that list, are a few basic fields, and another List/Array. And then THAT object just has a few fields. So, it's 3 levels. An object, with a List of objects, and those objects each have another list of objects...
My WebAPI call is working. I've debugged it, and the data is coming back perfectly.
self.loadData = function () {
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
});
}
I am trying to load the contents of this call, into an observable object called 'Data'. It was declared earlier:
self.Data = ko.observable();
TO load it, and keep everything observable, I am using the Knockout mapping plugin.
self.Data(ko.mapping.fromJS(data));
When I breakpoint on that, I am seeing what I expect in both data (the result of the API call), and self.Data()
self.Data seems to be an observable version of the data that I loaded. All data is there, and it all seems to be right.
I am able to alert the value of one of the fields in the root of the data object:
alert(self.Data().Description());
I'm also able to see a field within the first item in the list.
alert(self.Data().PlateTemplateGroups()[0].Description());
This indicates to me that Data is an observable and contains the data. I think I will later be able to post self.Data back to my API to save/update.
Now, the problems start.
On my View, I am trying to show a field which resides in the root class of my complex item. Something I alerted just above.
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Data.Description">
I get no error. Yet, the text box is empty.
If I change the code for the input box to be:
data-bind="value: Data().Description()"
Data is displayed. However, I am sitting with an error in the console:
Uncaught TypeError: Unable to process binding "value: function
(){return Data().Description() }" Message: Cannot read property
'Description' of undefined
I think it's due to the view loading, before the data is loaded from the WebAPI call, and therefore, because I am using ko.mapping - the view has no idea what Data().Description() is... and it dies.
Is there a way around this so that I can achieve what I am trying to do? Below is the full ViewModel.
function PlateTemplateViewModel() {
var self = this;
self.TemplateId = ko.observable($("#InputTemplateId").val());
self.CreateMode = ko.observable(!!self.TemplateId() == false);
self.IsComponentEditMode = ko.observable(false);
self.IsDisplayMode = ko.observable(true);
self.CurrentComponent = ko.observable();
self.Data = ko.observable();
self.EditComponent = function (data) {
self.IsComponentEditMode(true);
self.IsDisplayMode(false);
self.CurrentComponent(data);
}
self.loadData = function () {
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
});
}
self.cancel = function () {
window.location.href = "/PlateTemplate/";
};
self.save = function () {
var data = ko.mapping.toJS(self.Data);
$.post("/api/PlateTemplate/Save", data).done(function (result) {
alert(result);
});
};
if (!self.CreateMode()) {
self.loadData();
}
}
$(document).ready(function () {
ko.applyBindings(new PlateTemplateViewModel(), $("#plateTemplate")[0]);
});
Maybe the answer is to do the load inside the ready() function, and pass in data as a parameter? Not sure what happens when I want to create a New item, but I can get to that.
Additionally, when I try save, I notice that even though I might change a field in the view (Update Description, for example), the data in the observed view model (self.Data) doesn't change.
Your input field could be this:
<div data-bind="with: Data">
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Description">
</div>
I prefer using with as its cleaner and should stop the confusion and issues you were having.
The reason that error is there is because the html is already bound before the data is loaded. So either don't apply bindings until the data is loaded:
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
ko.applyBindings(self, document.getElementById("container"));
});
Or wrap the template with an if, therefore it won't give you this error as Data is undefined originally.
self.Data = ko.observable(); // undefined
<!-- ko if: Data -->
<div data-bind="with: Data">
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Description">
</div>
<!-- /ko -->
Also if you know what the data model is gonna be, you could default data to this.
self.Data = ko.observable(new Data());
Apply Bindings Method:
var viewModel = null;
$(document).ready(function () {
viewModel = new PlateTemplateViewModel();
viewModel.loadData();
});

Does a writable computed observable really need an extra internal observable?

I am trying to use a writable computed observable, but do not really get it why I cannot get some data in the observable by just typing in it. I found that I need an extra observable to copy content from the write: to the read:, which seems weird.
self.fullName = ko.pureComputed({
read: function () {
return ...data from other observables to show in the observable;
},
write: function (value) {
// value is the content in the input
// can be sent to other observables
},
owner: self
});
I found that in the above model, what you type in the observable is not really inside.
In the complete example below (including test output and comments) I use an extra observable to copy data from write: to read:. Crucial in my project is the checkbox to decide if you get two observables filled identically by typing in one of them, or differently by typing in both of them.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Writable computed observables</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src='https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.1/knockout-min.js'></script>
</head>
<body>
<h1>Writable computed observables</h1>
<p>See: Knockout Doc</p>
<h2>Original example</h2>
<div>
First name: <input data-bind="textInput: firstName" />
<span data-bind="text: firstName"></span>
</div>
<div>
Last name: <input data-bind="textInput: lastName" />
<span data-bind="text: lastName"></span>
</div>
<div class="heading">
Hello, <input data-bind="textInput: fullName" />
<span data-bind="text: fullName"></span>
</div>
<h2>My example</h2>
<div>
Name: <input data-bind="textInput: Name" />
<span data-bind="text: Name"></span>
</div>
<div>
Mirror first name? <input type="checkbox" data-bind="checked: cbMirror" />
<span data-bind="text: cbMirror"></span>
</div>
<script>
function MyViewModel() {
var self = this;
// example from knockout site:
self.firstName = ko.observable('Planet');
self.lastName = ko.observable('Earth');
self.fullName = ko.pureComputed({
read: function () {
//return;
return self.firstName() + " " + self.lastName();
},
write: function (value) {
// value is the content in the input field, visible on form,
// but apparently not yet in the observable.
// now copy this value to first/last-name observables,
// that in turn copy it to the read-function,
// that returns it to the observable.
var lastSpacePos = value.lastIndexOf(" ");
if (lastSpacePos > 0) { // Ignore values with no space character
self.firstName(value.substring(0, lastSpacePos)); // Update "firstName"
self.lastName(value.substring(lastSpacePos + 1)); // Update "lastName"
}
},
owner: self
});
// checkbox whether or not to mirror between two fields
self.cbMirror = ko.observable(false);
// this observable is to help the writable computed observable to copy input from write() to read()
self.tmpName = ko.observable();
// the writable computed observable that may mirror another field, depending on the checkbox
self.Name = ko.pureComputed({
read: function () {
return self.cbMirror() ? self.firstName() : self.tmpName();
},
write: function (value) {
//if (self.cbMirror()){
// self.firstName(value);
//}else{
self.tmpName(value);
//}
},
owner: self
});
}
ko.applyBindings(new MyViewModel());
</script>
</body>
</html>
The question, hence: is there really no better way, to directly get some content from write: to read: without the extra observable self.tmpName?
Update:
With the understanding gained from the Answer below, I could simplify the write: part of my example code, see the unneeded code that I commented-out.
Yes you need an observable if you want to store the user input. A computed doesn't store information it only modifies it. It's like the difference between a variable and a function. Functions don't store their values for later viewing it's only modifying an input and giving an output.
the writable computed observable doesn't store data. It passes the data to the backing observable. That's the entire point of the write portion is to take the value and store it somewhere. If you add another span to look at the value of tmpName you'll see that it's storing whatever you type unless cbMirror is checked.

cant make an object I added to a knockout model observable

Here is the fiddle demonstrating the problem http://jsfiddle.net/LkqTU/31955/
I made a representation of my actual problem in the fiddle. I am loading an object via web api 2 and ajax and inserting it into my knockout model. however when I do this it appears the attributes are no longer observable. I'm not sure how to make them observable. in the example you will see that the text box and span load with the original value however updating the textbox does not update the value.
here is the javascript.
function model() {
var self = this;
this.emp = ko.observable('');
this.loademp = function() {
self.emp({
name: 'Bryan'
});
}
}
var mymodel = new model();
$(document).ready(function() {
ko.applyBindings(mymodel);
});
here is the html
<button data-bind="click: loademp">
load emp
</button>
<div data-bind="with: emp">
<input data-bind="value: name" />
<span data-bind="text: name"></span>
</div>
You need to make name property observable:
this.loademp = function(){
self.emp({name: ko.observable('Bryan')});
}

Read content from DOM element with KnockoutJS

The goal
Read content from DOM element with KnockoutJS.
The problem
I have a list of products in my HTML. The code is something like this:
<li>
<div class="introduction">
<h3>#Model["ProductName"]</h3>
</div>
<form data-bind="submit: addToSummary">
<input type="number"
placeholder="How much #Model["ProductName"] do you want?" />
<button>Add product</button>
</form>
</li>
When I click on <button>Add Product</button>, I want to send to KnockoutJS the text inside <h3></h3> of the element that was submitted.
The file to work with KnockoutJS is external and independent of HTML. It name is summary.js and the code is below:
function ProductReservation(id, name, unity, quantity) {
var self = this;
self.id = id;
self.name = name;
self.unity = unity;
self.quantity = quantity;
}
function SummaryViewModel() {
var self = this;
self.products = ko.observableArray([
new ProductReservation(1, "Testing", "kgs", 1)
]);
self.addToSummary = function () {
// Do Something
}
}
What I'm thinking about
HTML:
<li>
<div class="introduction">
<h3 data-bind="text: productName">#Model["ProductName"]</h3>
</div>
[...]
</li>
JS:
productName = ko.observable("text: productName");
But, of course, no success — this is not the correct context or syntax, was just to illustrate.
So I ask: what I need to do?
You're binding addToSummary via the submit binding. By default KO sends the form element to submit-bound functions.
So addToSummary should have a parameter -
self.addToSummary = function (formElement) {
// Do Something
}
You can pass additional parameters to this function (as described in KO's click binding documentation under 'Note 2'), or you could just add a hidden field to your form element and pull it from there.
<li>
<div class="introduction">
<h3>#Model["ProductName"]</h3>
</div>
<form data-bind="submit: addToSummary">
<input type="number" name="quantity"
placeholder="How much #Model["ProductName"] do you want?" />
<input type="hidden" name="productName" value="#Model["ProductName"]" />
<button>Add product</button>
</form>
</li>
Then in your knockout code you could use jQuery to process the form element to pull out the data -
self.addToSummary = function (formElement) {
var productName = $(formElement).children('[name="productName"]')[0].val();
var quantity= $(formElement).children('[name="quantity"]')[0].val();
// ...
self.products.push(new ProductReservation(?, productName, ?, quantity));
}
One strategy that has worked well in my experience is to implement a toJSON extension method that serializes your model (if you use a library like JSON.NET you can have a lot of control over what gets serialized and what does not).
Inside of the view where you initialize KnockoutJS, you could serialize your model and pass it into the KnockoutJS ViewModel you're creating (assuming :
Main view:
<script type="text/javascript">
var viewModel = new MyViewModel(#Model.ToJSON());
</script>
ViewModel:
function MyViewModel(options) {
this.productName = ko.observable(options.ProductName);
}

How can I get Knockout JS to data-bind on keypress instead of lost-focus?

This example of knockout js works so when you edit a field and press TAB, the viewmodel data and hence the text below the fields is updated.
How can I change this code so that the viewmodel data is updated every keypress?
<!doctype html>
<html>
<title>knockout js</title>
<head>
<script type="text/javascript" src="js/knockout-1.1.1.debug.js"></script>
<script type="text/javascript">
window.onload= function() {
var viewModel = {
firstName : ko.observable("Jim"),
lastName : ko.observable("Smith")
};
viewModel.fullName = ko.dependentObservable(function () {
return viewModel.firstName() + " " + viewModel.lastName();
});
ko.applyBindings(viewModel);
}
</script>
</head>
<body>
<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
</body>
</html>
<body>
<p>First name: <input data-bind="value: firstName, valueUpdate: 'afterkeydown'" /></p>
<p>Last name: <input data-bind="value: lastName, valueUpdate: 'afterkeydown'" /></p>
<h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
</body>
From the documentation
Additional parameters
valueUpdate
If your binding also includes a parameter called valueUpdate, this
defines which browser event KO should use to detect changes. The
following string values are the most commonly useful choices:
"change" (default) - updates your view model when the user
moves the focus to a different control, or in the case of
elements, immediately after any change
"keyup" - updates your view model when the user releases a key
"keypress" - updates your view model when the user has typed a
key. Unlike keyup, this updates repeatedly while the user holds a key
down
"afterkeydown" - updates your view model as soon as the user
begins typing a character. This works by catching the browser’s
keydown event and handling the event asynchronously.
Of these options, "afterkeydown" is the best choice if you want to
keep your view model updated in real-time.
In version 3.2 you can simply use textinput binding.:
<input data-bind="textInput: userName" />
It does two important things:
make immediate updates
handles browser differences for cut, drag, autocomplete ...
So no need for additional modules, custom controls and other things.
If you want it to do updates on afterkeydown "by default," you could inject the valueUpdate binding in the value binding handler. Just supply a new allBindingsAccessor for the handler to use that includes afterkeydown.
(function () {
var valueHandler = ko.bindingHandlers.value;
var getInjectValueUpdate = function (allBindingsAccessor) {
var AFTERKEYDOWN = 'afterkeydown';
return function () {
var allBindings = ko.utils.extend({}, allBindingsAccessor()),
valueUpdate = allBindings.valueUpdate;
if (valueUpdate === undefined) {
return ko.utils.extend(allBindings, { valueUpdate: AFTERKEYDOWN });
} else if (typeof valueUpdate === 'string' && valueUpdate !== AFTERKEYDOWN) {
return ko.utils.extend(allBindings, { valueUpdate: [valueUpdate, AFTERKEYDOWN] });
} else if (typeof valueUpdate === 'array' && ko.utils.arrayIndexOf(valueUpdate, AFTERKEYDOWN) === -1) {
valueUpdate = ko.utils.arrayPushAll([AFTERKEYDOWN], valueUpdate);
return ko.utils.extend(allBindings, {valueUpdate: valueUpdate});
}
return allBindings;
};
};
ko.bindingHandlers.value = {
// only needed for init
'init': function (element, valueAccessor, allBindingsAccessor) {
allBindingsAccessor = getInjectValueUpdate(allBindingsAccessor);
return valueHandler.init(element, valueAccessor, allBindingsAccessor);
},
'update': valueHandler.update
};
} ());
If you're not comfortable "overriding" the value binding, you could give the overriding custom binding a different name and use that binding handler.
ko.bindingHandlers.realtimeValue = { 'init':..., 'update':... };
demo
A solution like this would be suitable for Knockout version 2.x. The Knockout team has fleshed out a more complete binding for text-like inputs through the textInput binding in Knockout version 3 and up. It was designed to handle all text input methods for text inputs and textarea. It will even handle real time updating which effectively renders this approach obsolete.
Jeff Mercado's answer is fantastic, but unfortunately broken with Knockout 3.
But I found the answer suggested by the ko devs while working through Knockout 3 changes. See the bottom comments at https://github.com/knockout/knockout/pull/932. Their code:
//automatically add valueUpdate="afterkeydown" on every value binding
(function () {
var getInjectValueUpdate = function (allBindings) {
return {
has: function (bindingKey) {
return (bindingKey == 'valueUpdate') || allBindings.has(bindingKey);
},
get: function (bindingKey) {
var binding = allBindings.get(bindingKey);
if (bindingKey == 'valueUpdate') {
binding = binding ? [].concat(binding, 'afterkeydown') : 'afterkeydown';
}
return binding;
}
};
};
var valueInitHandler = ko.bindingHandlers.value.init;
ko.bindingHandlers.value.init = function (element, valueAccessor, allBindings, viewModel, bindingContext) {
return valueInitHandler(element, valueAccessor, getInjectValueUpdate(allBindings), viewModel, bindingContext);
};
}());
http://jsfiddle.net/mbest/GKJnt/
Edit
Ko 3.2.0 now has a more complete solution with the new "textInput" binding. See SalvidorDali's answer

Categories

Resources