If statement and visible binding in knockout not working - javascript

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.

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/

binding knockout observable arrays to multiple <ul> elements

For a navigation menu, I have two groups of links, each group and link showing up or not dependent on a user's role. The roles are looked up when the link structure is being built and the list of links is built accordingly. The returned JSON gets parsed, put into observable arrays with no problem, but when I actually try and apply the bindings, the binding fails because the observables are blank. Here is the HTML...
<ul id="user-menu" class="menu" data-bind="foreach: areas">
<li>
<a data-bind="attr: { href: areaLink }">
<img data-bind="attr: { src: iconUri }" />
<span data-bind="text: areaName"></span>
</a>
</li>
</ul>
<ul id="admin-menu" class="menu" data-bind="foreach: adminAreas">
<li>
<a data-bind="attr: { href: areaLink }">
<img data-bind="attr: { src: iconUri }" />
<span data-bind="text: areaName"></span>
</a>
</li>
</ul>
Knockout view model in the background...
var navigation = (function() {
function Area() {
var self = this;
self.areaName = ko.observable();
self.areaLink = ko.observable();
self.iconUri = ko.observable();
self.sequenceNo = ko.observable();
self.isAdmin = ko.observable();
self.loadFromVM = function (vm) {
self.areaName(vm.name || '');
self.areaLink(vm.link || '');
self.iconUri(vm.iconUri || '');
self.sequenceNo(vm.sequenceNo || '');
self.isAdmin(vm.isAdmin);
}
}
function viewModel() {
var self = this;
self.areas = ko.observableArray([]);
self.adminAreas = ko.observableArray([]);
self.setup = function () {
var data = {}; // population with basic session data
$.getJSON('....php', { JSON.stringify(data) }, function (results) {
for (var i = 0; i < results.length; i++) {
var area = new Area();
area.loadFromVM(results[i]);
if (area.isAdmin()) {
self.adminAreas().push(area);
} else {
self.areas().push(area);
}
}
});
};
}
var vmInstance;
return {
setup: function () {
vmInstance = new viewModel();
vmInstance.setup();
ko.applyBindings(vmInstance, $('#user-menu')[0]);
ko.applyBindings(vmInstance, $('#admin-menu')[0]);
}
};
})();
And then I bring it together with this in the navigation view file...
navigation.setup();
So after I get my JSON back, parse it, and organize it when I loop through the success function of the $.getJSON method, putting a watch on self.areas() and self.adminAreas() does show that the arrays have the exact information I want them to. But by the time they have to be applied, calling vmInstance.areas().length or vmInstance.adminAreas().length returns zero. Even more oddly, putting in an alert with the length of the arrays right after the $.getJSON call but within the setup() function will cause the alert to fire first, show zeroes, then goes through the get, populates the array, then fires zeroes again.
Not exactly sure what's going on here, but I can't remember seeing this kind of behavior in another project so I'm not quite sure what I'm doing wrong here. Any ideas or advice would be greatly appreciated.
EDIT: Nevermind on the Fiddle. It doesn't really capture my actual error.
adminarea object is not initialized.you made the adminArea variable but instead of this you have used same area variable to set values.
var adminArea = new Area();
adminArea.areaName('test admin area');
adminArea.areaLink('#');
adminArea.iconUri('http://evernote.com/media/img/getting_started/skitch/windows8/win8-checkmark_icon.png');
adminArea.sequenceNo(1);
adminArea.isAdmin(true);
Fiddle Demo

Knockout.js - Creating a reusable cascading select

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)

Categories

Resources