KnockoutJS : Validate model's property only if the bound control is visible - javascript

I have model in a page that is bound to several controls. Based on some condition some of these controls will be visible or invisible. And on the final submit I should only validate those which are visible.
The following is a sample code to explain my requirement
<script src="knockout-3.4.0.js" type="text/javascript"></script>
<input type="checkbox" data-bind="checked:requireAge" >Age Required</input><br />
Name : <input data-bind="value:Name" /><br />
<div data-bind="visible:requireAge">
Age: <input data-bind="value:Age,visible:requireAge" />
</div>
<button type="button" onclick="validateModel();">Validate</button>
<script type="text/javascript">
var viewModel = { Name: ko.observable(), Age: ko.observable(),requireAge:ko.observable(false) };
ko.applyBindings(viewModel);
function validateModel() {
//validate visible properties and throw a common message that all visible fields should be filled
}
</script>

My suggestion is to use the knockout-validation library (you made no mention of it in your question so I assume you're not using it already) It ties in seamlessly with knockout and makes validation far more convenient. I've used it extensively over the past year and its make my life a whole lot easier. No need to create computeds to keep track of whether an observable contains a valid value or not. You can find the knockout-validation library on github.
In your case you can simply do the following:
var viewModel = function(){
var self = this;
self.name = ko.observable();
self.requireAge = ko.observable(false);
self.age = ko.observable().extend({
required: {
onlyIf: function() { return self.requireAge(); }
}
});
};
Validation error messages are inserted automatically below the element the observable is bound to. You can also create your own validation rules but there are many that work straight out the box including the one demonstrated above. You can even use some data attributes for some rules. This is probably the best way to go about validation in conjunction with knockout.

Based on some condition some of these controls will be visible or invisible.
It would be better if these conditions are contained in the model. And validation method too.
See snippet:
var viewModel = function() {
this.Name = ko.observable("");
this.Age = ko.observable("");
this.requireAge = ko.observable(false);
this.isValid = ko.computed(function() {
if (ko.unwrap(this.Name).length === 0) return false;
if (ko.unwrap(this.requireAge) &&
ko.unwrap(this.Age).length === 0) return false;
return true;
}, this);
};
window.onload = function() {
ko.applyBindings(new viewModel());
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="checkbox" data-bind="checked:requireAge" >Age Required</input><br />
Name : <input data-bind="value:Name" /><br />
<div data-bind="visible:requireAge">
Age: <input data-bind="value:Age,visible:requireAge" />
</div>
<div>is valid: <span data-bind="text: isValid"></span></div>

Related

Referencing properties of object constructor outside view model in knockout

So, I'm not entirely sure how to phrase this question as it's sort of two in one. I'm having a weird issue where I have an object constructor to create new 'projects' from an HTML form which are then pushed into an observableArray when the form is submitted. Everything works fine but to reference the related observables I have to use 'value: Project.title' or 'value: Project.whatever'. I haven't seen 'value: NameOfConstructor.property' used in any of the examples I've seen. I assume this works this way because the constructor is outside of my view model.
My question is this: Is there a better way to assign the value of a property in a constructor that is not in my view model? In other words, is there a better or more correct way than 'Project.title', ect?
I ask partially because one thing in my code doesn't work currently; the knockout enable property doesn't work on my "New Project" button, it stays disabled even if there is something written in the 'title' input box. I have the feeling it's because it's written as data-bind='enable: Project.title' but I can't figure how else to write it.
I've included a jsfiddle for reference though it obviously isn't working because of external dependencies.
https://jsfiddle.net/bmLh0vf1/1/
My HTML:
<form id='addBox' action='#' method='post'>
<label for='pTitle'> Title: </label>
<input id='pTitle' data-bind='value: Project.title' />
<br/>
<label for='pPriority'> Priority </label>
<select id='pPriority' data-bind='options: priorityOptions, value: Project.priority'></select>
<br/>
<button data-bind='enable: Project.title, click: newProject'>New Project</button>
</form>
And my Javascript:
function Project(title, priority) {
this.title = ko.observable(title);
this.priority = ko.observable(priority);
};
function ProjectViewModel() {
var self = this;
this.priorityOptions = ko.observableArray(['High', 'Medium', 'Low'])
this.projectList = ko.observableArray([
new Project('Create App', 'High')
]);
this.newProject = function() {
var np = new Project(Project.title, Project.priority);
self.projectList.push(new Project(Project.title, Project.priority));
console.log(self.projectList().length);
if (self.projectList().length > 1) {
console.log(self.projectList()[1].title());
};
}
};
var viewModel = new ProjectViewModel();
$(document).ready(function() {
ko.applyBindings(viewModel);
});
Lastly, I apologize if I've missed any posting conventions or if my code is especially bad. I'm very new and still teaching myself.
Your code is setting title and priority properties on the object created by new Project, but then later you're expecting to see those properties on Project itself. It doesn't have them; Project is the function, not the object created by new Project. So Project.title and Project.priority will give you undefined (and not an observable, and so not useful targets for the value binding).
Instead, have an "editing" Project instance that you use, binding the value of the inputs to the editing' instances title and priority, and then in newProject grab that instance and replace it with a new, fresh one.
Roughly speaking, in ProjectViewModel's constructor:
this.editing = ko.observable(new Project());
Update Project to default title and priority:
function Project(title, priority) {
this.title = ko.observable(title || "");
this.priority = ko.observable(priority || "Medium");
}
And in the bindings:
<input id='pTitle' data-bind='value: editing().title' />
<select id='pPriority' data-bind='options: priorityOptions, value: editing().priority'></select>
And in newProject:
var np = this.editing();
this.editing(new Project());
Then use np (instead of another new Project) when adding to the array.
Here's a simplified example:
function Project(title, priority) {
this.title = ko.observable(title || "");
this.priority = ko.observable(priority || "Medium");
}
function ProjectViewModel() {
var self = this;
this.priorityOptions = ko.observableArray(["High", "Medium", "Low"]);
this.projects = ko.observableArray();
this.editing = ko.observable(new Project());
this.addProject = function() {
this.projects.push(this.editing());
this.editing(new Project());
};
}
ko.applyBindings(new ProjectViewModel(), document.body);
<div>
<div>
<label>
Title:
<input type="text" data-bind="value: editing().title, valueUpdate: 'input'">
</label>
</div>
<div>
<label>
Priority:
<select data-bind='options: priorityOptions, value: editing().priority'></select>
</label>
</div>
<div>
<button type="button" data-bind="click: addProject, enable: editing().title">Add Project</button>
</div>
<hr>
<div>Projects:</div>
<div data-bind="foreach: projects">
<div>
<span data-bind="text: title"></span>
(<span data-bind="text: priority"></span>)
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

Radio buttons bound to knockout computed

I have some problems with knockoutjs writables computes observables I think.
I created a fiddle.
What I need is actually not so hard:
I have the nullable WeightInGramms and VolumeInMilliliters values.
These values should be bound to two input fields (only one of them should be visible).
At the top, the user can choose which of these values he want to use with the radio buttons.
At initialisation, when both of them are null, the "g" radio button should be checked, also when WeightInGramms is not null. When VolumeInMilliliters have some value, the "ml" radio button should be checked.
I used a knockoutjs writable computes observable for this, please correct me if there is a better way to do this!
So, the read function seems to work, when I change the value in the input which are bind to WeightInGramms or VolumeInMilliliters. But when I change the radio buttons nothing happens...
var ViewModel = function (data) {
var self = this;
this.VolumeInMilliliters = ko.observable(data.VolumeInMilliliters);
this.WeightInGramms = ko.observable(data.WeightInGramms);
this.GrammIsSelected = ko.computed({
read: function() {
return (!self.WeightInGramms() && !self.VolumeInMilliliters()) || !self.VolumeInMilliliters();
},
write: function (newValue) {
console.log(newValue);
return newValue;
},
owner: this
});
};
When I change the radio buttons, the corresponding input field should be visible:
<div data-bind="visible: GrammIsSelected">g is active</div>
<div data-bind="visible: !GrammIsSelected()">ml is active</div>
Edit:
When the Form is loaded for the first Time both values will be null -> the "g" button should be checked.
The observables can be initialized with:
null, null
33, null
null, 33
Both can be null, but only one of them can have a value.
If the user types in a value, and then clicks the other radio the value can be applied to the other value.
I hop it is a bit clearer
Some tips:
Make your viewModel (JS) resemble the view (HTML) as much as possible. Additionally, this avoids having to repeat too much markup. In this case, radio buttons are always lists, and so it is most convenient to store the options in an array.
Instead of testing whether GrammIsselected, you should define a selected observable that holds the selected metric. This way if you ever add more options, the code will still work without refactoring.
When to use a computed property? A computed property adds readonly value by calculating a result based on multiple observables/ variables. A writeable computed property does the same, except you can write back changes. This makes it especially useful for 'Select all' style checkboxes (see example 2 in the docs), data validation & transformations.
The absolutely clearest setup for what you want to achieve would be the following:
var ViewModel = function (data) {
this.metrics = [
{ name: 'g', value: ko.observable(data.WeightInGramms) },
{ name: 'ml', value: ko.observable(data.VolumeInMilliliters) }
];
this.selectedMetric = ko.observable(this.metrics[0]);
};
By setting an object as observable (selectedMetric), you can furthermore simplify the markup for the volume/weight input:
<div class="control-group">
<label class="control-label">choose</label>
<div class="controls" data-bind="with: selectedMetric">
<input type="text" data-bind="value: value">
<span class="help-inline" data-bind="text: '(' + name + ')'"></span>
</div>
</div>
Getting the 'final value' of your app would be as easy as retrieving selectedMetric().value().
A computed property isn't super useful here, but for example, if you wanted to provide a way for the user to both set the g/ml with radio buttons and text, you could add the following method to your viewModel:
this.selectedMetricByText = ko.computed({
read: function() {
return this.selectedMetric().name;
},
write: function(value) {
var newMetric = ko.utils.arrayFirst(this.metrics, function(metric) {
return metric.name === value;
}) || false;
this.selectedMetric(newMetric || this.metrics[0]);
}
}, this);
Fiddle
Your write function doesn't write anything, it seems?
Contrary to this other answer, based on my experience I'll give you the advice not to avoid writeable computeds: used wisely they can be very effective!
Note: in my answer I try to remain close to the original design from the question, but if you're able (have resources available) I recommend redesigning things even more based on the answer by #Tyblitz.
Here's the way you could approach this utilizing a computed:
var ViewModel = function (data) {
var self = this;
self.VolumeInMilliliters = ko.observable(data.VolumeInMilliliters);
self.WeightInGramms = ko.observable(data.WeightInGramms);
var _measurementType = ko.observable("volume");
self.MeasurementType = ko.computed({
read: function() {
return _measurementType();
},
write: function (newValue) {
_measurementType(newValue);
self.VolumeInMilliliters(newValue === "volume" ? 0 : null);
self.WeightInGramms(newValue === "mass" ? 0 : null);
}
});
};
ko.applyBindings(new ViewModel({ VolumeInMilliliters: 12 }));
label { cursor: pointer; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<label>
<input type="radio" name="measurementType" value="volume" data-bind="checked: MeasurementType">
Volume
</label>
<input data-bind="value: VolumeInMilliliters, enable: MeasurementType() === 'volume'">
<label>
<input type="radio" name="measurementType" value="mass" data-bind="checked: MeasurementType">
Weight in gramms
</label>
<input data-bind="value: WeightInGramms, enable: MeasurementType() === 'mass'">
For radio buttons, you need to use the "checked" binding.
http://knockoutjs.com/documentation/checked-binding.html
And for my personal experience (as a KO nija) I have to give you the advice: avoid writeable ko computed.
<input type="radio" name="unitSelector" value="g" data-bind="checked: unit" /> Grams</br>
<input type="radio" name="unitSelector" value="ml" data-bind="checked: unit" /> Millis</br>
Now the view model
var ViewModel = function (data) {
var self = this;
self.unit = ko.observable('g');
self.userValue = ko.observable(data.WeightInGramms);
};
Now the binding should only care about the value entered by the user, you don't need computed here and you don't need two fields...
<input type="text" data-bind="textInput: userValue ">
<span data-bind="text: unit"> </span>
It looks really too simple but that's what you need, as #Jotabe mentioned, you should take measurement and the unit as two separate things... what you do with this thing later, could be done with computed observables.
If this thing doesn't solve your problem then you should tell what you really want...

DataPicker not getting binded to textbox ? fiddle provided

Well in other cases i will get datepicker binded to my textbox which will be straight forward but not in this case .
Fiddle link : http://jsfiddle.net/JL26Z/1/ .. while to setup perfect seanrio i tried but unable to bind datepicker to textboxes . except that everything is in place
My code :
**<script id="Customisation" type="text/html">** // here i need to have text/html
<table style="width:1100px;height:40px;" align="center" >
<tr>
<input style="width:125px;height:auto;" class="txtBoxEffectiveDate" type="text" id="txtEffective" data-bind="" />
</tr>
</script>
The above code is used for my dynamic generation of same thing n no of time when i click each time on a button . So above thing is a TEMPLATE sort of thing .
My knockout code :
<div data-bind="template:{name:'Customisation', foreach:CustomisationList},visible:isVisible"></div>
<button data-bind="click:$root.CustomisatioAdd" >add </button>
I tried same old way to bind it with datepicker
$('#txtEffective').datepicker(); // in document.ready i placed
Actually to test this i created a textbox with some id outside script with text/html and binded datepicker to it and It is working fine sadly its not working for the textbox inside text/html and i want to work at any cost.
PS: well i haven't posted my view model as it is not required in this issue based senario
View model added with Js
var paymentsModel = function ()
{
function Customisation()
{
var self = this;
}
var self = this;
self.isVisible = ko.observable(false);
self.CustomisationList = ko.observableArray([new Customisation()]);
self.CustomisationRemove = function () {
self.CustomisationList.remove(this);
};
self.CustomisatioAdd = function () {
if (self.isVisible() === false)
{
self.isVisible(true);
}
else
{
self.CustomisationList.push(new Customisation());
}
};
}
$(document).ready(function()
{
$('#txtEffective').datepicker();
ko.applyBindings(new paymentsModel());
});
Any possible work around is appreciated
Regards
The best way I've found to do this is create a simple bindingHandler.
This is adapted from code I have locally, you may need to tweak it...
** code removed, see below **
Then update your template:
** code removed, see below **
By using a bindingHandler you don't need to try to hook this up later, it's done by knockout when it databinds.
Hope this is helpful.
EDIT
I created a fiddle, because I did indeed need to tweak the date picker binding quite a lot. Here's a link to the Fiddle, and here's the code with some notes. First up, the HTML:
<form id="employeeForm" name="employeeForm" method="POST">
<script id="PhoneTemplate" type="text/html">
<div>
<span>
<label>Country Code:</label>
<input type="text" data-bind="value: countryCode" />
</span>
<span><br/>
<label>Date:</label>
<input type="text" data-bind="datepicker: date" />
</span>
<span>
<label>Phone Number:</label>
<input type="text" data-bind="value: phoneNumber" />
</span>
<input type="button" value="Remove" data-bind="click: $parent.remove" />
</div>
</script>
<div>
<h2>Employee Phone Number</h2>
<div data-bind="template:{name:'PhoneTemplate', foreach:PhoneList}">
</div>
<div>
<input type="button" value="Add Another" data-bind="click: add" />
</div>
</div>
</form>
Note I removed the id=... from in your template; because your template repeats per phone number, and ids must be unique to be meaningful. Also, I removed the datepicker: binding from the country code and phone number elements, and added it only to the date field. Also - the syntax changed to "datepicker: ". If you need to specify date picker options, you would do it like this:
<input type="text" data-bind="datepicker: myObservable, datepickerOptions: { optionName: optionValue }" />
Where optionName and optionValue would come from the jQueryUI documentation for datepicker.
Now for the code and some notes:
// Adapted from this answer:
// https://stackoverflow.com/a/6613255/1634810
ko.bindingHandlers.datepicker = {
init: function(element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().datepickerOptions || {},
observable = valueAccessor(),
$el = $(element);
// Adapted from this answer:
// https://stackoverflow.com/a/8147201/1634810
options.onSelect = function () {
if (ko.isObservable(observable)) {
observable($el.datepicker('getDate'));
}
};
$el.datepicker(options);
// set the initial value
var value = ko.unwrap(valueAccessor());
if (value) {
$el.datepicker("setDate", value);
}
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$el.datepicker("destroy");
});
},
update: function(element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()),
$el = $(element);
//handle date data coming via json from Microsoft
if (String(value).indexOf('/Date(') === 0) {
value = new Date(parseInt(value.replace(/\/Date\((.*?)\)\//gi, "$1")));
}
var current = $el.datepicker("getDate");
if (value - current !== 0) {
$el.datepicker("setDate", value);
}
}
};
function Phone() {
var self = this;
self.countryCode = ko.observable('');
self.date = ko.observable('');
self.phoneNumber = ko.observable('');
}
function PhoneViewModel() {
var self = this;
self.PhoneList = ko.observableArray([new Phone()]);
self.remove = function () {
self.PhoneList.remove(this);
};
self.add = function () {
self.PhoneList.push(new Phone());
};
}
var phoneModel = new PhoneViewModel();
ko.applyBindings(phoneModel);
Note the very updated binding handler which was adapted from this answer for the binding, and this answer for handling onSelect.
I also included countryCode, date, and phoneNumber observables inside your Phone() object, and turned your model into a global variable phoneModel. From a debugger window (F12 in Chrome) you can type something like:
phoneModel.PhoneList()[0].date()
This will show you the current value of the date.
I notice that your form is set up to post somewhere. I would recommend instead that you add a click handler to a "Submit" button and post the values from your phoneModel using ajax.
Hope this edit helps.
Dynamic entities need to have datepicker applied after they are created. To do this I'd use an on-click function somewhere along the lines of
HTML
<!-- Note the id added here -->
<button data-bind="click:$root.CustomisatioAdd" id="addForm" >add </button>
<script>
$(document).on('click', '#addForm', function(){
$('[id$="txtEffective"]').datepicker();
});
</script>

knockoutjs automatic generation of viewmodel

Does anyone know if it is possible to generate the viewmodel for knockoutjs automatically.
1) I have (for example) the following html code:
<input type="text" data-bind="value: XYZ" />
<input type="text" data-bind="value: ABC" />
<input type="text" data-bind="value: Prop1" />
2) Then I need to create a viewmodel like this:
function ViewModel()
{
this.XYZ = ko.observable();
this.ABC = ko.observable();
this.Prop1 = ko.observable();
}
I would like to skip step 2, because I already defined the properties which should be available in the html-markup (sort of).
update:
As mentioned in the comments, there would be drawbacks to do such a thing. (That's probably the reason it is not supported by knockoutjs). I will take a different approach, but nevertheless I made a simple Javascript function which can generate a ViewModel out of a VERY simple markup. If anyone is interested, that's what I came up with:
function getViewModelFromView(selector) {
var vm = {};
$("[data-bind]", selector).each(function () {
var attribute = $(this).attr("data-bind");
if (attribute.indexOf(':') > 0) {
vm[attribute.slice(attribute.indexOf(':')+1).trim()] = ko.observable();
}
});
return vm;
}

Working with a list of checkboxes in knockoutjs

I'm trying to get my head around Knockout.js and I'm quite stuck when it comes to checkboxes.
Server side I'm populating a set of checkboxes with their corresponding values. Now, when any of the unchecked checkboxes are checked, I need to store it's value in a comma-seperated string. When they're unchecked, the value needs to be deleted from the string.
Have anyone got a hint on how to achieve this with knockoutjs?
I have the following code so far:
ViewModel:
$().ready(function() {
function classPreValue(preValue)
{
return {
preValue : ko.observable(preValue)
}
}
var editOfferViewModel = {
maxNumOfVisitors : ko.observable(""),
goals : ko.observable(""),
description : ko.observable(""),
contact : ko.observable(""),
comments : ko.observable(""),
classPreValues : ko.observableArray([]),
addPreValue : function(element) {
alert($(element).val());
this.classPreValues.push(new classPreValue(element.val()));
}
};
ko.applyBindings(editOfferViewModel);
});
And my checkboxes are populated with a foreach loop:
<input data-bind="checked: function() { editOfferViewModel.addPreValue(this) }"
type="checkbox" checked="yes" value='#s'>
#s
</input>
I try to pass the checkbox element as the parameter to my addPreValue() function, but nothing seems to happen when I check the checkbox?
Any help/hints on this is greatly appreciated!
The checked binding expects to be passed a structure that it can read/write against. This could be a variable, an observable, or a writable dependentObservable.
When passed an array or observableArray, the checked binding does know how to add and remove simple values from the array.
Here is a sample that also includes a computed observable that contains the array as comma delimited values. http://jsfiddle.net/rniemeyer/Jm2Mh/
var viewModel = {
choices: ["one", "two", "three", "four", "five"],
selectedChoices: ko.observableArray(["two", "four"])
};
viewModel.selectedChoicesDelimited = ko.computed(function() {
return this.selectedChoices().join(",");
}, viewModel);
ko.applyBindings(viewModel);
HTML:
<ul data-bind="template: { name: 'choiceTmpl', foreach: choices, templateOptions: { selections: selectedChoices } }"></ul>
<script id="choiceTmpl" type="text/html">
<li>
<input type="checkbox" data-bind="attr: { value: $data }, checked: $item.selections" />
<span data-bind="text: $data"></span>
</li>
</script>
Why isn't there a Mutually exclusive checkboxes example Online somewhere
Since this link came up first whilst I was searching for mutually exclusive checkboxes I will share my answer here. I was banging my head against the wall with all my attempts. By the way, when you handle the click event in a binding in-line knockoutjs it seems to disconnect the bindings(maybe only because I tried to call my resetIllnesses function as defined below) even if you return true from the function. Maybe there is a better way but until then follow my lead.
Here is the type I needed to bind.
var IllnessType = function (name,title) {
this.Title = ko.observable(title);
this.Name = ko.observable(name);
this.IsSelected = ko.observable(false);
};
The array to bind with.
model.IllnessTypes = ko.observableArray(
[new IllnessType('IsSkinDisorder', 'Skin Disorder'),
new IllnessType('IsRespiratoryProblem', 'Respiratory Problem'),
new IllnessType('IsPoisoning', 'Poisoning'),
new IllnessType('IsHearingLoss', 'Hearing Loss'),
new IllnessType('IsOtherIllness', 'All Other Illness')]
);
The reset illness function to clear them all.
model.resetIllnesses = function () {
ko.utils.arrayForEach(model.IllnessTypes(), function (type) {
type.IsSelected(false);
});
};
The markup
<ul data-bind="foreach:IllnessTypes,visible: model.IsIllness()">
<li><label data-bind="html: Title"></label></li>
<li><input class="checkgroup2" type="checkbox"
data-bind="attr:{name: Name },checked:IsSelected" /></li>
</ul>
This just doesn't work
If you have been struggling with trying to call the resetIllness function as I below, you will feel my pain.
<input type='checkbox' data-bind="checked:IsSelected,
click: function() { model.resetIllnesses(); return true; }" />
you have been sharing my pain. Well, it works! when you call it from following example.
Notice that there is a class that I added above so that I can add the click function.
The script that makes all your problems go away.
<script type="text/javascript">
$(function() {
$(".checkgroup2").on('click', function() {
model.resetIllnesses();
var data = ko.dataFor(this);
data.IsSelected(true);
});
});
</script>
Send info to the server
Also, in my case I had to send the information up to the server differently than the default html format so I changed the inputs a little.
<input class="checkgroup2" type="checkbox" data-bind="checked:IsSelected" />
<input type="hidden" data-bind="attr:{name: Name },value:IsSelected" />

Categories