How to combine templates and radio buttons in knockoutjs scripts? - javascript

I am trying to show a group of radio buttons using knockoutjs. I also want to have one of the radio buttons selected based on a certain value and know the value of the selected radio button in case someone selects a different item.
This is the code I am trying to use:
<form>
<div data-bind="template: { name: 'items-template', foreach: items }"></div>
<h3 data-bind="text: selectedItem"></h3>
</form>
</body>
<script type="text/html" id="items-template">
<input type="radio" name="skuitem" data-bind="value: id, checked: selectedItem"><span data-bind="text: name"></span><br /></input>
</script>
<script type="text/javascript">
function ItemsViewModel() {
var _this = this;
_this.items = ko.observableArray();
_this.items.push({id: "1", name: "Item A"});
_this.items.push({id: "7", name: "Item B"});
_this.items.push({id: "10", name: "Item C"});
_this.selectedItem = ko.observable("1");
}
ko.applyBindings(new ItemsViewModel());
</script>
The problem is that I get an error in the console:
ReferenceError: selectedItem is not defined
From what I understand, knockout looks into the objects contained in the items array for "selectedItem" instead of binding to the "selectedItem" variable in my ItemsViewModel.
How can I bind my radio buttons to the selectedItem observable when I'm using templates ?

Inside your template, your binding context is an item, you'll only have id and name available to link to directly.
Any properties in the ItemsViewModel can be accessed using $parent or $root.
So, you'll have to write: data-bind="value: id, checked: $parent.selectedItem"
function ItemsViewModel() {
var _this = this;
_this.items = ko.observableArray();
_this.items.push({
id: "1",
name: "Item A"
});
_this.items.push({
id: "7",
name: "Item B"
});
_this.items.push({
id: "10",
name: "Item C"
});
_this.selectedItem = ko.observable("1");
}
ko.applyBindings(new ItemsViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<form>
<div data-bind="template: { name: 'items-template', foreach: items }"></div>
<h3 data-bind="text: selectedItem"></h3>
</form>
<script type="text/html" id="items-template">
<label>
<input type="radio" data-bind="value: id,
checked: $parent.selectedItem" />
<span data-bind="text: name"></span>
</label>
</script>
Instead of solving the issue in the view, you can also solve it in your models. You can pass a reference to the selection observable to each item:
function ItemsViewModel() {
var _this = this;
_this.items = ko.observableArray();
_this.selectedItem = ko.observable("1");
_this.items.push({
id: "1",
name: "Item A",
selectedItem: _this.selectedItem
});
_this.items.push({
id: "7",
name: "Item B",
selectedItem: _this.selectedItem
});
_this.items.push({
id: "10",
name: "Item C",
selectedItem: _this.selectedItem
});
}
ko.applyBindings(new ItemsViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<form>
<div data-bind="template: { name: 'items-template', foreach: items }"></div>
<h3 data-bind="text: selectedItem"></h3>
</form>
<script type="text/html" id="items-template">
<label>
<input type="radio" data-bind="value: id,
checked: selectedItem"/>
<span data-bind="text: name"></span>
</label>
</script>

Related

KnockoutJS - How to hide certain elements inside foreach using Observable Arrays?

I have a list of WebsiteOwners. I'm trying to build a UI which will display more information about the owners when I click on them.
this.toExpand = ko.observableArray(); //initialize an observable array
this.invertExpand = ko.observable("");
this.invertExpand = function (index) {
if (self.invertExpand[index] == false) {
self.invertExpand[index] = true;
alert(self.invertExpand[index]); //testing whether the value changed
}
else {
self.invertExpand[index] = false;
alert(self.invertExpand[index]); //testing whether the value changed
}
};
Here's the HTML code :
<div data-bind="foreach: WebsiteOwners">
<div>
<button data-bind="click: $root.invertExpand.bind(this,$index())" class="label label-default">>Click to Expand</button>
</div>
<div data-bind="visible: $root.toExpand()[$index]">
Primary Owner: <span data-bind="text:primaryOwner"></span>
Website Name : <span data-bind="text:websiteName"></span>
//...additional information
</div>
</div>
You can store one of your WebsiteOwner items directly in your observable. No need to use an index.
Don't forget you read an observable by calling it without arguments (e.g. self.invertExpand()) and you write to it by calling with a value (e.g. self.invertExpand(true))
I've included 3 examples in this answer:
One that allows only a single detail to be opened using knockout
One that allows all details to be opened and closed independently using knockout
One that does not use knockout but uses plain HTML instead 🙂
1. Accordion
Here's an example for a list that supports a single expanded element:
const websiteOwners = [
{ name: "Jane", role: "Admin" },
{ name: "Sarah", role: "Employee" },
{ name: "Hank", role: "Employee" }
];
const selectedOwner = ko.observable(null);
const isSelected = owner => selectedOwner() === owner;
const toggleSelect = owner => {
selectedOwner(
isSelected(owner) ? null : owner
);
}
ko.applyBindings({ websiteOwners, isSelected, toggleSelect });
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<ul data-bind="foreach: { data: websiteOwners, as: 'owner' }">
<li>
<span data-bind="text: name"></span>
<button data-bind="
click: toggleSelect,
text: isSelected(owner) ? 'collapse' : 'expand'"></button>
<div data-bind="
visible: isSelected(owner),
text: role"></div>
</li>
</ul>
2. Independent
If you want each of them to be able to expand/collapse independently, I suggest adding that state to an owner viewmodel:
const websiteOwners = [
{ name: "Jane", role: "Admin" },
{ name: "Sarah", role: "Employee" },
{ name: "Hank", role: "Employee" }
];
const OwnerVM = owner => ({
...owner,
isSelected: ko.observable(null),
toggleSelect: self => self.isSelected(!self.isSelected())
});
ko.applyBindings({ websiteOwners: websiteOwners.map(OwnerVM) });
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<ul data-bind="foreach: websiteOwners">
<li>
<span data-bind="text: name"></span>
<button data-bind="
click: toggleSelect,
text: isSelected() ? 'collapse' : 'expand'"></button>
<div data-bind="
visible: isSelected,
text: role"></div>
</li>
</ul>
3. Using <details>
This one leverages the power of the <details> element. It's probably more accessible and by far easier to implement!
const websiteOwners = [
{ name: "Jane", role: "Admin" },
{ name: "Sarah", role: "Employee" },
{ name: "Hank", role: "Employee" }
];
ko.applyBindings({ websiteOwners });
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<ul data-bind="foreach: websiteOwners">
<li>
<details>
<summary data-bind="text: name"></summary>
<div data-bind="text: role"></div>
</details>
</li>
</ul>

ObservableArray to Select Option in KnockOut JS

I want to turn array to select option via knockout js, I know 3 methods for this case, but none of these work perfectly with what I really want, what I want is:
Set default option Choose an option
get selected value
set attr for options
Each method has own issue, but last method has default option and can get selected value, but can't set attr, any idea?
Method 1:
Error:
Uncaught Error: The binding 'value' cannot be used with virtual
elements
Status: not working
function myfunc() {
var self = this;
self.estimate = ko.observableArray([]);
self.selectedValue = ko.observable();
var obj = [{
method_title: "blah blah",
price: "1000"
},
{
method_title: "blah blah 2",
price: "2000"
}
];
self.estimate(obj);
self.selectedValue.subscribe(function(value) {
alert(value);
});
}
ko.applyBindings(new myfunc());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select id="method">
<option value="0">Choose an option</option>
<!-- ko foreach: estimate, value: selectedValue -->
<option data-bind="text: method_title,
attr: { 'data-price': price, 'value': method_title },
text: method_title"></option>
<!-- /ko -->
</select>
Method 2:
Status: working but I could not add default option, it looped everytime.
function myfunc() {
var self = this;
self.estimate = ko.observableArray([]);
self.selectedValue = ko.observable();
var obj = [{
method_title: "blah blah",
price: "1000"
},
{
method_title: "blah blah 2",
price: "2000"
}
];
self.estimate(obj);
self.selectedValue.subscribe(function(value) {
alert(value);
});
}
ko.applyBindings(new myfunc());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select id="method" data-bind="foreach: estimate,value: selectedValue">
<option value="0">Choose an option</option>
<option data-bind="text: method_title,attr: {'data-price': price, value: method_title}"></option>
</select>
Method 3:
Status: working but I could not set attr
function myfunc() {
var self = this;
self.estimate = ko.observableArray([]);
self.selectedValue = ko.observable();
var obj = [{
method_title: "blah blah",
price: "1000"
},
{
method_title: "blah blah 2",
price: "2000"
}
];
self.estimate(obj);
self.selectedValue.subscribe(function(value) {
alert(value);
});
}
ko.applyBindings(new myfunc());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select id="method" data-bind="value: selectedValue,options: estimate,
optionsText: 'method_title',
optionsValue: 'method_title',
optionsCaption: 'Choose an option'"></select>
Your first method had the most promise and so I have corrected that. You don't need to use the value binding in the foreach loop. It has to be used in the <select>, and it works fine.
function myfunc() {
var self = this;
self.estimate = ko.observableArray([]);
self.selectedValue = ko.observable();
var obj = [{
method_title: "blah blah",
price: "1000"
},
{
method_title: "blah blah 2",
price: "2000"
}
];
self.estimate(obj);
self.selectedValue.subscribe(function(value) {
console.log(value);
});
}
ko.applyBindings(new myfunc());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select id="method" data-bind="value: selectedValue">
<option value="0">Choose an option</option>
<!-- ko foreach: estimate -->
<option data-bind="text: method_title,
attr: { 'data-price': price, 'value': method_title }"></option>
<!-- /ko -->
</select>
You just need a little bit modification with your 3rd method.
From knockout official documentation Knockout: The "options" binding you can use optionsAfterRender Parameter. I have modified your code. See if it helps
function myfunc() {
var self = this;
self.estimate = ko.observableArray([]);
self.selectedValue = ko.observable();
var obj = [{
method_title: "blah blah",
price: "1000",
href: "href 1",
title: "go to href 1"
},
{
method_title: "blah blah 2",
price: "2000",
href: "href 2",
title: "go to href 2"
}
];
self.setOptionAttr = function(option, item) {
if(item)
{
ko.applyBindingsToNode(option, {attr: {href:item.href,title:item.title}}, item);
}
}
self.estimate(obj);
self.selectedValue.subscribe(function(value) {
alert(value);
});
}
ko.applyBindings(new myfunc());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<select id="method" data-bind="value: selectedValue,options: estimate,
optionsText: 'method_title',
optionsValue: 'method_title',
optionsCaption: 'Choose an option',
optionsAfterRender: setOptionAttr"></select>

Change select drop-down option based on input value using autocomplete in Knockout JS

Using Knockout JS, when a user types into an input field and selects a value (ex: Fruit) using jquery-ui autocomplete selection, I'm trying to change the select drop-down of options in a separate drop-down.
Example scenario:
(1) User begins to type "Fru.."
(2) Selects "Fruit" in autocomplete input field.
(3) Dropdown options changes based on the value: "Fruit"
(4) Dropdown options only shows "Apples" or other options with equals id (ex: ABC)
Autocomplete Input Field
HTML
<input type="text"
id="searchItem"
placeholder="Search"
tabindex="0"
data-bind="textInput: searchItem, valueUpdate: 'onkeyup'"/>
ViewModel/JQuery (Autocomplete)
// Search Item
$(function() {
var searchItem = [
{ id: "ABC", name: "Fruit" },
{ id: "DEF", name: "Animal" },
{ id: "GHI", name: "Color" },
{ id: "JKL", name: "Clothing" }
];
$("#searchItem").autocomplete({
source: searchItem
});
});
Select dropdown
HTML
<select class="form-control"
id="alphabetList"
data-toggle="tooltip"
tabindex="0"
data-bind=" foreach: { data: alphabetList, as: 'item' }, value: selectedItem">
<option data-bind=" attr: { 'value': item.id }, text: item.name"></option>
</select>
ViewModel
// Alphabet List
this.alphabetList = ko.observableArray([
{ id: "ABC", name: "Apples" },
{ id: "DEF", name: "Dog" },
{ id: "GHI", name: "Green" },
{ id: "JKL", name: "Jacket" }
]);
On selection of an item in autocomplete, populate an observable called selectedId. Then create a computed property which filters the alphabetList based on selectedId
You have not mentioned where this autocomplete code exists. But, since you have mentioned ViewModel, I'm assuming you have access to the viewModel's instance in your jquery code.
Also, you don't need to use the foreach binding for displaying options. You can use options binding.
Here's a working snippet with all these changes:
var viewModel = function() {
var self = this;
self.selectedAlphabet = ko.observable();
self.selectedId = ko.observable();
self.searchItem = ko.observable();
self.alphabetList = ko.observableArray([
{ id: "ABC", name: "Apples" },
{ id: "DEF", name: "Dog" },
{ id: "GHI", name: "Green" },
{ id: "JKL", name: "Jacket" }
]);
// this gets triggerred everytime selectedId changes
self.availableAlphabetList = ko.pureComputed(() => {
return self.alphabetList().filter(item => item.id == self.selectedId());
});
}
// I have created an instance to use it in jquery code
var instance = new viewModel();
ko.applyBindings(instance);
$(function() {
var searchItem = [
{ id: "ABC", name: "Fruit" },
{ id: "DEF", name: "Animal" },
{ id: "GHI", name: "Color" },
{ id: "JKL", name: "Clothing" }];
$("#searchItem").autocomplete({
// creating an array with a "label" property for autocomplete
source: searchItem.map(function(item) {
return {
label: item.name,
id: item.id
}
}),
// on select populate the selectedId
select: function(event, ui) {
// if this jquery code is within viewModel, then use "self.selectedId"
instance.selectedId(ui.item.id)
}
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.9.2/jquery-ui.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.9.2/themes/base/jquery-ui.css">
<input type="text" id="searchItem" placeholder="Search" tabindex="0" data-bind="textInput: searchItem, valueUpdate: 'onkeyup'" />
<select class="form-control" id="alphabetList" data-toggle="tooltip" tabindex="0" data-bind="options: availableAlphabetList,
optionsText: 'name',
optionsValue: 'id',
value: selectedAlphabet
optionsCaption: 'Choose..'">
</select>
You can also go through this question which has good answers for creating a custom binding for autocomplete

Binding radio buttons to array of objects

I have my view model:
function (dataservice, Person){
var genders = ko.observableArray();
var languages = ko.observableArray();
var person = ko.observableArray();
function activate(){
dataservice.getGenders(genders);
dataservice.getGenders(languages);
}
var vm = {
genders: genders,
languages: languages,
person: person
};
}
function Person(person){
var firtstName = person.firtstName;
var familyName = person.familyName;
var genderId = person.genderId;
var languageId = person.languageId;
}
It's simplified for clarity.
Then I have my `Genders' data from server, it looks like this:
[{
$id: "1",
GenderId: 2,
GenderName: "Female",
GenderDescription: "Female",
GenderCode: "F"
}]
I also have Languages that looks like this:
[{
"$id": "1",
"LanguageId": 2,
"LanguageName": "Afar",
"LanguageDescription": "Afar",
"LanguageCode": "aa"
}]
What I am trying to achieve is to bind the genders array from my view model as a data source and use Person.GenderId as the value to be updated, in a way such that the correct radio button is initially selected. This selection depended on Person.GenderId.
I did something similar with Languages using a drop down and that works just fine:
<section data-bind="with: $root.personModel">
<select id="language"
data-bind="options: $root.languages,
optionsText: 'LanguageName',
optionsValue: 'LanguageId',
value: LanguageId,
optionsCaption: 'none'">
</select>
</section>
Now I am trying to do the same thing with radio buttons, but I don't know how to make it work. Here's what I have:
<section data-bind="with: $root.personModel">
<!-- ko foreach: $root.genders -->
<input type="radio"
name="genders"
data-bind="attr: {value: GenderId}, checked: GenderId" />
<span data-bind="text: $data.GenderName"></span>
<!-- /ko -->
</section>
If I understood things correctly, the foreach binding works like with and changes my context, so I can't reach GenderId from my exposed Person.
change
<input type="radio" name="genders" data-bind="attr: {value: GenderId}, checked: GenderId" />
to
<input type="radio" name="genders" data-bind="attr: {value: GenderId}, checked: $parent.GenderId" />
as explained here
$parent: This is the view model object in the parent context, the one immeditely outside the current context. In the root context, this is undefined.
You will need to utilize the checkdValue binding, see the lower part of the "checked" documentation.
Your code didn't quite translate to a repro, but here's my version more or less in your scenario. Suppose this bit of code:
var Person = function(person) {
var self = this;
self.name = ko.observable(person.name);
self.gender = ko.observable(person.gender);
};
var root = {
genders: [{ $id: "1", GenderId: 1, GenderName: "Female"},
{ $id: "2", GenderId: 2, GenderName: "Male"}]
};
root.personModel = new Person({name: 'john', gender: root.genders[1]});
ko.applyBindings(root);
Alongside this markup:
<section data-bind="with: $root.personModel">
<!-- ko foreach: $root.genders -->
<input type="radio"
name="genders"
data-bind="checkedValue: $data, checked: $root.personModel.gender" />
<span data-bind="text: $data.GenderName"></span><br />
<!-- /ko -->
</section>
This should work, see this fiddle.
The objects from the genders array are bound to the checkedValue of each input, and the personModel's gender is bound to checked.

knockout.js selectedOptions is not updated

I'm struggling with knockout.js selectedOptions binding.
I fill multiselect with items from observableArray A, choose some, store result in observableArray B. When item gets removed from array A, the B array is not updated.
Is this knockout issue or am I doing something wrong?
HTML code:
<h4>All items:</h4>
<div data-bind="foreach: items">
<p data-bind="text: name"></p>
<button data-bind="click: $parent.remove">Remove item</button>
</div>
<select multiple="multiple" data-bind="
options: items,
selectedOptions: selectedItems,
optionsText: 'name',
optionsCaption: 'Choose one or more...'
"></select>
<h4>Selected items:</h4>
<div data-bind="foreach: selectedItems">
<p data-bind="text: name"></p>
</div>
Javascript:
var viewModel = {
items: ko.observableArray([
{ name: "Item 1", id: "1" },
{ name: "Item 2", id: "2" },
{ name: "Item 3", id: "3" }
]),
selectedItems: ko.observableArray(),
remove: function(item) {
viewModel.items.remove(item);
}
}
ko.applyBindings(viewModel);
Here's the fiddle: http://jsfiddle.net/3FYAe/
How to reproduce:
select one or more items in the multiselect field, they appear in the list below ("Selected items")
remove one of the selected items
the selectbox is updated
4.
Expected: "Selected items" is updated
Actual: "Selected items" keeps deleted values
Answering my own question:
The trivial solution would be to remove the item from selectedItems array as well, i. e.
remove: function(item) {
viewModel.items.remove(item);
viewModel.selectedItems.remove(item);
}
Updated fiddle: http://jsfiddle.net/3FYAe/1/
However, I would like to find a nicer solution as I'm dealing with many more lists and many more dependencies; this is just a simplified example.

Categories

Resources