Knockout.js - Creating a reusable cascading select - javascript

Is it possible to create a reusable cascading combo box from a dictionary object using KO ?
For example, this data
{'A' : { 'A1':11, 'A2':12} ,
'B' : { 'B1':21, 'B2':22, 'B3':33},
'C' : { 'C1':31}}
would produce two cascading boxes, the first with the options 'A,B,C'. the second would update according to the selection. The dict might change in height but the tree will always be balanced.
Is it possible to create the elements from within a custom binding ? can a custom binding contain other custom bindings and subscribe to them ? is custom binding even the right approach here ?
I would appreciate some guidance.

Basically this is what I have done
Create a predefined structure /class which knows if it has list of
values or just a single value.
On the view side show the dropdown if its list else just show the text.
On the root vm nest the structure created in step one and create the dict.
Here is the VM
var optionVM = function (name,isList, v) {
var self = this;
self.name=ko.observable(name);
if (isList) self.values = ko.observableArray(v);
else self.value = ko.observable(v);
self.isList = ko.observable(isList);
self.selected = ko.observable();
}
var vm = function () {
var self = this;
var a1Vm = new optionVM('A1',true, [new optionVM('A11',false,111), new optionVM('A12',false,122)]);
var aVm = new optionVM('A',true, [new optionVM('A2',false,'21'), a1Vm]);
var d = new optionVM('Root',true, [aVm, new optionVM('B',false,'B1'),new optionVM('C',false,'C1')]);
self.dict = ko.observable(d);
}
ko.applyBindings(new vm());
Here is the view
<select data-bind='options:dict().values,optionsText:"name",value:dict().selected'>
</select>
<div data-bind="template: {name: 'template-detail', data: dict().selected}"></div>
<script type="text/html" id='template-detail'>
<!-- ko if:$data.isList -->
<span> List:</span>
<select data-bind='options:values,optionsText:"name",value:selected'>
</select>
<div data-bind="template: {name: 'template-detail', data: selected}"></div>
<!-- /ko -->
<!-- ko ifnot:$data.isList -->
Value:<span data-bind="text:value"></span>
<!-- /ko -->
</script>
And here is the jsFiddle
Improvements:
You can use isArray to identify if its list in the optionVM.
Some of the observables can be replaced with simple values if they are not going to change (e.g:name)

Related

KoJs: bind a dynamic number of text boxes to elements of an array

I have a front-end which allows for adding and removing of text boxes suing the foreach binding. A text box looks something like this
<div id="dynamic-filters" data-bind="foreach: filterList">
<p>
<input type="text" data-bind="textInput: $parent.values[$index()], autoComplete: { options: $parent.options}, attr: { id : 'nameInput_' + $index() }"/>
</p>
</div>
What I want to do, as shown in the code above is to bind each of these dynamically generated text boxes to an element in the array using the $index() context provided by knockout.js
However it doesn't work for me, my self.values=ko.observableArray([]) doesn't change when the text boxes change.
My question is, if I want to have a way to bind these dynamically generated text boxes, is this the right way to do it? If it is how do I fix it? If it's not, what should I do instead?
Thanks guys!
EDIT 1
the values array is an observable so I thought I should unwrap it before use. I changed the code to
<input type="text" data-bind="textInput: $parent.values()[$index()], autoComplete: { options: $parent.options}, attr: { id : 'nameInput_' + $index() }"/>
This works in a limited way. When I add or change the content of text boxes, the array changes accordingly. However when I delete an element it fails in two ways:
If I delete the last item, the array simply doesn't change
If I delete an item in between, everything is shifted back
I suppose I have to add a function that changes the text-input value before destroying the text box itself.
Any help or advice on how to do this?
I would suggest taking the array of values and mapping it to some kind of model first, then dumping it into the filterList ko.observableArray. It can be as complex or as simple as need be.
That way you have direct access to those properties at the ko foreach: level instead of having to do the goofy index access.
I've added a simple knockout component example as well to show you what can be achieved.
var PageModel = function() {
var self = this;
var someArrayOfValues = [{label: 'label-1', value: 1},{label: 'label-2', value: 2},{label: 'label-3', value: 3},{label: 'label-4', value: 4}];
this.SimpleInputs = ko.observableArray(_.map(someArrayOfValues, function(data){
return new SimpleInputModel(data);
}));
this.AddSimpleInput = function(){
self.SimpleInputs.push(new SimpleInputModel({value:'new val', label:'new label'}));
};
this.RemoveSimpleInput = function(obj){
self.SimpleInputs.remove(obj);
}
}
var SimpleInputModel = function(r) {
this.Value = ko.observable(r.value);
this.Label = r.label;
};
var SimpleInputComponent = function(params){
this.Id = makeid();
this.Label = params.label;
this.Value = params.value;
function makeid() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 5; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
}
ko.components.register('input-component', {
viewModel: SimpleInputComponent,
template: '<label data-bind="text: Label, attr: {for: Id}"></label><input type="text" data-bind="textInput: Value, attr: {id: Id}" />'
})
window.model = new PageModel();
ko.applyBindings(model);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<!-- ko if: SimpleInputs -->
<h3>Simple Inputs</h3>
<!-- ko foreach: SimpleInputs -->
<input-component params="value: Value, label: Label"></input-component>
<button data-bind="click: $parent.RemoveSimpleInput">X</button>
<br>
<!-- /ko -->
<!-- /ko -->
<button data-bind="click: AddSimpleInput">Add Input</button>
EDIT (7/16/2020):
Mind explaining this without requiring lodash? I literally googled "how to lodash map using plain javascript". Excellent answer otherwise! – CarComp
In this scenario the lodash _.map method could be overkill unless you are executing the script in an environment that does not have native support for the vanilla array map method. If you have support for the vanilla method, go ahead and use that. The map method essentially iterates over each array using the method it is handed to return a transformed array of the original items. Implementation of vanilla code would look like so.
this.SimpleInputs = ko.observableArray(someArrayOfValues.map(function(data) {
return new SimpleInputModel(data);
}));
Here we are taking the values of someArrayOfValues and telling it to use each item to build a new SimpleInputModel and return it using that item data. [SimpleInputModel, SimpleInputModel, SimpleInputModel, SimpleInputModel] is what the new array turns into after mapping. Each of these items has all the functionality described in the SimpleInputModel class, Value and Label.
So with the new array you could, if you wanted, access the values like this as well self.SimpleInputs[0].Value() or self.SimpleInputs[0].Label
Hope that helps to clarify.

binding multi dropdown list in knockout.js

I have a multi dropdown list and I need to do the following:
1. Make sure that when selecting value in one dropdown list it won't appear in the others (couldn't find a proper solution here).
2. When selecting the value "Text" a text field (<input>) will apear instead of the Yes/no dropdown.
3. "Choose option" will appear only for the first row (still working on it).
4. Make sure that if "Text" is selected, it always will be on the top (still working on it).
JSFiddle
HTML:
<div class='liveExample'>
<table width='100%'>
<tbody data-bind='foreach: lines'>
<tr>
<td>
Choose option:
</td>
<td>
<select data-bind='options: filters, optionsText: "name", value: filterValue'> </select>
</td>
<td data-bind="with: filterValue">
<select data-bind='options: filterValues, optionsText: "name", value: "name"'> </select>
</td>
<td>
<button href='#' data-bind='click: $parent.removeFilter'>Remove</button>
</td>
</tr>
</tbody>
</table>
<button data-bind='click: addFilter'>Add Choice</button>
JAVASCRIPT:
var CartLine = function() {
var self = this;
self.filter = ko.observable();
self.filterValue = ko.observable();
// Whenever the filter changes, reset the value selection
self.filter.subscribe(function() {
self.filterValue(undefined);
});
};
var Cart = function() {
// Stores an array of filters
var self = this;
self.lines = ko.observableArray([new CartLine()]); // Put one line in by default
// Operations
self.addFilter = function() { self.lines.push(new CartLine()) };
self.removeFilter = function(line) { self.lines.remove(line) };
};
ko.applyBindings(new Cart());
I will appeaciate your assist here! Mainly for the first problem.
Thanks!
Mike
If you want to limit the options based on the options that are already selected in the UI, you'll need to make sure every cartLine gets its own array of filters. Let's pass it in the constructor like so:
var CartLine = function(availableFilters) {
var self = this;
self.availableFilters = availableFilters;
// Other code
// ...
};
You'll have to use this new viewmodel property instead of your global filters array:
<td>
<select data-bind='options: availableFilters,
optionsText: "name",
value: filterValue'> </select>
</td>
Now, we'll have to find out which filters are still available when creating a new cartLine instance. Cart manages all the lines, and has an addFilter function.
self.addFilter = function() {
var availableFilters = filters.filter(function(filter) {
return !self.lines().some(function(cartLine) {
var currentFilterValue = cartLine.filterValue();
return currentFilterValue &&
currentFilterValue.name === filter.name;
});
});
self.lines.push(new CartLine(availableFilters))
};
The new CartLine instance gets only the filter that aren't yet used in any other line. (Note: if you want to use Array.prototype.some in older browsers, you might need a polyfill)
The only thing that remains is more of an UX decision than a "coding decision": do you want users to be able to change previous "Choices" after having added a new one? If this is the case, you'll need to create computed availableFilters arrays rather than ordinary ones.
Here's a forked fiddle that contains the code I posted above: http://jsfiddle.net/ztwcqL69/ Note that you can create doubled choices, because choices remain editable after adding new ones. If you comment what the desired behavior would be, I can help you figure out how to do so. This might require some more drastic changes... The solution I provided is more of a pointer in the right direction.
Edit: I felt bad for not offering a final solution, so here's another approach:
If you want to update the availableFilters retrospectively, you can do so like this:
CartLines get a reference to their siblings (the other cart lines) and create a subscription to any changes via a ko.computed that uses siblings and their filterValue:
var CartLine = function(siblings) {
var self = this;
self.availableFilters = ko.computed(function() {
return filters.filter(function(filter) {
return !siblings()
.filter(function(cartLine) { return cartLine !== self })
.some(function(cartLine) {
var currentFilterValue = cartLine.filterValue();
return currentFilterValue &&
currentFilterValue.name === filter.name;
});
});
});
// Other code...
};
Create new cart lines like so: self.lines.push(new CartLine(self.lines)). Initiate with an empty array and push the first CartLine afterwards by using addFilter.
Concerning point 2: You can create a computed observable that sorts based on filterValue:
self.sortedLines = ko.computed(function() {
return self.lines().sort(function(lineA, lineB) {
if (lineA.filterValue() && lineA.filterValue().name === "Text") return -1;
if (lineB.filterValue() && lineB.filterValue().name === "Text") return 1;
return 0;
});
});
Point 3: Move it outside the foreach.
Point 4: Use an if binding:
<td data-bind="with: filterValue">
<!-- ko if: name === "Text" -->
<input type="text">
<!-- /ko -->
<!-- ko ifnot: name === "Text" -->
<select data-bind='options: filterValues, optionsText: "name", value: "name"'> </select>
<!-- /ko -->
<td>
Updated fiddle that contains this code: http://jsfiddle.net/z22m1798/

Knockout: Best way to bind visibility to both item and a parent property?

I am creating an edit screen where I want people to delete items from a list. The list is displayed normally, until the "controller" object goes into edit mode. Then the user can delete items. Items should be flagged for deletion and displayed as such, then when the user saves the edit, they are deleted and the server notified.
I actually have this all working, but the only way I could do it was using literal conditions in the bindings, which looks ugly and I don't really like. Is there a better way of doing it?
Working Fiddle: http://jsfiddle.net/L1e7zwyv/
Markup:
<div id="test">
<a data-bind="visible: IsViewMode, click: edit">Edit</a>
<a data-bind="visible: IsEditMode, click: cancel">Cancel</a>
<hr/>
<ul data-bind="foreach: Items">
<li data-bind="css: CssClass">
<span data-bind="visible: $parent.IsViewMode() || $data._Deleting(), text: Value"></span>
<!-- ko if: $parent.IsEditMode() && !$data._Deleting() -->
<input type="text" data-bind="value: Value"/>
<a data-bind="click: $parent.deleteItem">Del</a>
<!-- /ko -->
</li>
</ul>
</div>
Code:
function ItemModel(val)
{
var _this = this;
this.Value = ko.observable(val);
this._Deleting = ko.observable();
this.CssClass = ko.computed(
function()
{
return _this._Deleting() ? 'deleting' : '';
}
);
}
function ManagerModel()
{
var _this = this;
this.Items = ko.observableArray([
new ItemModel('Hell'),
new ItemModel('Broke'),
new ItemModel('Luce')
]);
this.IsEditMode = ko.observable();
this.IsViewMode = ko.computed(function() { return !_this.IsEditMode(); });
this.edit = function(model, e)
{
this.IsEditMode(true);
};
this.cancel = function(model, e)
{
for(var i = 0; i < _this.Items().length; i++)
_this.Items()[i]._Deleting(false);
this.IsEditMode(false);
};
this.deleteItem = function(model, e)
{
model._Deleting(true);
};
}
ko.applyBindings(new ManagerModel(), document.getElementById('test'));
you could:
wrap another span around to separate the bindings but this would be less efficient.
use both a visible: and if: binding on the same element to achieve the same functionality,
write a function on the itemModel isVisible() accepting the parent as an argument making your binding visible: $data.isVisible($parent).
Afterthought: If this comes up in multiple places you could write a helper function to combine visibility bindings
// reprisent variables from models
var v1 = false;
var v2 = false;
var v3 = false;
// Helper functions defined in main script body - globally accessible
function VisibilityFromAny() {
var result = false;
for(var i = 0; i < arguments.length; i++ ) result |= arguments[i];
return Boolean(result);
}
function VisibilityFromAll() {
var result = true;
for(var i = 0; i < arguments.length; i++ ) result &= arguments[i];
return Boolean(result);
}
// represent bindings
alert(VisibilityFromAny(v1, v2, v3));
alert(VisibilityFromAll(v1, v2, v3));
The third option is the most popular technique with MVVM aficionados like yourself for combining variables in a single binding from what I've seen, it makes sense and keeps all the logic away from the view markup in the view models.
Personally I like the syntax you have at present, (even though I count myself amongst the MVVM aficionado gang as well) this clearly shows in the view markup that the visibility of that element is bound to 2 items rather then hiding these details in a function.
I try to think of view models as a model for my view, not just a place where logic resides. When possible I also try to move complex logic back the view model and use descriptive names for my variables so the code is more readable.
I would suggest adding this to your view model -
var isViewable = ko.computed(function () { return IsViewMode() || _Deleting(); });
var isEditable = ko.computed(function() { return IsEditMode() && !_Deleting(); });
And in your view -
<li data-bind="css: CssClass">
<span data-bind="visible: isViewable, text: Value"></span>
<!-- ko if: isEditable -->
<input type="text" data-bind="value: Value"/>
<a data-bind="click: $parent.deleteItem">Del</a>
<!-- /ko -->
</li>
This cleans the bindings up and allows you to more easily adjust the logic without having to do many sanity checks in your view and view model both. Also I personally name variables that return a boolean such as this as isWhatever to help be more descriptive.
The benefit is that as your view and view model grow larger you can keep the DOM clean of clutter and also your view model becomes testable.
Here is a 'code complete' version of your fiddle with this added -
http://jsfiddle.net/L1e7zwyv/3/

Dynamically creating kendo-grid columns in angular controller

I am trying to dynamically build the structure of a kendo-angular grid. My problem is that the grid options are not known when the k-options attribute is evaluated, so the grid is binding to ALL of the columns on the datasource.
Here is the HTML:
<div kendo-grid k-options="{{gridModel.options}}"
k-data-source="gridModel.myDataSource">
</div>
And here is the javascript in the controller:
// this is called after the api call has successfully returned with data
function getSucceeded(){
...
$scope.gridModel.options = function(){
// function that properly builds options object with columns, etc.
}
// this is just shown for example... the data is properly loading
$scope.gridModel.myDataSource.data(ds.data());
}
The data is properly loading, but because gridModel.options was evaluated in the HTML prior to being set by the success method, it is essentially ignored and all of the columns from the datasource are being rendered.
This works like a champ when gridModel.options is static.
How can I defer the evaluation of k-options and/or force a reevaluation after they've been set by the controller?
I was able to figure it out. I had to do four things:
Update my version of angularjs (I was on 1.08 which does not have the ng-if directive). I updated to 1.2.0rc3.
Wrap my kendo-grid div in an ng-if div
Invoke my function! I was just setting $scope.gridModel.options to a function - I needed to actually invoke the function so I'd be setting the variable to the value returned from the function.
I had to update my angular.module declaration to include ngRoute (based on it being separated into it's own module in 1.2.x).
Here's the updated HTML:
<div data-ng-if="contentAvailable">
<div kendo-grid k-options="{{gridModel.options}}"
k-data-source="gridModel.myDataSource">
</div>
</div>
And here's the updated controller (not shown: I set $scope.contentAvailable=false; at the beginning of the controller):
// this is called after the api call has successfully returned with data
function getSucceeded(){
...
$scope.gridModel.options = function(){
// function that dynamically builds options object with columns, etc.
}(); // <----- NEED to invoke function!!
// this is just shown for example... the data is properly loading
$scope.gridModel.myDataSource.data(ds.data());
$scope.contentAvailable=true; // trigger the ng-if
}
I actually moved the function into a config file so I'm not polluting the controller with too much configuration code. Very happy to have figured this out.
Here is a sample using 'Controller As' syntax, dynamic columns and paging.
var app = angular.module("app", ["kendo.directives"]);
function MyCtrl() {
var colsList = [{
name: "col1"
}, {
name: "col2"
}, {
name: "col3"
}, {
name: "col4"
}];
var gridCols = [];
var iteration = 1;
var vm = this;
vm.gridOptions = {
columns: gridCols,
dataSource: new kendo.data.DataSource({
pageSize: 10
}),
pageable: true
};
vm.buildGrid = function() {
var data = {};
vm.gridOptions.columns = [];
for (var x = 0; x < colsList.length; x++) {
if (iteration % 2 === 0 && x === colsList.length - 1) continue;
var col = {};
col.field = colsList[x].name;
col.title = colsList[x].name;
data[col.field] = "it " + iteration + " " + (1111 * (x + 1));
vm.gridOptions.columns.push(col);
}
// add one row to the table
vm.gridOptions.dataSource.add(data);
iteration++;
};
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<link rel="stylesheet" href="http://cdn.kendostatic.com/2015.1.318/styles/kendo.common.min.css" />
<link rel="stylesheet" href="http://cdn.kendostatic.com/2015.1.318/styles/kendo.default.min.css" />
<script src="http://cdn.kendostatic.com/2015.1.318/js/kendo.all.min.js"></script>
<body ng-app="app">
<div ng-controller="MyCtrl as vm">
<button ng-click="vm.buildGrid()">Build Grid</button>
<div kendo-grid="grid" k-options="vm.gridOptions" k-rebind="vm.gridOptions"></div>
</div>
</body>
We can use the k-rebind directive for this. From the docs:
Widget Update upon Option Changes
You can update a widget from controller. Use the special k-rebind attribute to create a widget which automatically updates when some scope variable changes. This option will destroy the original widget and will recreate it using the changed options.
Apart from setting the array of columns in the GridOptions as we normally do, we have to hold a reference to it:
vm.gridOptions = { ... };
vm.gridColumns = [{...}, ... ,{...}];
vm.gridOptions.columns = vm.gridColumns;
and then pass that variable to the k-rebind directive:
<div kendo-grid="vm.grid" options="vm.gridOptions" k-rebind="vm.gridColumns">
</div>
And that's it when you are binding the grid to remote data (OData in my case). Now you can add or remove elements to/from the array of columns. The grid is going to query for the data again after it is recreated.
When binding the Grid to local data (local array of objects), we have to somehow postpone the binding of the data until the widget is recreated. What worked for me (maybe there is a cleaner solution to this) is to use the $timeout service:
vm.gridColumns.push({ ... });
vm.$timeout(function () {
vm.gridOptions.dataSource.data(vm.myArrayOfObjects);
}, 0);
This has been tested using AngularJS v1.5.0 and Kendo UI v2016.1.226.

If statement and visible binding in knockout not working

I am trying to display a div depending on the count of the users for the div. If the div contains more than x number of users then display the div above the users if not then don't show. I am displaying the users through a foreach loop.
View:
<div class="collapse in" data-bind="template: { name: 'list', foreach: $data.Users }">
</div>
<script type="text/html" id="list">
<!-- ko if: ShowLetter -->
<div id="letter" data-bind=" text: Letter"></div>
<!-- /ko -->
</script>
I also tried this in my view:
<div id="letter" data-bind="visible:ShowLetter, text: Letter"></div>
But when i render the page either i get no letters or the letters would show up for a group of users that are less than x number. My results show three groups 1st group only has 1 user and shouldn't show letter - 2nd group has 2 users which shouldn't show letter either and the 3rd group has 30 users and should show letter.
Javascript:
var userViewModel = function (data) {
var _self = this;
_self.Name = ko.observable(data.Name);
_self.Letter = ko.observable(data.Letter);
_self.ShowLetter = ko.computed(function () {
return (roleViewModel.UserCount > properties.RoleUser);
});
};
var typeViewModel = function (data) {
var _self = this;
_self.ContentType = ko.observable(data.ContentType);
_self.Name = ko.observable(data.Name);
_self.Rank = ko.observable(data.Rank);
_self.UserCount = ko.observable(data.UserCount);
_self.Users = ko.observableArray([]);
};
How can i get my view to function properly for each group looping in the foreach?
Since the property ShowLetter is a computed observable, you must invoke it, otherwise you are just passing the prototype...
eg.
would be
or data-bind="visible:ShowLetter, text: Letter"
would be data-bind="visible:ShowLetter(), text: Letter"
That should take care of it.
In your computed, you want to call roleViewModel.UserCount() (Not sure about RoleUser) as it's an observable. Otherwise everything look fine, although some code is missing.

Categories

Resources