Change the observable to another on the html dynamically - html

I'm trying to update the observable on the html by changing the visibility, I thought it should update the binding but is not happening, is there another way to update the binding?
// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
this.isVisible = ko.observable(true);
this.counter0 = ko.observable(0);
this.counter1 = ko.observable(0);
this.counterDisplay = this.counter0;
this.add = function() {
console.log(this.counterDisplay());
const newValue = this.counterDisplay() + 1;
this.counterDisplay(newValue);
};
this.changeCounter = () => {
this.isVisible(!this.isVisible());
if(this.counterDisplay === this.counter0) {
this.counterDisplay = this.counter1;
console.log('change to counter1');
} else {
this.counterDisplay = this.counter0;
console.log('change to counter0');
}
this.isVisible(true);
}
}
// Activates knockout.js
ko.applyBindings(new AppViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p>Counter Main: <div data-bind="text: counterDisplay, visible: isVisible"></div></p>
<button data-bind="click: add">Add</button>
<button data-bind="click: changeCounter">Change</button>
<p>Counter0: <div data-bind="text: counter0"></div></p>
<p>Counter1: <div data-bind="text: counter1"></div></p>
On the example the counter main is displaying the value for counter 0, but after click on Change button the counter main should change to display the counter1 value, I thought that changing the visibility should re-render the DOM and bind to the counter1 value but it remains with the counter0 binding.

Visibility binding does not affect bindings itself, it only changes DOM element display status.
Changing binding can be achieved by using ko.cleanNode(DOMElement) but it should only be used if you really need to rebuild binding completely, which is not the case 99 times of 100.
In your case it is easier to simply create an observable which stores the index of active counter and a computed which displays active counter value. See the code below.
// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
const self = this;
self.activeCounterIndex = ko.observable('0');
self.counter0 = ko.observable(0);
self.counter1 = ko.observable(0);
this.activeCounterValue = ko.computed(function(){
return self['counter'+self.activeCounterIndex()]();
});
this.add = function() {
const newValue = self['counter'+self.activeCounterIndex()]() + 1;
self['counter'+self.activeCounterIndex()](newValue);
};
this.changeCounter = () => {
if (self.activeCounterIndex() === '0') {
self.activeCounterIndex('1');
} else {
self.activeCounterIndex('0');
}
}
}
// Activates knockout.js
ko.applyBindings(new AppViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p>Active counter (#<span data-bind="text:activeCounterIndex"></span>): <b data-bind="text: activeCounterValue"></b></p>
<button data-bind="click: add">Increment active counter</button>
<button data-bind="click: changeCounter">Swich counter</button>
<p>Counter #0: <span data-bind="text: counter0"></span></p>
<p>Counter #1: <span data-bind="text: counter1"></span></p>

Related

Nesting options under an if binding causes wrong behavior

I want to have a <select> without using the options binding, and nest the <option> element under an if binding.
The following is what I did (here's also a fiddle), which displays a behavior I wasn't expecting: The if seems to fire for each option selection, whereas what I expected is that it would fire only when adding the options elements to DOM.
Thus, when an option is chosen, it doesn't displayed. Only when choosing the same option again, it renders as it should.
What did I do wrong?
var DogHouseViewModel = function() {
var self = this;
self.allowedNames = ["A", "B", "C"];
self.puppies = ko.observableArray([]);
self.createPuppy = function () {
self.puppies.push(new DogViewModel());
}
self.isNameAlreadyTaken = function (puppyName) {
var puppies = self.puppies();
for (var i = 0; i < puppies.length; i++) {
if (puppies[i].dogName() == puppyName) {
return true;
}
}
return false;
}
self.printPuppiesName = function () {
self.puppies().forEach(function (puppy) {
alert(puppy.dogName())
})
}
}
var DogViewModel = function (dogName) {
var self = this;
self.dogName = ko.observable();
}
vm = new DogHouseViewModel()
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.1.0/knockout-min.js"></script>
<div>
<button data-bind="text: 'create a puppy', click: createPuppy"></button>
<button data-bind="text: 'print puppies names', click: printPuppiesName"></button>
<div data-bind="foreach: puppies">
<select data-bind="value: dogName">
<!-- ko foreach: $parent.allowedNames -->
<!-- ko if: !($root.isNameAlreadyTaken($data)) -->
<option data-bind="value: $data, text: $data"></option>
<!-- /ko -->
<!-- /ko -->
</select>
</div>
</div>
I believe this is what you are trying to do. The problem is that your allowedNames are the values of the options do you can't just remove them from the array. But you can clone the parent array and as it changes compute the array by returning a list of not used values.
I also added a check to make sure we don't accidentally add a puppy object to the puppies array when no names are available.
var DogHouseViewModel = function() {
var self = this;
self.allowedNames = ["A", "B", "C"];
self.puppies = ko.observableArray([]);
self.createPuppy = function() {
var newPuppy = new DogViewModel(self);
if(newPuppy.allowedNames().length > 0) { // Check to see if there are any names left.
self.puppies.push(newPuppy);
}
}
self.removePuppy = function(obj) {
self.puppies.remove(obj);
}
self.printPuppiesName = function() {
self.puppies().forEach(function(puppy) {
alert(puppy.dogName())
})
}
}
var DogViewModel = function(parent) {
var self = this;
self.dogName = ko.observable();
self.allowedNames = ko.computed(function() {
var allowedNamesClone = parent.allowedNames.slice(0);
var usedNames = parent.puppies().filter(function(pup) { // get all pups who have a name
return pup.dogName() !== '' && pup.dogName() !== self.dogName();
})
usedNames.forEach(function(pup) {
var index = allowedNamesClone.indexOf(pup.dogName());
if (index > -1) {
allowedNamesClone.splice(index, 1); // remove name from cloned array
}
})
return allowedNamesClone; // return new array
})
}
vm = new DogHouseViewModel()
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>
<button data-bind="text: 'create a puppy', click: createPuppy"></button>
<button data-bind="text: 'print puppies names', click: printPuppiesName"></button>
<div data-bind="foreach: puppies">
<!-- ko if: allowedNames().length > 0 -->
<select data-bind="options: allowedNames, value: dogName"></select>
<button data-bind="click: $root.removePuppy">x</button>
<!-- /ko -->
</div>
</div>

Knockout observable field in array to indicate order of items

I have a html view that's connected to Knockout viewmodel, and displays a list of items.
Each item in the list contains a textual name field, and a numeric order field.
A user can perform a "drag and drop" action to items in the UL list.
The "drag and drop" event changes the order of the items as follows:
<div id="wrapper">
<ul data-bind="foreach:Items">
<li draggable="true"
ondragover="event.preventDefault();"
data-bind="event:{dragstart:$root.dragItem,drop:$root.dropItem}">
<label data-bind="text:name"></label>
<label data-bind="text:orderNo"></label>
<input type="text" data-bind="value:name" />
</li>
</ul>
<script type="text/javascript">
var list = [{ name: 'Red', orderNo: 0 }
, { name: 'Green', orderNo: 1 }
, { name: 'Blue', orderNo: 2 }];
function viewmodel() {
var self = this;
self.Items = ko.mapping.fromJS(list);
self.ItemToDrag = ko.observable();
self.dragItem = function (item, event) {
self.ItemToDrag(item);
return true;
}
self.dropItem = function (item, event) {
event.preventDefault();
var up = self.ItemToDrag().orderNo() > item.orderNo();
self.ItemToDrag().orderNo(up ? item.orderNo() - 0.5 : item.orderNo() + 0.5);
//order this list
self.Items.sort(function (left, right) {
return left.orderNo() == right.orderNo() ? 0 : (left.orderNo() < right.orderNo() ? -1 : 1);
});
//set integer number
for (var i = 0; i < self.Items().length; i++) {
self.Items()[i].orderNo(i);
}
}
}
var vm;
$(document).ready(function () {
vm = new viewmodel();
ko.applyBindings(vm, $("#wrapper")[0]);
});
My question is, if it is possible with Knockout to change the contents of the order field automatically when the items of the list change their order through the UI.
Something like
<ul data-bind="foreach:Items,orderKey:orderNo"></ul>
Where orderKey indicates the order of the items, and which field to update in case of order change.
I'm not sure this is exactly what you need. This is custom binding, that sorts an array from foreach binding before:
ko.bindingHandlers.foreach["after"] = ["orderKey"];
ko.bindingHandlers.orderKey = {
update: function (el, valueAccessor, allBindingsAccessor, viewModel) {
var key = ko.unwrap(valueAccessor());
var allBindings = allBindingsAccessor();
if("foreach" in allBindings) {
var array = ko.unwrap(allBindings.foreach);
array.sort(function(a, b) { return a[key] > b[key]; });
allBindings.foreach = array;
}
}
};
// The model
var model = { Items: ko.observableArray([{text: 3}, {text: 1}, {text: 2}]) };
// Apply
ko.applyBindings(model);
// This simulate changes in observableArray
setTimeout(function() { model.Items.push({text: 0}) }, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<ul data-bind="foreach: Items, orderKey: 'text'">
<li data-bind="text: text"></li>
</ul>
No, there is no specific binding for that use case. In knockout, however, it is simple to write a custom binding. See the documentation. In the company I'm working for, we're using a knockout-based framework (developed by us) with tons of custom bindings, some of them really complex.
I just started to create such a binding for your use case. But I realized, it won't fit the purpose unless you have dozens of such lists.
What you can do, however, is to sort put the actual sorting into a knockout computed and just do the updating of the sort index in your drop function. See example below and don't hesitate to ask if something is not clear.
var list = [{ name: 'Red', orderNo: 0 }
, { name: 'Green', orderNo: 1 }
, { name: 'Blue', orderNo: 2 }];
function viewmodel() {
var self = this;
self._items = ko.mapping.fromJS(list);
self.Items = ko.pureComputed(function () {
return self._items().sort(function (a, b) {
return a.orderNo() < b.orderNo() ? -1 : 1;
});
});
self.ItemToDrag = ko.observable();
self.dragItem = function (item, event) {
self.ItemToDrag(item);
return true;
}
self.dropItem = function (item, event) {
event.preventDefault();
var up = self.ItemToDrag().orderNo() > item.orderNo();
self.ItemToDrag().orderNo(up ? item.orderNo() - 0.5 : item.orderNo() + 0.5);
}
}
var vm;
$(document).ready(function () {
vm = new viewmodel();
ko.applyBindings(vm, $("#wrapper")[0]);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<div id="wrapper">
<ul data-bind="foreach:Items">
<li draggable="true"
ondragover="event.preventDefault();"
data-bind="event:{dragstart:$root.dragItem,drop:$root.dropItem}">
<label data-bind="text:name"></label>
<label data-bind="text:orderNo"></label>
<input type="text" data-bind="value:name" />
</li>
</ul>

Get the value of all checkbox when checkall checkbox is checked

I'am new to angularjs, I'm creating an application of attendance. When i check the checkall checkbox all the checkbox of name is also check and What i really wanted to achieve is to get the value of checked checkboxes. I'm done with checking all checkboxes. I just want to store all the value of checkboxes in an array. I can only get data when i check those checkboxes one by one. Thank you in advance.
In my html here is my code.
<ion-checkbox ng-model="Selected" ng-click="checkAll()">
<div class="wew">
Check All Checkbox
</div></ion-checkbox>
</label></div>
<table><tr><th><center>
List of Names
</center></th>
<th colspan="3">
Actions
</th></tr><tr><td><label>
<ion-checkbox ng-repeat="role in roles" ng-model="isChecked" ng-
change="format(isChecked,role,$index)"><div class="wew">
{{role}}
</div></ion-checkbox>
</label></td>
And in my controllers code. First this is my code where i get the list of names.
$http.post(link1, {section: section}).success(function(attendance){
for(a = 0; a<attendance.length; a++){
$scope.roles = [
attendance[0].Full_Name,
attendance[1].Full_Name,
attendance[2].Full_Name,
attendance[3].Full_Name,
attendance[4].Full_Name,
attendance[5].Full_Name,
attendance[6].Full_Name,
attendance[7].Full_Name,
attendance[8].Full_Name,
attendance[9].Full_Name,
]
}
})
.error(function(err) {
console.log(err)
})
And this is my code where i wanted to execute the checkall and automatically store the data in $scope.selected = []; if i click the check all checkbox..
$scope.checkAll = function () {
if ($scope.Selected) {
$scope.Selected = false;
} else {
$scope.Selected = true;
}
$scope.isChecked= $scope.Selected;
$scope.selected = [];
$scope.format = function (isChecked, role, $index) {
if (isChecked == true) {
$scope.selected.push(role);
}
else {
var _index = $scope.selected.indexOf(role);
$scope.selected.splice(_index, 1);
}
var students = $scope.selected;
console.log(students);
}
}
try this code
<script>
$(function(){
var numbers = $("input[type='checkbox']:checked").map(function(_, el) {
return $(el).val();
}).get();
});
</script>

What does this tag do?

I have been learning how to read other people's code and when ever I see something like this <meganav-item item="item" ng-repeat="item in website.nav.primary"></meganav-item> I get stuck.
I have a basic understand of angular, but the problem is the <meganav> tag. I do not know what this is..I have done a Google search, but nothing useful is showing.
Update
I have managed to locate the file of the <meganav> element. After following the instructions from the links that you guys have provided, it led me to a file named "MegaNavItem.js". Here is the code:
window.tcoStore.directive('meganavItem', ['$timeout','transport', function($timeout,transport) {
var lockTimeout = false;
var meganavLocks = transport.getModel('meganavLocks', {lock : false});
var clear = function (){
if(meganavLocks.timeout){
$timeout.cancel(meganavLocks.timeout);
}
}
var action = function(callback, time) {
if(meganavLocks.lock){
return;
}
clear();
meganavLocks.timeout = $timeout(callback, time);
}
var dropLock = function(callback, time) {
meganavLocks.lock = false;
}
return {
restrict : 'E',
replace: true,
templateUrl : '/page/header/meganav/item.html',
scope : {
item : '=',
clickOnly : '#',
delayIn : '#',
delayOut : '#'
},
link : function($scope, elem, attrs){
if(!$scope.clickOnly){
$scope.delayInValue = parseInt($scope.delayIn || 300,10);
$scope.delayOutValue = parseInt($scope.delayOut || 500,10);
elem.on('mouseenter', $scope.showDelayed);
if($scope.delayOutValue > 0){
elem.on('mouseleave', $scope.hideDelayed);
}
}
},
controller: ['$scope', '$timeout', 'transport', '$location' ,
function($scope, $timeout, transport,$location) {
// When $location changes ...
$scope.$on('$locationChangeSuccess', function() {
$scope.hide(true);
$scope.isActive = !_.isUndefined($scope.item.link) && ($scope.item.link.replace(/\/+$/,'') == $location.path().replace(/\/+$/,''));
});
$scope.loadSubmenu =0;
// tranposrt model accessable by other items
var meganavVisibleModel = transport.getModel('meganavActive');
var meganavVisibleModelId = $scope.item.$$hashKey;
meganavVisibleModel[meganavVisibleModelId] = false;
// hide and show funs
$scope.hide = function(forceFullClose){
clear();
meganavVisibleModel[meganavVisibleModelId] = false;
if(forceFullClose) {
meganavLocks.lock = true;
$timeout.cancel(lockTimeout);
lockTimeout = $timeout(dropLock, 1000);
}
};
$scope.hideDelayed = function (delay) {
action($scope.hide, _.isNumber(delay) ? delay : $scope.delayOutValue);
};
$scope.show = function(){
if(meganavLocks.lock){
return;
}
clear();
$scope.loadSubmenu = 1;
for(var i in meganavVisibleModel){
meganavVisibleModel[i] = (meganavVisibleModelId == i);
}
};
$scope.showDelayed = function (delay) {
action($scope.show, _.isNumber(delay) ? delay : $scope.delayInValue);
};
$scope.$watch(function(){
$scope.visible = meganavVisibleModel[meganavVisibleModelId];
});
// first touch click, second touch go to link
$scope.touch = function($event, path){
if(!$scope.visible) {
//$event.preventDefault();
$scope.show();
}else if(tco.empty(path)) {
$scope.hide();
} else {
if(path.match(/^https?:/)){
window.location.href = path;
}else{
$location.path(path);
}
}
}
}]
}
}]);
And this file led me to another file named item.html. The code :
<li class="header--menu__item my-repeat-animation" ng-class="{ 'is-active': isActive, open : visible && item.groups.length}" off-click="hide()" >
<a ng-if=":: item.groups.length"
ng-class="{active: item.active}"
class="header--menu__item--link has-children"
ng-click="show()"
title="{{::item.name}}">
{{::item.name}}
</a>
<a ng-if=":: !item.groups.length"
class="header--menu__item--link"
href="{{::item.link}}"
title="{{::item.name}}">
{{::item.name}}
</a>
<div class="header-menu-dropdown ng-hide" ng-show="visible" ng-if=":: item.groups.length">
<ul class="header-menu-dropdown__meganavGroup">
<li ng-repeat="meganavGroup in item.groups" class="header--menu-group">
<span class="meganav--group--name">{{::meganavGroup.name}}</span>
<ul class="meganav--group--items">
<li ng-repeat="groupItem in meganavGroup.items">
{{::groupItem.name}}
<span class="icon"></span>
</li>
</ul>
</li>
<li class="header-menu-offers" ng-repeat="offer in item.offers">
<a href="{{::offer.offer_link}}" class="placeholder">
<img tco-image="offer.offer_image" crop="3" alt="{{::offer.offer_name}}" />
</a>
<span class="offer-name">{{::offer.offer_name}}</span>
</li>
</ul>
<div class="header-menu-message" ng-bind-html="item.message"></div>
</div>
</li>
My issue is now that I cannot make out what where to find {{::item.name}}, which is the thing that I want to change. What technique can I use to find {{::item.name}}?
Sorry for all the noob questions! Your help is much appreciated!
In Angular it is possible to build your own HTML element. You won't find any information about this element because it doesn't exist. The developer has created that on its own and handles the content inside a module. Have a look at http://www.aleaiactaest.ch/2012/07/29/build-your-own-html-element-with-angular/ for more information.
Hope this helps, Cheers.
As i've noticed it's Angular App, so probably there are defined an directive which is called meganavItem. See Angular Directive for more information, you have to find definition of that directive and discover what is html layout and logic lives under <meganav-item>. However if there are no directive with defined name.
Also it may be separate registered element, see "Custom Elements
"article of how it's done and it would be more easy for you to find out how it works ( if it registered in that way...)

watch changes on JSON object properties

I'm trying to implement a directive for typing money values.
var myApp = angular.module('myApp', []);
var ctrl = function($scope) {
$scope.amount = '0.00';
$scope.values = {
amount: 0.00
};
};
myApp.directive('currency', function($filter) {
return {
restrict: "A",
require: "ngModel",
scope: {
separator: "=",
fractionSize: "=",
ngModel: "="
},
link: function(scope, element, attrs) {
if (typeof attrs.separator === 'undefined' ||
attrs.separator === 'point') {
scope.separator = ".";
} else {
scope.separator = ",";
};
if (typeof attrs.fractionSize === 'undefined') {
scope.fractionSize = "2";
};
scope[attrs.ngModel] = "0" + scope.separator;
for(var i = 0; i < scope.fractionSize; i++) {
scope[attrs.ngModel] += "0";
};
scope.$watch(attrs.ngModel, function(newValue, oldValue) {
if (newValue === oldValue) {
return;
};
var pattern = /^\s*(\-|\+)?(\d*[\.,])$/;
if (pattern.test(newValue)) {
scope[attrs.ngModel] += "00";
return;
};
}, true);
}
};
});
HTML template:
<div ng-app="myApp">
<div ng-controller="ctrl">
{{amount}}<br>
<input type="text" style="text-align: right;" ng-model="amount" currency separator="point" fraction-size="2"></input>
</div>
</div>
I want to bind the value in my input element to values.amount item in controller but the watch instruction of my directive doesn't work.
How do I leverage two-way-data-binding to watch JSON objects?
To understand problem more precise I've created a jsfiddle.
The task is the following: Add extra zeros to the input element if user put a point. I mean if the value in input element say "42" and user put there a point, so the value now is "42." two extra zeros have to be aded like this "42.00".
My problems:
If I use ng-model="amount" the logic in input element works, but amount value of outer controller doesn't update.
If I use ng-model="values.amount" for binding, neither amount of outer controller nor input element logic works.
I really have to use ng-model="values.amount" instruction, but it doesn't work and I don't know why.
Any ideas?