knockout if bindings with boolean not evaluating correctly - javascript

I have the following in my view, using a span to show a readonly view and an input to show an edit view.
<span data-bind="{ ifnot:IsEditing, text:SystemName }"></span>
<input type="text" id="SystemName" data-bind="{ if:IsEditing, value:SystemName }" />
The IsEditing observable is evaluating to false. I'm returning JSON that has the following hierarchy.
Project
.
.
Systems (collection)
SystemName
I'm loading the value in via JQuery, creating the observable model using the following:
$.ajax({
type: "get",
url: "..",
success: function (d) {
var pList = [];
for (var p = 0, plen = d.Data.length; p < plen; p++) {
var proj = d.Data[p];
var systems = proj.Systems;
var sList = [];
proj = ko.mapping.fromJS(proj);
for (var s = 0, slen = systems.length; s < slen; s++) {
sList.push(ko.mapping.fromJS(systems[s]));
}
proj.Systems = ko.observableArray(sList);
pList.push(proj);
}
window["model"].projects(pList);
},
error: function (e) {
debugger;
alert("An error occurred");
}
});
On load, whenever the model is loaded and this expression is evaluated, both elements always show up, instead of showing the span and hiding the input. When I change this to the visible binding, only the span shows. Why is the input showing with the if binding, when IsEditing is evaluating to false?

If your IsEditing an observable the if binding should evaluate it correctly.
However the if binding should be applied on "container like" elements e.g. on elements which can have child elements.
The documentation a little bit confusing and talks about contained markup:
The if binding, however, physically adds or removes the contained markup in your DOM,
Because the input cannot have child elements the if cannot remove anything from the input. You need to wrap your input into a div and apply there the if or you can use the container-less binding syntax:
<!-- ko if: IsEditing -->
<input type="text" id="SystemName" data-bind="{ value: SystemName }" />
<!-- /ko -->
Demo JSFiddle.

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.

Give every angular material checkbox and ID

I am currently having a problem:
I need to create md-checkboxes from a Database. This part works fine with ng-repeat. But I am having a problem reading those checkboxes.
Every entry in the Database has its own unique ID (I am using RethinkDB) so I thought I just can apply this as an ID.
md-card(ng-repeat="n in ideas")
md-card-content
span
md-checkbox(type="checkbox" id='n.id') {{n.idea}}
I am working with Jade / Pug as View Engine.
But how am I now able to read out all checkboxes at once?
I tried many methods like looping through all ElementsByTagName("md-checkbox") and than with a for to read the checked value but it always returns undefined.
const checkboxes = document.getElementsByTagName("md-checkbox");
console.log(checkboxes) //works fine, prints all checkboxes
for (let i = 0; i < checkboxes.length; i++) {
console.log(checkboxes[i].checked); //returns undefined
}
Do you have any Ideas how to read all Boxes at once?
Edit #1 - More Code
index.js
angular.module('votes', ['ngMaterial'])
.controller("VoteMainController", function ($scope) {
$scope.safeApply = function (fn) {
var phase = this.$root.$$phase;
if (phase == '$apply' || phase == '$digest') {
if (fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
register = [];
//var to store database content and add it to page
$scope.ideas;
//Downloading Database stuff as JSON
$.ajax({
type: "GET",
url: "./api",
async: true,
success: function (content) {
for (let i = 0; i < content.length; i++) {
register.push({
[content[i].id]: {
checked: false
}
})
}
$scope.ideas = content;
$scope.safeApply();
},
});
function checkChecked() {
const checkboxes = document.getElementsByTagName("md-checkbox");
for (let i = 0; i < checkboxes.length; i++) {
console.log(checkboxes[i].checked);
}
}
})
index.jade
form(id="login" method="post")
md-card(ng-repeat="n in ideas")
md-card-content
span
md-checkbox(type="checkbox" id='n.id') {{n.idea}}
md-input-container
label Andere Idee?
md-icon search
input(name='idea', ng-model='idea', id='idea', type='text')
div(layout="column")
md-button(type='submit', class="md-raised md-primary unselectable")
| Senden!
Your question title asks about assigning ids but Your question, which you wrote at the very end is
"Do you have any Ideas how to read all Boxes at once?"
So if you wanna do something with multiple or all checkbox elements you should assign them some css class, say "idea-checkbox".
Then in css styles or in jQuery you can access all those checkboxes at once by using the dotted syntax of ".idea-checkbox".
css ids are used to distinctively access any one html element and classes are used to get access to all the elements at once which has that class.
You can also try to the use angular's "track by n.id" syntax if you want to track them by their id.
You should be able to do this with ng-list and ng-model:
HTML
<div ng-app="MyApp" ng-controller="MyAppController">
<ng-list>
<ng-list-item ng-repeat="n in ideas">
<p>{{n.id}}: <input type="checkbox" ng-model="n.isChecked"></p>
</ng-list-item>
</ng-list>
<p>Current state of model: {{ ideas }}</p>
</div>
<p>Current state of model: {{ ideas }}</p>
Angular
angular
.module("MyApp", ['ngMessages'])
.controller('MyAppController', AppController);
function AppController($scope) {
$scope.ideas = [{
id: 193872768,
isChecked: false
},{
id: 238923113,
isChecked: false
}];
}
Demo
This also works with the material design counterparts, md-list and md-checkbox.

How to filter through a table using ng-repeat checkboxes with Angularjs

Once upon a time this was working but somehow it's broken. I want to be able to produce checkboxes using ng-repeat to get as many checkboxes as required based on stored data and use these to filter through a table produced.
Additionally I don't want identical values for the checkboxes to be repeated.
I have made a plnkr with the code.
<div class="row">
<label data-ng-repeat="x in projects">
<input
type="checkbox"
data-ng-true-value="{{x.b}}"
data-ng-false-value=''
ng-model="quer[queryBy]" />
{{x.b}}
</label>
</div>
http://plnkr.co/edit/RBjSNweUskAtLUH3Ss6r?p=preview
So in summary.
Checkboxes to filter Ref.
Checkboxes to be unique.
Checkboxes to be made based off ng-repeat using Ref.
Okay, here's how to do it.
First, let's add a couple of lines of CSS in your to make sure all the checkboxes are visible:
<style>
.row { margin-left: 0px }
input[type=checkbox] { margin-left: 30px; }
</style>
Next, add the following lines to your controller:
app.filter('unique', function() {
return function (arr, field) {
var o = {}, i, l = arr.length, r = [];
for(i=0; i<l;i+=1) {
o[arr[i][field]] = arr[i];
}
for(i in o) {
r.push(o[i]);
}
return r;
};
})
app.controller("maincontroller",function($scope){
$scope.query = {};
$scope.quer = {};
$scope.queryBy = '$';
$scope.isCollapsed = true;
$scope.selectedRefs = [];
$scope.myFilter = function (item) {
var idx = $scope.selectedRefs.indexOf(item.b);
return idx != -1;
};
$scope.toggleSelection = function toggleSelection(id) {
var idx = $scope.selectedRefs.indexOf(id);
if (idx > -1) {
$scope.selectedRefs.splice(idx, 1);
}
else {
$scope.selectedRefs.push(id);
}
};
Phew.
For some reason, your Plunkr's version of AngularJS didn't recognise the unique attribute, so I added one to your controller.
Finally, change your html to this:
<div class="row">
<label data-ng-repeat="x in projects | unique:'b' | orderBy:'b'" >
<input
id="x.b"
type="checkbox"
ng-click="toggleSelection(x.b)"
ng-init="selectedRefs.push(x.b)"
ng-checked="selectedRefs.indexOf(x.b) > -1" />
{{x.b}}
</label>
</div>
... and your ng-repeat to this...
<tr ng-click="isCollapsed = !isCollapsed" ng-repeat-start="x in projects | filter:myFilter | orderBy:orderProp">
If you're interested in knowing how this works, add these lines:
<div style="margin:10px 10px 30px 10px">
<pre>{{ selectedRefs }} </pre>
</div>
I love this trick: you can see the exact contents of our "selectedRefs" array, and see it change as we tick/untick our checkboxes. This really helps when developing/testing our bindings!
As you can see, these changes use the new unique function to get your list of distinct values from your project array, and when the page first loads, we push all of the values into our new "selectedRefs" array.
["123","321","456","654","789","987"]
Then, as you tick/untick the checkboxes, we add/remove that item from this list.
Finally, we use that filter in the ng-repeat.
ng-repeat-start="x in projects | filter:myFilter | orderBy:orderProp"
Job done !
Update
If you wanted to start off with all checkboxes unticked, then it's a simple change. Just remove this line...
ng-init="selectedRefs.push(x.b)"
..and change the myFilter function to show all items initially..
$scope.myFilter = function (item) {
if ($scope.selectedRefs.length == 0)
return true;
var idx = $scope.selectedRefs.indexOf(item.b);
return idx != -1;
};
And to add a "Clear all" button, simply add a button to your form which calls a function in your AngularJS controller like this..
$scope.clearAll = function () {
$scope.selectedRefs = [];
};
(I haven't tested these suggestions though.)
ng-false-value directive needs a value set. Try ng-false-value='false' or ng-false-value='null' (in fact you can skip this one entirely if it has to just be a falsy value and not something concrete, like a string or certain number).
As you've pointed out in the comments, after selecting and then clearing the checkboxes, all rows are filtered out. It happens because unchecking the checkbox will set its value to false, and this does not agree with your entities' values (as you probably know, just stating it for others).
Therefore you do need to set this value to empty string in the end. That'd be the way:
$scope.$watch('quer.$', function () {
if ($scope.quer.$ === false) {
$scope.quer.$ = '';
}
});

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.

Categories

Resources