Knockout: remove select options from array when already in use - javascript

I have a little knockout form which I am able to duplicate using a button to allow multiple sets of data to be provided.
However, each value for the 'sample rate' select can only be used once. For example, the first form by default is set to 192000. So when I click 'add srate element' the form that is generated should not include 192000 as an option in the Sample rate drop-down.
Obviously if the Sample rate for the first form is set to something else, that value should be removed for the second form, and so on.
How do I remove the already-selected Sample rate from the array when a new form is added, so that it is not available in further copies of the form? It is even possible, given the structure of the view model?
There's a fiddle here: http://jsfiddle.net/3lliot/x3cg131g/
There's a bit of logic included already to prevent more than 6 forms.
Any tips will be appreciated ...
Html code:
<body>
<ul data-bind="foreach: forms">
<li>
<!-- This is a *view* - HTML markup that defines the appearance of your UI -->
<p><span style="color:#AB0002">Sample rate element <span data-bind="text: formNum"></span></span>
</p>
<p>Sample rate (Hz):
<select data-bind="options: $parent.sampleRate, value: selectedSampleRate"></select>
</p>
<p>TDM channels per line:
<select data-bind="options: tdmChans, value: selectedTdmchan"></select>
</p>
</li>
</ul>
<button data-bind="click: addForm">Add <srate> element</button>
<button data-bind="click: removeForm">Remove</button>
<hr/>
<ul data-bind="foreach: forms">
<li>
<!-- render the json -->
<p class="code"><srate id="<span data-bind="text: formNum"></span>">
<br/> <sample_rate><span data-bind="text: selectedSampleRate"></span></sample_rate>
<br/> <tdm_chan><span data-bind="text: selectedTdmchan"></span></tdm_chan>
<br/>
</p>
</li>
</ul>
</body>
JS code:
window.onload = startKnockout;
var formNum;
var i = -1;
function Form() {
var self = this;
// Declare observables
self.selectedSampleRate = ko.observable();
self.selectedTdmchan = ko.observable();
self.formNum = ko.observable();
// Define controls
self.tdmChans = ko.computed(function() {
if (self.selectedSampleRate() == 44100 || self.selectedSampleRate() == 48000) {
return ['2', '4', '8', '16'];
} else if (self.selectedSampleRate() == 88200 || self.selectedSampleRate() == 96000) {
return ['2', '4', '8'];
} else if (self.selectedSampleRate() == 176400 || self.selectedSampleRate() == 192000) {
return ['2', '4'];
} else {
// do nothing
}
}, self);
i++;
self.formNum = i;
}
var Vm = function() {
var self = this;
var item = 0;
self.forms = ko.observableArray([]);
self.forms.push(new Form());
item++;
self.addForm = function() {
if (i < 5) {
self.forms.push(new Form());
item++;
} else {
alert("Can't have more than 6 <srate> elements!")
}
};
self.removeForm = function() {
if (item > 1) {
self.forms.splice(item - 1, 1);
item--;
i--;
} else {
alert("Must have at least one <srate> element!")
}
};
self.sampleRate = ko.observableArray(['192000', '176400', '96000', '88200', '48000', '44100']);
return self;
}
// Activates knockout.js
function startKnockout() {
ko.applyBindings(new Vm());
};

Take a look at this:
http://jsfiddle.net/martinbooth/x3cg131g/1/
importantly, compute the available samples rates based on what has been selected in other forms:
self.sampleRates = ko.computed(function(){
var formsValue = forms(),
availableSampleRates = ko.utils.arrayFilter(allSampleRates, function(sampleRate){
return !ko.utils.arrayFirst(formsValue, function(form){
return form != self && form.selectedSampleRate() === sampleRate;
});
});
return availableSampleRates;
});

Related

Selected Dropdown ids in Knockout JS

I have an array doorsForSitewhere each item will have a Doors Array and each door will have a Schedules array.
It looks like :
var scheduleList = new[]
{
new { ScheduleId = "Schedule1",ScheduleName = "Always On" },
new { ScheduleId = "Schedule2",ScheduleName = "Never On"}
};
var doorsForSite = new[]
{
new { ControllerId ="controller1",ControllerName="Eagle",IsChecked = "false",
Doors = new[]
{
new { DoorId="Door1",DoorName="DoorOne",Schedules = scheduleList},
new { DoorId = "Door2", DoorName = "DoorTwo",Schedules = scheduleList}
}
},
new { ControllerId ="controller2",ControllerName="NetAxis",IsChecked = "false",
Doors = new[]
{
new { DoorId= "Door3",DoorName="DoorThree",Schedules = scheduleList},
new { DoorId = "Door4", DoorName = "DoorFour",Schedules = scheduleList},
new { DoorId = "Door5", DoorName = "DoorFive",Schedules = scheduleList}
}
}
};
Now in UI ..
<ul class="accgrouptableaccordian scroll-x scroll-y">
<!-- ko foreach: $data.AddModel.ControllerDoors -->
<li>
<div class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<span>
<span>
<span class="ispicon ispicon_accordian_droparrow">
</span>
<span class="title" style="line-height:20px;" data-bind="text: ControllerName() + ' - ' + Doors().length + ' Doors'">
</span>
</span>
<span>
</span>
</span>
</h4>
</div>
<div class="panel-collapse">
<div class="panel-body">
<div class="table-responsive panel-body">
<table class="table">
<tbody data-bind="foreach:Doors">
<tr>
<td>
<div>
<span data-bind="text:DoorId"></span>
</div>
</td>
<td class="col-md-4">
<select name="drpSchedulesAccess" class="form-control drpSchedulesAccess" data-bind="options:$data.Schedules,
optionsText: 'ScheduleName',
optionsValue: 'ScheduleId',
value: $data.ScheduleId"></select>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</li>
<!-- /ko -->
</ul>
But in Viewmodel I only want to get the values of the checked Door's selected Schedule in the dropdown.
But that's not happening.
I did
ko.utils.arrayForEach(this.AddModel.ControllerDoors(), function (item) {
ko.utils.arrayForEach(item.Doors(), function (item) {
doorsSelected.push(item);
});
});
var doors = ko.utils.arrayFilter(doorsSelected, function (item) {
return item.IsChecked == true;
});
var doorIds = ko.utils.arrayMap(doors, function (door) {
if (jQuery.inArray(door.DoorId, doorIds) == -1) {
return door.DoorId;
}
});
ko.utils.arrayForEach(doors, function (item) {
debugger;
ko.utils.arrayForEach(item.Schedules, function (item) {
$('.drpSchedulesAccess option:selected').each(function (i) {
schedulesSelected.push($(this).val());
});
});
});
and I checked 3 doors with 3 selected Schedule from dropdown.
But I am getting a schedule array length of 30.
Why is it so ?
you might need to slightly tweek your code & most importantly do everthing is knockout-ish way .
viewModel:
var ViewModel = function() {
var self = this;
self.ControllerDoors = ko.observableArray(ko.mapping.fromJS(doorsForSite)()); // mapping to convert everything to observable
self.check = function() {
var doorsSelected = [];
ko.utils.arrayForEach(self.ControllerDoors(), function(item) {
//you can add condition here based on controllerName IsChecked & reduce looping
ko.utils.arrayForEach(item.Doors(), function(item) {
if (item.IsChecked())
doorsSelected.push(item);
});
});
console.log(doorsSelected);
}
};
catch the working demo here and check console window to find Results .
Things to note :
() : used to read observable (you can find it's usage on current view)
The way you look up the selected options in your view is not "the knockout way". In these lines:
$('.drpSchedulesAccess option:selected').each(function (i) {
schedulesSelected.push($(this).val());
});
you're using the DOM to store your ViewModel state, which isn't bad per se, but not how knockout works.
When using knockout, your ViewModel should contain all data and communicate with the DOM through data-binds.
You haven't shown us a snippet of working code, so I'll try to recreate parts of your UI to show how it should work.
var DoorViewModel = function(schedules, door) {
return Object.assign({
checked: ko.observable(true),
selectedSchedule: ko.observable(),
schedules: schedules
}, door);
};
var ViewModel = function(doors, schedules) {
this.doors = ko.observableArray(doors.map(DoorViewModel.bind(null, schedules)));
this.selectedScheduleIds = ko.computed(function() {
return this.doors()
.filter(function(door) { return door.checked(); })
.map(function(door) {
return door.selectedSchedule()
? door.selectedSchedule().ScheduleId
: null;
});
}, this);
};
var schedules = [{
'ScheduleId': "Schedule1",
'ScheduleName': "Always On"
}, {
'ScheduleId': "Schedule2",
'ScheduleName': "Never On"
}];
var doors = [{
'DoorId': "Door1",
'DoorName': "DoorOne"
}, {
'DoorId': "Door2",
'DoorName': "DoorTwo"
}];
ko.applyBindings(new ViewModel(doors, schedules));
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: doors">
<li>
<input type="checkbox" data-bind="checked: checked">
<span data-bind="text: DoorName"></span>
<select data-bind="options: schedules,
value: selectedSchedule,
optionsText: 'ScheduleName'"></select>
</li>
</ul>
<pre data-bind="text: JSON.stringify(selectedScheduleIds(), null, 2)"></pre>

knockout if binding working in laptops, but not working in mobile and tablet

I am trying some thing with if binding from knockout. If value is true I want to show some text and if it is false, I want to show some different text, as given in the code.
When I am opening the page with this html, I am getting the expected results.
But when I am trying to get the result in phones and in kindle tab (working fine in wondows tab), it is not giving the results for the if binding I have used in html.
I tried removing '()' from failStatus and status in html, but it is not working. Is it any issue of binding or I am doing any thing wrong?
Thanks for any help.
function temp()
{
this.inviteeEmailList = ko.observableArray([]);
var emailList = {};
emailList['email'] = {'a#x.y , b#c.n'};
emailList['status'] = ko.observable();
emailList['failStatus'] = ko.observable();
this.showList = function()
{
for(var k in inviteeEmailList)
{
if(some_condition)
{
this.inviteeEmailList()[k]['status'](true);
this.inviteeEmailList()[k]['failStatus']("");
}
else
{
this.inviteeEmailList()[k]['status'](false);
this.inviteeEmailList()[k]['failStatus']("not exist");
}
}
}
}
<div id="foundEmail" data-bind="foreach : inviteeEmailList">
<span data-bind="if: $data.status()">
<span>Success</span>
</span>
<span data-bind="if:(!$data.status() && $data.failStatus()) ">
<span>hello world</span>
</span>
<div data-bind="text:$data.email"></div>
<div data-bind="if:!$data.status()">
<div data-bind="text:$data.failStatus()"></div>
</div><br/>
</div>
Instead of using if binding, I tried using visible binding, which worked properly for me.
Giving code below
function temp()
{
this.inviteeEmailList = ko.observableArray([]);
var emailList = {};
emailList['email'] = {'a#x.y , b#c.n'};
emailList['status'] = ko.observable();
emailList['failStatus'] = ko.observable();
this.showList = function()
{
for(var k in inviteeEmailList)
{
if(some_condition)
{
this.inviteeEmailList()[k]['status'](true);
this.inviteeEmailList()[k]['failStatus']("");
}
else
{
this.inviteeEmailList()[k]['status'](false);
this.inviteeEmailList()[k]['failStatus']("not exist");
}
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="foundEmail" data-bind="foreach : inviteeEmailList">
<span data-bind="visible: $data.status()">
<span>Success</span>
</span>
<span data-bind="visible:(!$data.status() && $data.failStatus()) ">
<span>hello world</span>
</span>
<div data-bind="text:$data.email"></div>
<div data-bind="visible:!$data.status()">
<div data-bind="text:$data.failStatus()"></div>
</div><br/>
</div>

Knockout js: can't write value from form inside array

I have a script that produces an array of forms, with each form affecting the available options for the next form. The awesome martin booth solved the problem of getting the displayed values to update as new forms are added.
however, I have an observable array (defaultSampleRates) that sits outside the forms array, and for the life of me I can't get the form to push items into that array. I've tried declaring it in a dozen different places in a dozen different ways, but it just won't stick.
basically I need the 'Default sample rate' drop-down to show the sample rates that have been selected in the form above (the user must only be able to choose a default sample rate from a displayed one, rather than from the full list).
Any tips much helpo brain pain. fiddle here: http://jsfiddle.net/3lliot/9vsa4hh7/
html:
<body>
<div style="float:left; width:60%">
<div data-bind="foreach: forms">
<div style="float:left; margin-right:20px"> <span>
<!-- This is a *view* - HTML markup that defines the appearance of your UI -->
<p><span style="color:#AB0002">Sample rate element <span data-bind="text: formNum"></span></span>
</p>
<p>Sample rate (Hz):
<select data-bind="options: sampleRates, value: selectedSampleRate"></select>
</p>
</span>
</div>
</div>
<div style="float:left; clear:both; margin-bottom:20px">
<hr/>
<button data-bind="click: addForm">Add <srate> element</button>
<button data-bind="click: removeForm">Remove</button>
<p>Default sample rate:
<select data-bind="options: defaultSampleRates, value: selectedDefaultSampleRate"></select>
</p>
</div>
</div>
<div style="float:right; width:38%; overflow:scroll; border-left:thin; border-left-style:solid; border-left-color:#dfdfdf;padding-left: 1%"> <span class="code"><audio></span>
<ul data-bind="foreach: forms">
<li>
<!-- render the json --> <span class="code"> <srate id="<span data-bind="text: formNum"></span>">
<br/> <sample_rate><span data-bind="text: selectedSampleRate"></span></sample_rate>
<br/> </srate></span>
</li>
</ul> <span class="code"> <default_srate><span data-bind="text: selectedDefaultSampleRate"></span></default_srate></span>
<br/><span class="code"></audio></span>
</div>
</body>
js:
// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
//window.onload = startKnockout;
window.onload = startKnockout;
var formNum;
var i = -1;
var selectedSampleRates = [];
function Form(allSampleRates, forms) {
var self = this;
// Declare observables
self.selectedSampleRate = ko.observable();
self.formNum = ko.observable();
self.sampleRates = ko.computed(function () {
var formsValue = forms(),
availableSampleRates = ko.utils.arrayFilter(allSampleRates, function (sampleRate) {
return !ko.utils.arrayFirst(formsValue, function (form) {
if (form != self) {
if (form.selectedSampleRate() === sampleRate) {
if (selectedSampleRates.indexOf(sampleRate) === -1) {
selectedSampleRates.push(sampleRate);
}
}
return form.selectedSampleRate() === sampleRate;
} else {
return form != self;
}
});
});
return availableSampleRates;
});
// count how many srate elements there are
i++;
self.formNum = i;
}
var Vm = function () {
var self = this;
var item = 0,
allSampleRates = ['192000', '176400', '96000', '88200', '48000', '44100'];
// declare observables for options outside the srate elements
self.selectedDefaultSampleRate = ko.observable();
// add remove forms stuff
self.forms = ko.observableArray([]);
self.forms.push(new Form(allSampleRates, self.forms));
item++;
self.addForm = function () {
if (i < 5) {
self.forms.push(new Form(allSampleRates, self.forms));
item++;
} else {
alert("Can't have more than 6 <srate> elements!")
}
};
self.removeForm = function () {
if (item > 1) {
self.forms.splice(item - 1, 1);
item--;
i--;
} else {
alert("Must have at least one <srate> element!")
}
};
// define arrays for options outside srate elements
self.defaultSampleRates = ko.observableArray([]);
return self;
}
// Activates knockout.js
function startKnockout() {
ko.applyBindings(new Vm());
};
You can make use of selectedOptions binding to add defaultSample rate.
I changed select sampleRates code to this
<select data-bind="options: sampleRates, value: selectedSampleRate, selectedOptions: $root.defaultSampleRates"></select>
Notice selectedOptions binding there..
Should work as per your need.
Updated Fiddle Demo here : http://jsfiddle.net/rahulrulez/9vsa4hh7/3/
I hope that's what you wanted.

A pattern to be designed using template binding of knockoutjs

We have a situation as mentioned below:
There is a set of data for a search panel, it's called in several pages with different types of components and placement of it. There can be combo boxes, radio buttons, input boxes and buttons.
Knockout has a feature of template binding in which we can have the flexibility to show numerous panels on condition using a template in the html mapped to MOdel.
Below is the code and pattern:
HTML:
<div id="content-wrapper">
<div class="spacer"></div>
<div>
<table class="data-table">
<thead>
<tr>
<th colspan="4"> Search </th>
</tr>
</thead>
<tbody data-bind="foreach: preSearchData" >
<tr>
<!-- ko template: { name: 'label_' + templateName()} -->
<!-- /ko -->
</tr>
</tbody>
</table>
</div>
</div>
<script type="text/html" id="label_Combo">
<td>It is a Combo </td>
</script>
<script type="text/html" id="label_Number">
<td>
It is a Number
</td>
</script>
MODEL:
Models.Components = function(data) {
var self = this;
self.number = data.number;
self.labelCd = data.labelCd;
self.xmlTag = data.xmlTag;
self.Type = new Cobalt.Models.Type(data.Type);
};
Models.Type = function(data) {
var self = this;
self.component = data.component;
self.records = data.records;
self.minLength = data.minLength;
self.maxLength = data.maxLength;
self.defaultValue = data.defaultValue;
self.targetAction = data.targetAction;
};
Models.ComponentType = function (paymentTypeCode, data, actionId) {
var ret;
self.templateName(data.component);
if (!data || (actionId === Cobalt.Constant.Dashboard.copyProfile))
data = {};
if (paymentTypeCode == Cobalt.Constant.Dashboard.creditCard)
ret = new Cobalt.Models.CreditCardPaymentType(data.cardHolderName, data.cardNumber, data.cardExpireDate);
else if (paymentTypeCode == Cobalt.Constant.Dashboard.dd)
ret = new Cobalt.Models.DDPaymentType(data.pinNumber);
else if (Cobalt.Utilities.startsWith(paymentTypeCode, Cobalt.Constant.Dashboard.yahooWallet)) {
if (!data && paymentTypeCode.indexOf('~') > -1) {
data.payCode = paymentTypeCode.substr(paymentTypeCode.indexOf('~') + 1, paymentTypeCode.lastIndexOf('~'));
data.billingAgentId = paymentTypeCode.substr(paymentTypeCode.lastIndexOf('~') + 1);
}
ret = new Cobalt.Models.WalletPaymentType(data.payCode, data.billingAgentId);
}
else if (paymentTypeCode == Cobalt.Constant.Dashboard.ajl) {
ret = new Cobalt.Models.DDPaymentType(data.pinNumber);
}
else
ret = data || {};
return ret;
};
Models.POCModel = function () {
var self = this;
self.templateName = ko.observable();
self.preSearchData = ko.observableArray([]);
self.getResultData = function () {
var data = Cobalt.Data.getResultData();
var componentList = data.componentList;
self.preSearchData(componentList);
};
};
Above code gives me a error saying:
Ajax error: parsererror ( Error: Unable to parse bindings. Message: ReferenceError: templateName is not defined; Bindings value: template:
{ name: 'label_' + templateName()} ) cobalt.init.js:66
This is not a direct answer to your question, but it shows an alternate way of doing this using the ViewModel type to find the view (Template)
http://jsfiddle.net/nmLsL/2
Each type of editor is a ViewModel
MyApp.Editors.BoolViewModel = function(data) {
this.checked = data;
};
MyApp.Editors.BoolViewModel.can = function(data) {
return typeof data === "boolean";
};
And it has a can function that determins if it can edit the value
I then usea library called Knockout.BindingConventions to find the template connected to the ViewModel
https://github.com/AndersMalmgren/Knockout.BindingConventions/wiki/Template-convention
Your foreach binding creates a child binding context, which doesn't include templateName since that's part of the parent. Replace it with
<!-- ko template: { name: 'label_' + $parent.templateName()} -->

knockout binding after array updated

I have an array that I'm removing items from but I'm keeping count of the number of items to do UI formatting. I need to be able to have the bind update.
ko.applyBindings(viewModel);
getFoos();
var viewModel = {
foos: ko.observableArray([]),
reloadFoos: function () {
getFoos();
},
removeFoo: function () {
remove(this);
}
};
var foo = function () {
this.Id = ko.observable();
this.Name = ko.observable();
this.Count = ko.observable();
};
function remove(foo) {
viewModel.foos.splice(viewModel.foos.indexOf(foo), 1);
viewModel.foos.each(function(index) {
viewModel.foos[index].Count = index%10 == 0;
});
}
function getFoos() {
viewModel.foos([]);
$.get("/myroute/", "", function (data) {
for (var i = 0; i < data.length; i++) {
var f = new foo();
f.Id = data[i];
f.Name = data[i];
f.Count = i%10 == 0;
viewModel.foos.push(f);
}
});
}
<div data-bind="foreach: foos">
<div style="float: left">
<a href="javascript:void(0);" data-bind="click : $parent.removeFoo, attr: { id: Id }">
<label data-bind="value: Name"></label>
</a>
</div>
<!-- ko if: Count -->
<div style="clear: left"></div>
<!-- /ko -->
</div>
When the click event fires the item is removed from the array but the if bind doesn't get updated and the ui formatting is off. I'm trying to keep from reloading the data because the ui block bounces as it removes and reloads.
Your UI is not being updated because when you do your assignment to Count, you aren't assigning as an observable. You are replacing the observable with a straight boolean value. So, your assignment calls like this one:
viewModel.foos[index].Count = index%10 == 0;
Will cause viewModel.foos[index].Count to be equal to true or false and the value won't be stored in the observable.
That line should be this instead:
viewModel.foos[index].Count(index%10 == 0);
That will set the observable correctly. Note that you must change all of your assignments to observables to be set this way. See the "Reading and Writing Observables" section of this page: Knockout Observables.

Categories

Resources