knockout binding print function text instead of variable value - javascript

I'm writing a small application in JS and I decided to use Knockout.
Everything work well except from a single value that is not printed correctly and I don't understand why.
This is the html view where error appends (viaggio.arrivo is not visualized, and in place of correct value appears a function code like this "function c(){if(0 <arguments.length){if ..." and so on)
<input data-bind="value: viaggio.arrivo" />
And this is the javascript View Model.
Code is pretty long so I put it in a jsFiddle.
function ViewModel() {
function Viaggiatore(nome, cognome, eta, citta) {
var self = this;
self.nome = nome; self.cognome = cognome;
self.eta = ko.observable(eta);
self.citta = ko.observable(citta);
}
function Viaggio(viaggiatore, partenza, arrivo, mete) {
var self = this;
self.viaggiatore = ko.computed(viaggiatore);
self.partenza = ko.computed(partenza);
self.arrivo = ko.observable(arrivo);
self.mete = ko.computed(mete);
}
self.viaggiatore = new Viaggiatore("Mario", "Rossi", 35, "Como");
self.viaggio = new Viaggio(
function(){ return self.viaggiatore.nome+" "+self.viaggiatore.cognome; },
function(){ return self.viaggiatore.citta; },
"Roma",
function(){ return "mete" ;}
);
}
ko.applyBindings(new ViewModel());

I think you need brackets on one of your parameters, like so:
<p data-bind="text: viaggio.partenza()"></p>
Check out the updated fiddle: http://jsfiddle.net/mGDwy/2/

Related

Knockout multiple dynamic text boxes with one observable array

I am trying to save the data from the text-boxes to the localStorage using knockout JS! However I am new and not able to figure out this particular scenario. The field has same observable name! Please find my code below.
HTML Code:
<form data-bind="foreach: trialData">
<input type="text" name="name" data-bind="textInput: myData"><br>
</form>
JS Code:
var dataModel = {
myData: ko.observable('new'),
dataTemplate: function (myData) {
var self = this;
self.myData = ko.observable(myData);
}
};
dataModel.collectedNotes = function () {
var self = this;
self.trialData = ko.observableArray([]);
for (var i=0; i<5; i++) {
self.trialData.push (new dataModel.dataTemplate());
}
};
dataModel.collectedNotes();
ko.applyBindings(dataModel);
Traget: The data entered inside the text-boxes should be available in localStorage.
You need to define a Handler function to read the data from the Textboxes and save it to the localstorage. You need to reference the Data which is bound to the click event, which can be accessed using the first parameter. Knockout passes the data and event information as 2 arguments to the click handler function. So, you can add the event handler to your viewModel using the click binding and then unwrap the value and save it to localStorage.
saveToLocalStorage : function(data){
var datatoStore = JSON.stringify(data.trialData().map(x=>x.myData()));
console.log(datatoStore);
localStorage.setItem("TextBoxValue", datatoStore);
}
Complete Code: Please note since this is a sandboxed environment (Running this js Snippet on StackOverflow), localStorage wouldn't work, but it should work in your code. I have added a line in console to get the value to Store.
var dataModel = {
myData: ko.observable('new'),
dataTemplate: function (myData) {
var self = this;
self.myData = ko.observable(myData);
},
saveToLocalStorage : function(data){
var datatoStore = JSON.stringify(data.trialData().map(x=>x.myData()));
console.log(datatoStore);
localStorage.setItem("TextBoxValue", datatoStore);
}
};
dataModel.collectedNotes = function () {
var self = this;
self.trialData = ko.observableArray([]);
for (var i=0; i<5; i++) {
self.trialData.push (new dataModel.dataTemplate());
}
};
dataModel.collectedNotes();
ko.applyBindings(dataModel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<form data-bind="foreach: trialData">
<input type="text" name="name" data-bind="textInput: myData"><br>
</form>
<button data-bind="click:saveToLocalStorage">Save To local storage</button>

Strange behavior with variable names and data between controllers

I have a controller that is supposed to write text from an input field to the screen. If I use this controller by itself everything works as expected:
(function() {
angular.module('test', []);
function OneCtrl() {
vm = this;
vm.changeHandler = changeHandler;
vm.item = "";
vm.value = "";
}
angular
.module('test')
.controller('OneCtrl', OneCtrl);
var changeHandler = function() {
vm.value = vm.item;
console.log(vm.item);
};
})();
Try here: http://codepen.io/minuskruste/pen/qdrZqq
However, if I add another controller with the same behavior something really weird happens. First of all, the input from field 1 is not sent to console anymore and the text is also not inserted into the html body. Second of all, when I type something into input field 2 it behaves correctly. If I now go back to field 1 and type there, suddenly field 2 input is output to console, even though controller two was never told to do so! This is controller 2:
(function(){
function TwoController(){
vm = this;
vm.changeHandler = changeHandler;
vm.item = "";
vm.value = "";
}
angular
.module('test')
.controller('TwoController', TwoController);
var changeHandler = function() {
vm.value = vm.item;
};
})();
Try here: http://codepen.io/minuskruste/pen/QbpNdY
Is this normal behavior? I was very surprised by it. I also checked if maybe the changeHandler() leaked to global space but since I've put everything in closures that's not the case. Furthermore this is consistent over different platforms i.e. Chrome and FF. Any ideas?
Part of the issue you are having is that you are declaring vm without the var keyword, which makes it a global variable.
However, vm with the var keyword is in the local scope of the controller. As a result, it's not available to the changeHandler() anymore. If you reorder your code and declare changeHandler() inside the controller, it will work.
(function() {
angular.module('test', []);
function OneCtrl() {
var vm = this;
vm.item = "";
vm.value = "";
vm.changeHandler = function() {
vm.value = vm.item;
console.log(vm.item);
}
}
angular
.module('test')
.controller('OneCtrl', OneCtrl);
})();
(function(){
function TwoController() {
var vm = this;
vm.item = "";
vm.value = "";
vm.changeHandler = function() {
vm.value = vm.item;
console.log(vm.item);
}
}
angular
.module('test')
.controller('TwoController', TwoController);
})();
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script>
<body ng-app="test">
<h1>Choice array</h1>
<div>
<form novalidate ng-controller="OneCtrl as one">
<input type="text" ng-change="one.changeHandler()" ng-model="one.item">
<div>{{one.value}}</div>
</form>
<br><br><br>
</div>
<div>
<form novalidate ng-controller="TwoController as two">
<input type="text" ng-change="two.changeHandler()" ng-model="two.item">
<div>{{two.value}}</div>
</form>
</div>
</body>
This happens because you're using global "vm" variable, which containg this reference of controller. And with second controller you're overwriting vm variable with reference to second controller.
I've updated your code to use proper this references
Also angular supports another data binding approach with special $scope object: https://docs.angularjs.org/guide/scope
(function() {
angular.module('test', []);
function OneCtrl($scope) {
// This construction makes sure I know which context is addressed. I can now hand vm (view-model) inside an object and the context doesn't change.
this.changeHandler = angular.bind(this, changeHandler);
this.item = "";
this.value = "";
}
angular
.module('test')
.controller('OneCtrl', OneCtrl);
var changeHandler = function() {
this.value = this.item;
console.log(this.item);
};
})();
(function(){
function TwoController(){
// This construction makes sure I know which context is addressed. I can now hand vm (view-model) inside an object and the context doesn't change.
this.changeHandler = angular.bind(this, changeHandler);
this.item = "";
this.value = "";
}
angular
.module('test')
.controller('TwoController', TwoController);
var changeHandler = function() {
this.value = this.item;
};
})();
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script>
<body ng-app="test">
<h1>Choice array</h1>
<div>
<form novalidate ng-controller="OneCtrl as one">
<input
type="text"
ng-change="one.changeHandler()" ng-model="one.item">
<div>{{one.value}}</div>
</form>
<br>
<br>
<br>
</div>
<div>
<form novalidate ng-controller="TwoController as two">
<input
type="text"
ng-change="two.changeHandler()" ng-model="two.item">
<div>{{two.value}}</div>
</form>
</div>
</body>
Here is a good article on the Controller As syntax.
http://www.johnpapa.net/angularjss-controller-as-and-the-vm-variable/
If you declare the vm variable in each controller it will prevent the overwriting behavior you are seeing. Javascript is functional scoped, which means if it doesn't find the variable declaration for vm in the current function, it will go up the prototypical chain until it finds the declaration (var vm). If it doesn't find any declaration in the global scope, it will automatically create one for you. By declaring inside each controller you will prevent them from both sharing the same global scope.
function OneCtrl() {
// This construction makes sure I know which context is addressed. I can now hand vm (view-model) inside an object and the context doesn't change.
var vm = this;
vm.item = "";
vm.value = "";
vm.changeHandler = function() {
console.log(vm.item);
vm.value = vm.item;
};
}
http://plnkr.co/edit/KdZvG7d2COLNcjRIfyHb?p=preview

Angular edit a copy of a copy

I'm having a few problems editing a copy of a copy.
When you first edit a record it is assigned to a $scope.original and a copy is taken for editing and stored in $scope.copy which can be changed and saved back to $scope.original which in-turn updates $scope.something correctly.
The problem is while editing the first record if you then take a copy of one of the values for further editing, it doesn't get updated when the $scope.saveSomething() function is called.
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.Something = [{
name: "Aye",
desc: new Date()
}, {
name: "Bee",
desc: new Date()
}, {
name: "See",
desc: new Date()
}];
//=================== First copy
$scope.edit = function(what) {
$scope.original = what;
$scope.copy = angular.copy(what);
}
$scope.save = function(copy) {
angular.copy($scope.copy, $scope.original);
$scope.cancel();
}
$scope.cancel = function() {
$scope.copy = null;
}
//=================== Second copy
$scope.editName = function(what) {
$scope.originalName = what;
$scope.copyName = angular.copy(what);
}
$scope.saveName = function() {
angular.copy($scope.copyName, $scope.originalName);
$scope.cancelName();
}
$scope.cancelName = function() {
$scope.copyName = null;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<div ng-repeat="s in Something">
<pre>{{s | json}}</pre>
<a ng-click='edit(s)'>edit</a>
<br/>
<br/>
</div>
<input type='text' ng-model='copy.name' />
<input type='text' ng-model='copy.desc' />
<br/>
<button ng-click='save(copy)' ng-disabled="!copy">save</button>
<button ng-click='cancel()' ng-disabled="!copy">cancel</button>
<a ng-click='editName(copy.name)'>edit name</a>
<br>
<br>
<input type='text' ng-model='copyName' />
<br>
<button ng-click='saveName()' ng-disabled="!originalName">saveName</button>
<button ng-click='cancelName()' ng-disabled="!originalName">cancelName</button>
</div>
</div>
I'm fairly new to Angular, and have been scratching my head on this one for a while now, any ideas why?
Edit
Updated the code to give a better example, the first version suggested that you might know which value of the first edit's values you were editing, and the solution scarlz posted ( http://jsfiddle.net/Karl33to/w23ppp9r/ ) just sets that value directly in the second save function, but I need to be able to do the second edit on any of the values that the first edit loads.
Have also created a fiddle if that's easier for you to run / fork http://jsfiddle.net/w23ppp9r/2/
Your problem arises from your use of angular.copy. In $scope.saveName, your destination $scope.originalName is a string, which will result in angular throwing an error.
There is actually no reason to use angular.copy at all if you're working with primitives. Instead, you could use the following here:
$scope.editName = function(what) {
$scope.originalName = what;
$scope.copyName = what;
};
$scope.saveName = function() {
$scope.copy.name = $scope.copyName;
$scope.cancelName();
}
I've managed to come up with a simple solution, which seems to work.
Instead of passing the primitive to the second edit function, if I pass in the key and a copy of the object instead, I can then update the first copy correctly.
Here's a working fiddle http://jsfiddle.net/w23ppp9r/3/
... and the relevant bit of code:
//=================== Second copy
$scope.editSomething = function(key, obj) {
$scope.originalKey = key;
$scope.originalObj = obj;
$scope.copyVal = obj[key];
};
$scope.saveSomething = function(newVal) {
$scope.originalObj[$scope.originalKey] = newVal;
$scope.cancelEdit();
}
$scope.cancelEdit = function() {
$scope.originalKey = null;
$scope.originalObj = null;
$scope.copyVal = null;
}
Is there a better answer?

How can I toggle the display of a textarea via a button using knockout with the foreach binding?

I am new to knockout. For my problem, I am trying to make it so that for each project, there is a button and textarea. The textarea will be hidden upon page load. If I click the button, it will show the textarea (toggle). Currently, if I click the button, ALL textareas on the page will show, rather than just the corresponding textarea.
I'm hoping the fix for this isn't too dramatic and involving a complete reworking of my code as by some magic, every other functionality has been working thus far. I added the {attr id: guid} (guid is a unique identifier of a project retrieved from the database) statement in an attempt to establish a unique ID so that the right controls were triggered...although that did not work.
Sorry I do not have a working jfiddle to show the issue... I tried to create one but it does not demonstrate the issue.
JS:
//if a cookie exists, extract the data and bind the page with cookie data
if (getCookie('filterCookie')) {
filterCookie = getCookie('filterCookie');
var cookieArray = filterCookie.split(",");
console.log(cookieArray);
$(function () {
var checkboxes = new Array();
for (var i = 0; i < cookieArray.length; i++) {
console.log(i + cookieArray[i]);
checkboxes.push(getCheckboxByValue(cookieArray[i]));
//checkboxes.push(document.querySelectorAll('input[value="' + cookieArray[i] + '"]'));
console.log(checkboxes);
checkboxes[i].checked = true;
}
})
filterCookie = getCookie('filterResultsCookie');
cookieArray = filterCookie.split(",");
filterCookieObj = {};
filterCookieObj.action = "updateProjects";
filterCookieObj.list = cookieArray;
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify(filterCookieObj)
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
//if the cookie doesn't exist, just bind the page
else {
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify({
action: "getProjects"
})
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
View Model:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
};
};
HTML:
<!-- ko foreach: projects -->
<div id="eachOppyProject" style="border-bottom: 1px solid #eee;">
<table>
<tbody>
<tr>
<td><a data-bind="attr: { href: '/tools/oppy/' + guid }" style="font-size: 25px;"><span class="link" data-bind=" value: guid, text: name"></span></a></td>
</tr>
<tr data-bind="text: projectDescription"></tr>
<%-- <tr data-bind="text: guid"></tr>--%>
</tbody>
</table>
<span class="forminputtitle">Have you done project this before?</span> <input type="button" value="Yes" data-bind="click: $parent.toggleTextArea" class="btnOppy"/>
<textarea placeholder="Tell us a little of what you've done." data-bind="visible: $parent.show, attr: {'id': guid }" class="form-control newSessionAnalyst" style="height:75px; " /><br />
<span> <input type="checkbox" name="oppyDoProjectAgain" style="padding-top:10px; padding-right:20px;">I'm thinking about doing this again. </span>
<br />
</div><br />
<!-- /ko -->
Spencer:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.projects().forEach(function() { //also tried proj.forEach(function())
self.projects().showComments = ko.observable(false);
self.projects().toggleComments = function () {
self.showComments(!self.showComments());
};
})
};
It's weird that
data-bind="visible: show"
doesn't provide any binding error because context of binding inside ko foreach: project is project not the ProjectViewModel.
Anyway, this solution should solve your problem:
function ViewModel() {
var self = this;
var wrappedProjects = proj.map(function(p) {
return new Project(p);
});
self.projects = ko.observableArray(wrappedProjects);
}
function Project(proj) {
var self = proj;
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
}
return self;
}
The problem is that the show observable needs to be defined in the projects array. Currently all the textareas are looking at the same observable. This means you'll have to move the function showTextArea into the projects array as well.
Also you may want to consider renaming your function or getting rid of it entirely. Function names which imply they drive a change directly to the view fly in the face of the MVVM pattern. I'd recommend a name like "toggleComments" as it doesn't reference a view control.
EDIT:
As an example:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
foreach(var project in self.projects()) {
project.showComments = ko.observable(false);
project.toggleComments = function () {
self.showComments(!self.showComments());
};
}
};
There is probably a much cleaner way to implement this in your project I just wanted to demonstrate my meaning without making a ton of changes to the code you provided.

knockout unable to process binding "foreach"

I'm new to Knockout and I'm building an app that's effectively a large-scale calculator. So far I have two instances of knockout running on one page. One instance is working perfectly fine, however the other one is entirely broken and just won't seem to register at all?
Below is my Javascript, fetchYear is the function that works perfectly fine and fetchPopulation is the one that's completely broken. It doesn't seem to register "ageview" from the HTML at all and I can't figure out.
The error:
Uncaught ReferenceError: Unable to process binding "foreach: function
(){return ageView }" Message: ageView is not defined
Thanks in advance.
JS:
var index = {
fetchYear: function () {
Item = function(year){
var self = this;
self.year = ko.observable(year || '');
self.chosenYear = ko.observable('');
self.horizon = ko.computed(function(){
if(self.chosenYear() == '' || self.chosenYear().horizon == undefined)
return [];
return self.chosenYear().horizon;
});
};
YearViewModel = function(yeardata) {
var self = this;
self.yearSelect = yeardata;
self.yearView = ko.observableArray([ new Item() ]);
self.add = function(){
self.yearView.push(new Item("New"));
};
};
ko.applyBindings(new YearViewModel(yearData));
},
fetchPopulation: function () {
popItem = function(age){
var self = this;
self.age = ko.observable(age || '');
self.chosenAge = ko.observable('');
self.population = ko.computed(function(){
if(self.chosenAge() == '' || self.chosenAge().population == undefined)
return [];
return self.chosenAge().population;
});
};
PopulationViewModel = function(populationdata) {
var self = this;
self.ageSelect = populationdata;
self.ageView = ko.observableArray([ new popItem() ]);
self.add = function(){
self.ageView.push(new popItem("New"));
};
};
ko.applyBindings(new PopulationViewModel(populationData));
}
}
index.fetchYear();
index.fetchPopulation();
HTML:
<div class="row" data-bind="foreach: yearView">
<div class="grid_6">
<img src="assets/img/index/calendar.png" width="120" height="120" />
<select class="s-year input-setting" data-bind="options: $parent.yearSelect, optionsText: 'year', value: chosenYear"></select>
<label for="s-year">Start year for the model analysis</label>
</div>
<div class="grid_6">
<img src="assets/img/index/clock.png" width="120" height="120" />
<select class="s-horizon input-setting" data-bind="options: horizon, value: horizon"></select>
<label for="s-horizon">Analysis time horizon</label>
</div>
</div>
<div class="row" data-bind="foreach: ageView">
<div class="grid_6">
<img src="assets/img/index/calendar.png" width="120" height="120" />
<select class="s-year input-setting" data-bind="options: ageSelect, optionsText: 'age', value: chosenAge"></select>
<label for="s-agegroup">Age group of <br> target population</label>
</div>
<div class="grid_6">
<img src="assets/img/index/clock.png" width="120" height="120" />
<input class="s-population input-setting"></input>
<label for="s-population">Size of your patient <br> population <strong>National</strong> </label>
</div>
</div>
When you do this (in fetchYear):
ko.applyBindings(new YearViewModel(yearData));
You are binding the entire page with the YearViewModel view model. But the YearViewModel doesn't have a property called ageView so you get the error and knockout stops trying to bind anything else.
What you need to do is restrict your bindings to cover only part of the dom by passing the element you want to ko.applyBindings. For example:
<div class="row" id="yearVM" data-bind="foreach: yearView">
//....
<div class="row" id="popVM" data-bind="foreach: ageView">
And then:
ko.applyBindings(new YearViewModel(yearData), document.getElementById("yearVM"));
//...
ko.applyBindings(new PopulationViewModel(populationData), document.getElementById("popVM"));
Now your bindings are restricted just to the part of the DOM that actually displays stuff from that model.
Another alternative is to just have your two view models as part of a parent view model and then you can apply the binding to the entire page. This makes it easier if you need to mix parts from both VMs and they are not conveniently separated in distinct sections of your page. Something like:
var myParentVM = {
yearVM : index.fetchYear(), // note, make this return the VM instead of binding it
popVM : index.fetchPopulation(), // ditto
}
ko.applyBindings(myParentVM);
And then you'd declare your bindings like so:
<div class="row" data-bind="foreach: yearVM.yearView">
The main reason why this is not working is because you call ko.applyBindings() more than once on a page (that is not really forbidden but is a bad practice in my opinion).
If you need to call it twice, you must call it with a container for which region this bind is meant to.
Something like this:
ko.applyBindings(new YearViewModel(yearData), document.getElementById('YourYearViewElementId'));
The error you get is from the first binding, which tries to process the whole page and does not find the 'ageView' in its ViewModel.
Better would be if you build a single ViewModel for a single Page where you have sub-models for sections if needed.
Some pseudo code for such a scenario:
var Section1ViewModel = function() {
var self = this;
self.property1 = ko.observable();
self.myComputed = ko.computed(function () {
// do some fancy stuff
});
self.myFunc = function() {
// do some more fancy stuff
};
}
var Section2ViewModel = function() {
var self = this;
self.property1 = ko.observable();
self.myComputed = ko.computed(function () {
// do some fancy stuff
});
self.myFunc = function() {
// do some more fancy stuff
};
}
var PageViewModel = function() {
var self = this;
self.section1 = ko.observable(new Section1ViewModel());
self.section2 = ko.observable(new Section2ViewModel());
self.myGlobalFunc = function() {
// do some even more fancy stuff
}
}
ko.applyBindings(new PageViewModel());
Hope that helps.
Best regards,
Chris

Categories

Resources