Master Details in Knockout JS - javascript

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/

Related

Knockout Js: foreachBinding

I have object models which is of format below,
models:
[
0: {
[functions]: ,
__proto__: { },
Main: "Sample",
sub: [
0: " Sub1",
1: " Sub2",
2: " sub3",
3: " sub4",
4: " sub5",
5: " sub6",
6: " sub7",
7: " sub8",
length: 8
]
},
1: { },
2: { },
I have listed first three elements of the object as sample. Also first element is elaborated. I am trying to list this as below.
Main 1
sub details
Main 2
sub details
I have tried like, I could not figure out why my data binding is not working. Following code not displaying anything in the screen.
<td>
<ul class="tree">
<li data-bind="foreach: models" >
<span data-bind="text: $data[$index].main"></span>
<ul data-bind="foreach: subDetails in models.sub">
<li><span data-bind="text: $data[$index].sub"></span></li>
</ul>
</li>
</ul>
</td>
Please check my fiddle here. Any help would be appreciated.
There are a lot of issues with your fiddle. Here is a list -
You have written ko.applybindigs which should have been ko.applyBindings
The model should be a part of your viewModel, so you will need to attach that variable to your viewModel. Like var self = this; self.model = [].... Declaring it just as a private var will not expose it to the knockout bindings and hence that will not work.
Once inside of a foreach binding, you don't need $index to access the objects inside of the array, just accessing them by their names is all you need. So <span data-bind="text: $data[$index].Main"></span> is replaced by <span data-bind="text: Main"></span>
You have another foreach nested inside the outer one, there too you have used $index accessor which really isn't necessary. To add to that <ul data-bind="foreach: subdetails in model.sub"> can easily be replaced with <ul data-bind="foreach: Sub"> (please also notice the typo in sub which should have been Sub)
Corrected fiddle here

Knockout JS parameters with Grapnel routing

I'm working on a knockout app that now requires routing to be implemented. Grapnel looks like a great solution however I've hit a bit of a brick wall with it.
Knockout click events pass the current 'view model' to whatever function you define in your view - as documented here. As mentioned this is really useful when working with collections and the app I mention uses this quite a lot.
I'm looking for a way of being able to make use of this from within grapnel routes however I'm lost when it comes to a solution.
I've put together a rather simple fiddle to try to help explain things:
https://jsfiddle.net/nt0j49x7/4/
HTML
<div id="app">
<ul class="playlist" data-bind="foreach: albumList">
<li class="album">
<a href="" data-bind="click: $root.showAlbumInfo">
<span data-bind="text: title"></span> -
<strong data-bind="text: artist"></strong>
</a>
</li>
</ul>
<div data-bind="with: selectedAlbum">
<img data-bind="attr:{src: coverImg}" />
<div>
<span data-bind="text: title"></span> - <span data-bind="text: artist"></span>
<a data-bind="attr:{href: spotifyLink}">Listen on spotify</a>
</div>
</div>
</div>
Javascript
var appView = {
albumList: ko.observableArray([
{id: 1, title:'Helioscope' , artist: 'Vessels', coverImg: 'http://ecx.images-amazon.com/images/I/91nC-KVZBBL._SX466_.jpg', spotifyLink: 'https://open.spotify.com/album/3dARFB98TMzKLHwZOgKZhE'},
{id: 2, title:'Dilate' , artist: 'Vessels', coverImg: 'http://ecx.images-amazon.com/images/I/31AvNaBtnpL._SX466_PJautoripBadge,BottomRight,4,-40_OU11__.jpg', spotifyLink: 'https://open.spotify.com/album/7yapNLdtqlYiGFbuEuGRIt'},
{id: 3, title:'White fields and open devices' , artist: 'Vessels', coverImg: 'http://ecx.images-amazon.com/images/I/918vEDkM5PL._SX522_PJautoripBadge,BottomRight,4,-40_OU11__.jpg', spotifyLink: 'https://open.spotify.com/album/4kB1vlgei2DvIweeBoiNVu'}
]),
selectedAlbum: ko.observable(),
showAlbumInfo: function(album, event) {
// knockout supplies the clicked model value as the first parameter
appView.selectedAlbum(album);
}
};
var routes = {
'album/:id' : function(req, event){
// Any ideas on how to pass the 'album' object knockout is
// passing to the appView.showAlbumInfo method into this
// route handler? I can use the ID request param to
// get the model from albumList and set the selectedAlbum
// but that isn't what I'm trying to achieve.
}
};
//var router = new Grapnel(routes);
ko.applyBindings(appView, document.getElementById('app'));
I have been using knockout for year but only took a quick look at Grapnel. I don't see a way of passing objects as parameters. But this is obviously a single page app approach and you have declare your view model as a global. So you can access the "appView" within the router code:
var router = new Grapnel();
//must include the id in the route for function to fire on id change
router.navigate('/album/' + album.id);
console.info(appView.selectedAlbum());
}
);
Then in your viewModel event you can navigate after you set the observable.
showAlbumInfo: function(album, event) {
// knockout supplies the clicked model value as the first parameter
appView.selectedAlbum(album);
router.navigate('/album/' + album.id);
}
fiddle:example
Not sure what your app is going to be but Angular js will do what you are trying to do all in one package with observables and routeing. You won't need knockout.

ngRepeat Filter by Array name?

I have the following JSON structure in my Angular app:
Sample: http://pastie.org/pastes/9476207/text?key=u6mobe15chwyiz1jakn0w
And the following HTML which is displaying every product in the parent array:
<div class="bb-product-grid" ng-repeat="brand in notebooks">
<ul ng-repeat="products in brand">
<li ng-repeat="Notebook in products">
<span class="product-title">{{Notebook.Model}}</span>
<span class="product-description"><p>{{Notebook.Description}}</p></span>
<span class="product-image">image</span>
<span class="product-add-to-bundle"><button>Add to bundle</button></span>
<span class="product-price">{{Notebook.Price}}<sub>pcm</sub></span>
</li>
</ul>
</div>
I want to be able to filter the products by the brand names (array names, Acer, Apple etc.).
By default, only the 'Apple' products will be visible. You will then click a button to change the visible results to reflect what brand you've selected. So if you then select HP, you'll only see 'HP' products.
I'm a little stuck on this because the JSON structure is pretty nested, if it was 1/2 levels I'd be able to cope but I can't figure this out, I seem to only show all the products or break the app. Any advice on the best-practise approach to this (I'm guessing I might have to create a custom filter?) will be greatly appreciated!
If you can get your data to be in this format:
$scope.data = [
{
brand: 'Acer',
laptops: ['acer_1', 'acer_2', 'acer_3', 'acer_4', 'acer_5']
},
{
brand: 'Apple',
laptops: ['apple_1', 'apple_2', 'apple_3', 'apple_4', 'apple_5']
},
{
brand: 'Asus',
laptops: ['asus_1', 'asus_2', 'asus_3', 'asus_4', 'asus_5']
},
{
brand: 'HP',
laptops: ['hp_1', 'hp_2', 'hp_3', 'hp_4', 'hp_5']
},
{
brand: 'Lenovo',
laptops: ['lenovo_1', 'lenovo_2', 'lenovo_3', 'lenovo_4', 'lenovo_5']
},
{
brand: 'Toshiba',
laptops: ['toshiba_1', 'toshiba_2', 'toshiba_3', 'toshiba_4', 'toshiba_5']
}
];
Then you can use have a filter like so:
$scope.search = {
brand: 'HP'
};
With HTML:
<select ng-model="search.brand">
<option ng-repeat="company in data">{{company.brand}}</option>
</select>
<ul ng-repeat="company in data | filter:search">
<li><b ng-bind="company.brand"></b></li>
<ul ng-repeat="laptop in company.laptops">
<li ng-bind="laptop"></li>
</ul>
</ul>
Here is a jsfiddle.
brands is an array of objects with the brand names as keys. It doesn't make sense to use filters to solve this problem ... however it's easy to get the products if you have the key name. Imagine you have searchName = 'Apple' and obj = [{Apple: []}, {Acer: []}]. You could just use obj[0].Apple.
this.filterItem = "Apple";
this.productsToShow = productObject[0][this.filterItem];
<select ng-model=ctrl.filterItem><!-- brand names go here --></select>
<ul ng-repeat="product in ctrl.productsToShow">
Don't use filter, when only ever one is supposed to be visisble. Instead, skip the iteration over the brands, and instead just iterate over the selected brand. All you need to do is then make sure you set selectedBrand to the correct subset of your data structure.
Fiddle: http://jsfiddle.net/5wwboysu/2/
Show brand:
<span ng-repeat="(name,brand) in notebooks.Notebooks[0]" ng-click="selected(brand)">
{{name}} - <br>
</span>
<ul ng-repeat="products in selectedBrand">
<!-- snip -->
</ul>
$scope.selected = function(brand) {
$scope.selectedBrand = brand;
};
However, if you have control over the JSON, I'd try to return it in a format better suited for what you're trying to do.
Add this filter which will take a brand:
<ul ng-repeat="brand in notebooks| filter:filterBrand">
in controller
$scope.SelectedBrandName= "Apple";
$scope.filterBrand = function(brand){
if(brand.Name == $scope.SelectedBrandName)
return true;
return false;
};
This will look at each brand and only allow brands that return true. If you have a button that changes the $scope.SelectedBrandName value to a different name, it will automatically filter through the ng-repeat to only brands that match.

KnockoutJS, how to update view model using HTML5 content editable?

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.

knockoutjs click binding within foreach binding

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/

Categories