EDIT: Problem was not related to the binding but to a simple JavaScript mistake.
I have a question concerning a click binding within a foreach binding.
I have a list with items showing a drop down box to select a value from the master data. Items can be added and removed from that list.
The button to remove items is nested in the foreach binding. Therefore I expected that I should bind it with $parent>
<button data-bind="click: $parent.removeNumber">-</button>
That does not work. But the following version works:
<button data-bind="click: removeNumber">-</button>
I do not understand why.
The code:
<h2>numbers:</h2>
<ul data-bind="foreach: numbers">
<li>
<select data-bind="value: id,
options: masterData,
optionsText: 'caption',
optionsValue: 'id'"></select>
<br />
value: <span data-bind="text: id"></span>
<br />
<button data-bind="click: $parent.removeNumber">-</button>
</li>
</ul>
<button data-bind="click: addNumber">+</button>
function ViewModel() {
self.masterData = [{ id: 1, caption: "One"},
{ id: 2, caption: "Two"}];
self.numbers = ko.observableArray([{
id: ko.observable(2)}]);
self.addNumber = function() {
self.numbers.push({
id: ko.observable(2)
});
};
self.removeNumber = function(item) {
self.numbers.destroy(item);
console.log("removed" + item);
};
}
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
I have created a fiddle (with the not working version):
http://jsfiddle.net/delixfe/NWWH8/
Thanks for your help.
You had me for a second!
You are correct, $parent should be required. Your mistake was not defining self in your viewmodel. After doing that, $parent was required on the removeButton, as well as the masterData binding.
Here is a working fiddle: http://jsfiddle.net/FpSWb/
Related
I have a knockout.js page which loads data from an API and uses the knockout mapping plugin to turn the data into a parameter on the ViewModel.
The data contains nested objects e.g.
[{
id: 1,
targetField: {
id: 132,
name: 'Field ABC',
...
},
conditionalOperator: {
id: 8,
display: 'Less Than'
},
conditionalValue:13
},
...
]
Loaded into the page view model
var PageViewModel = function() {
...
this.allConditionLogic = ko.observableArray();
}
var pageViewModel = new PageViewModel();
$.get('api/...')
.done(function(data) {
pageViewModel.allConditionLogic(ko.mapping.fromJS(data));
});
The html contains bindings to the objects
<div data-bind="foreach: allConditionLogic">
<p>Field id <span data-bind="text: targetField().id"></span> <span data-bind="text: conditionalOperator().display"></span> <span data-bind="text: conditionalValue"></span></p>
</div>
This however errors as before the ajax call has returned, targetField and conditionalOperator are null.
It is possible to use extra span elements and the with binding which doesn't create the internal html if the bound object doesn't exist - e.g.
<p>Field id <span data-bind="with: targetField"><span data-bind="text: id"></span> <span data-bind="with: conditionalOperator"><span data-bind="text: display"></span></span> <span data-bind="text: conditionalValue"></span></p>
however this seams somewhat overkill. I could define a blank object in allConditionLogic with the correct fields, but that requires a lot more typing and needs updating if the API changes.
Is there a better way of getting this to work?
A simple solution could be to create a knockout observable variable and set it to false until you have the data returned by the API. Then wrap your div with that observable inside a ko if: binding -
var PageViewModel = function() {
this.allConditionLogic = ko.observableArray();
//Set it to false initially
this.hasAPIreturnedData = ko.observableArray(false);
}
var pageViewModel = new PageViewModel();
$.get('api/...')
.done(function(data) {
pageViewModel.allConditionLogic(ko.mapping.fromJS(data));
//make it true after data is returned and is transformed
pageViewModel.hasAPIreturnedData(true);
});
<!--ko if: hasAPIreturnedData -->
<div data-bind="foreach: allConditionLogic">
<p>Field id <span data-bind="text: targetField().id"></span> <span data-bind="text: conditionalOperator().display"></span> <span data-bind="text: conditionalValue"></span></p>
</div>
<!--/ko-->
There could be more elegant ways of handling this but that depends on a lot of things. As I said, this is the simplest solution I could think of :)
I have the following code:
HTML:
<ul class="list" data-bind="foreach: list">
<li class="title" data-bind="text:title" contenteditable="true"></li>
<li class="item" data-bind="text:item" contenteditable="true"></li>
</ul>
<button type="button">Save</button>
JS:
var data = {
"list": [{
"title" : "title one",
"item" : "item one"
}]
}
var viewModel=
{
list : ko.observable(data.list)
};
ko.applyBindings(viewModel);
$("button").on("click", function(){
var vm = viewModel;
ko.applyBindings(vm);
var data = ko.toJSON(vm);
console.log(data);
});
However when I do this I get this error:
Uncaught Error: You cannot apply bindings multiple times to the same element.
knockout-3.1.0.js:58
What I would like to do is change the text of one of the items and have it save to the view model when I click the save button.
FIDDLE:
http://jsfiddle.net/sr4Fg/13/
There are few things.
only call applyBindings once, then ko will sync data for you.
you don't need to create a save button, ko sync data automatically.
your title and item are NOT ko.observable, hence ko has no way to auto-update it for you.
ko "text" binding is kind of one way binding, it only updates view when your value changes. You need to use "value" binding in an input tag to get two way binding.
right now, there is no existing ko binding supporting contenteditable. You may build a custom bindingHandler for it, but beware it's tricky to get contenteditable change event.
you list should be an observableArray.
Here is the working example with "value" binding: http://jsfiddle.net/sr4Fg/41/
<ul class="list" data-bind="foreach: list">
<li class="title"><input data-bind="value:title" /></li>
<li class="item"><input data-bind="value:item" /></li>
</ul>
<button type="button">Save</button>
var viewModel=
{
list : ko.observableArray([{
"title" : ko.observable("title one"),
"item" : ko.observable("item one")
}])
};
ko.applyBindings(viewModel);
$("button").on("click", function(){
var data = ko.toJSON(viewModel);
console.log(data);
});
Understand you may load JSON data from ajax call, it's tedious to change all the values into ko.observable. Try http://knockoutjs.com/documentation/plugins-mapping.html if you need.
I am looking to implement Angular, Knockout or another to data-bind a wizard-style application form proof-of-concept (no server-side code). However I appear to be breaking the data bindings when I clone the data-bound div.
The first few steps of the application form capture data, while the later steps present the data back to the user to allow them confirm what was entered. I am manipulating the DOM by inserting the appropriate step when 'next' is pressed and taking out the last step when 'previous' is pressed. I do this using detatch, clone and remove.
Can anyone give advise on the approach they would take to make this work, and what frameworks I should look at?
Below is pseudocode to give an idea of the structure. The pseudo-data-binding-code is just how I thought it would work, I'm not bedded to any framework.
HTML View
<div id="wizard">
<div id="step1">Enter your name: <input type="text" id="name" /></div>
</div>
<div id="actions"><input type="button" value="Previous" /><input type="button" value="Next" onClick="goNext();" /></div>
<div id="steps">
<div id="stepA">Enter your age: <input type="text" id="age" databind="theAge" /></div>
<div id="stepB">The age you entered - {{ theAge }} is too young!</div>
<div id="stepC">Enter your favourite colour: <input type="text" id="faveColour" databind="faveCol" /></div>
<div id="stepD">Hi {{ name }}. You are {{ age }} years old and your favourite colour is {{ faveCol }}</div>
</div>
JavaScript
<script>
function goNext() {
// figure out which step is next
insertStepIntoWizard(step, index, title);
}
function insertStepIntoWizard(step, index, title) {
var element = step.detach();
wizard.steps('insert', index, {
title: title,
content: element.clone()
});
console.log('insertStepIntoWizard - just inserted ' + step.attr('id') + ' into wizard position ' + index);
}
</script>
I think you're thinking in terms of having your view reflect your entity model, which is basically templating. If you're looking to use Knockout/Angular, consider using it's view model to manage page state / flow / actions, in addition to controlling the entity. (Writing jQuery code to poke about with the DOM and clone, show/hide, etc is no fun). #sabithpocker makes a similar point.
Working example: I'm familiar with Knockout, and have created an example jsFiddle of your scenario: http://jsfiddle.net/overflew/BfRq8/5/
Notes:
I've used the template tag to house each section of the wizard, and all steps point to the same model/entity within the viewmodel.
To emphasise what's happening with the publish/subscribe nature of the bindings:
The user input is also relayed at the bottom of the page.
The form title is dynamic, as well as the 'step'
ko.computed is used to 'compute' the full name and if there are any 'steps' left to go
Knockout-specific: Notice the occurrence of brackets popping up around the place. This is one thing that may catch you out occasionally if you choose to learn Knockout. It just means that you're evaluating the binding container to get the value.
View model
<div>
<h3 data-bind="text: currentStep().name"></h3>
<div data-bind="template: { name: 'wizard-step1' }, visible: currentStep().id === 0"></div>
<div data-bind="template: { name: 'wizard-step2' }, visible: currentStep().id === 1"></div>
<div data-bind="template: { name: 'wizard-step3' }, visible: currentStep().id === 2"></div>
<input type="button" value="Next step" data-bind="click: onNext, visible: hasNextSteps" />
<input type="button" value="Submit" data-bind="click: onSubmit,visible: !hasNextSteps()" />
<div data-bind="visible: submitResultMessage, text: submitResultMessage"></div>
</div>
<div>
<h3>Your inputs</h3>
<div data-bind="visible: questions.fullName">Full name: <span data-bind="text: questions.fullName"></span></div>
<div data-bind="visible: questions.age">Age: <span data-bind="text: questions.age"></span>
</div>
<div data-bind="visible: questions.favouriteColour">Favourite colour: <span data-bind="text: questions.favouriteColour"></span>
</div>
</div>
<script type="text/html" id="wizard-step1">
<div>
First name: <input data-bind="value: questions.firstName, valueUpdate:'afterkeydown'" />
</div>
<div>
Last name: <input data-bind="value: questions.lastName, valueUpdate:'afterkeydown'" />
</div>
</script>
<script type="text/html" id="wizard-step2">
<div>
Age: <input data-bind="value: questions.age, valueUpdate:'afterkeydown'" />
</div>
</script>
<script type="text/html" id="wizard-step3">
<div>
Favourite colour: <input data-bind="value: questions.favouriteColour, valueUpdate:'afterkeydown'" />
</div>
</script>
View
// Entity for holding form data.
var FormData = function() {
var self = this;
self.firstName = ko.observable("");
self.lastName = ko.observable("");
self.age = ko.observable("");
self.favouriteColour = ko.observable("");
self.fullName = ko.computed(function() {
if (!self.firstName() && !self.lastName()) {
return "";
}
return self.firstName() + " " + self.lastName();
});
}
// Quick handling for managing steps of the wizard
var wizardSteps = [
{ id: 0, name: "Wizard step 1"},
{ id: 1, name: "More questions"},
{ id: 2, name: "Last step"}
];
var ViewModel = function() {
var self = this;
// Properties
self.questions = new FormData();
self.currentStep = ko.observable(wizardSteps[0]);
self.submitResultMessage = ko.observable();
// Actions
self.onNext = function() {
var currentIndex = self.currentStep().id;
if (self.hasNextSteps()) {
// Move forward one step on the wizard
self.currentStep(wizardSteps[currentIndex + 1]);
}
};
self.onSubmit = function() {
self.submitResultMessage("Data is now submitted ");
};
// Page control
self.hasNextSteps = ko.computed(function() {
var currentIndex = self.currentStep().id;
return currentIndex < wizardSteps.length - 1;
});
};
ko.applyBindings(new ViewModel());
What most of the bleeding edge javascript frameworks is trying to do is attempting to manage the huge amount of code in current applications where large amount of business logic is implemented with javascript in client side. So they are mostly trying to provide you with some architecture to organize your code to make it easy to manage, read and scale.
In your case you are trying to neglect the architecture like MVC or MVWhatever they are providing and use the framework to do some templating, If I understand you correctly. For that you can better get the help of some templating engine in javascript like handlebars and use it to manually render your data stored in your current javascript app.
see it here http://handlebarsjs.com/
Simply re-initialize your bindings.
This solves my problem when using knockout.
When you change the main DOM element which is actually being referenced for MVVM, it turn-out that any change or re-structuring that element hampers the dom relationship.
Eventually, though the placement of the item looks same but technically, in java script engine it is not. So the problem occurs.
In order to fix something like this, simply have a constructor which will initialize or re-initialize the mapping for a particular element in DOM.
Hope that solves your problem.
Example:
function ReInitializeVM()
{
ko.cleanNode(document.getElementById("userDetails"));
ko.applyBindings(viewModel, document.getElementById("userDetails"));
}
The function can be called everytime where you feel the need of re-initializing the bindings. Calling the function will remove any binding on the element with id "userDetails" and then the applyBindings can be called to initialize the binding. This will invoke bindings for any new/changed element.
Is there a way to implement the statements below using a custom binding, to eliminate the if-binding:
<div data-bind="foreach: $root.customersVM.customers">
#Html.Partial("_Customer")
<div data-bind="foreach: $root.ordersVM.orders">
<!-- ko if: customer() == $parent.id() -->
#Html.Partial("_Order")
<!-- /ko -->
</div>
</div>
Or put it in another way: Does someone know Way 2 in the answer to Knockout.js foreach: but only when comparison is true?
How about creating another computed or function that does the filtering and that you can iterate over instead of iterating over orders?
HTML
<div data-bind="with: filteredCustomers('smith')">
<span data-bind="text: name"></span>
</div>
<div data-bind="foreach: customers">
<span data-bind="text: name"></span>
</div>
<div data-bind="foreach: filteredOrders(4)">
<span data-bind="text: id"></span>
</div>
<div data-bind="foreach: orders">
<span data-bind="text: id"></span>
</div>
<button data-bind="click: function() {customers.push({name:'Smith'});}">Add customer</button>
<button data-bind="click: function() {orders.push({id:4});}">Add order</button>
Javascript:
var vm = {
customers: ko.observableArray([
{name: 'Smith'}, {name: 'Williams'}, {name: 'Brown'}, {name: 'Miller'}
]),
orders: ko.observableArray([
{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 4}
])
};
// return first hit (unique ID)
vm.filteredCustomers = function(name) {
return ko.utils.arrayFirst(this.customers(), function(customer) {
return (customer.name.toLowerCase() === name.toLowerCase());
});
};
// return all hits
vm.filteredOrders = function(id) {
return ko.utils.arrayFilter(this.orders(), function(order) {
return (order.id === id);
});
};
ko.applyBindings(vm);
I think your best bet on performance would be to take the data from 2 different databases and put them together in the same viewmodel. For example, in your viewmodel in javascript, grab the customers first. Then grab the orders. Add an orders property to each customer and add an orders observablearray to it.
Your viewmodel is intended for use by the view. So its best to take the data, however it come sin, and make it work for the view. As you mention, the "if" will likely be a perf issue. Also, if you use a foreach in a function as you suggest in your comment it is needlessly looping through items when the observable arrays change. I prefer to get my viewmodel in order first, then the user interactions are fast.
2 cents :)
What am I doing wrong? I am trying to create a simple master details view a la the 'canonical MVVM' example.
Here's a simplified example in JSfiddle that doesn't work: http://jsfiddle.net/UJYXg/2/
I would expect to see the name of the selected 'item' in the textbox but instead it says 'observable'?
Here's my offending code:
var list = [ { name: "item 1"} , { name: "Item 2" }];
var viewModel = {
items : ko.observableArray(list),
selectedItem : ko.observable(),
}
viewModel.setItem = function(item) {
viewModel.selectedItem(item);
}
ko.applyBindings(viewModel);
And the HTML
<ul data-bind="foreach: items">
<li>
<button data-bind="click: $root.setItem, text:name"></button>
</li>
</ul>
<p>
<input data-bind="value:selectedItem.name" />
</p>
You are really close. Just need to do value: selectedItem().name or better use the with binding to change your scope. Also, the script that you are referencing is slightly out-of-date (in 2.0 click passes the data as the first arg).
Sample here: http://jsfiddle.net/rniemeyer/acUDH/