Goal:
use KO to show/hide folder, sub-folder, and files, as recursive UL LI list. When a user click on the folders, the child items under that folder will toggle hide/show.
Problem:
The recursive part is ok. But it does not do toggle. console.log says error that 'show' is undefined. Any idea what went wrong ?
Code
<script type="text/javascript">
$(function() {
ko.applyBindings(viewModel,document.getElementById('resources-panel'));
});
var viewModel = {
treeRoot: ko.observableArray()
};
var FileElement = function(ppp_name, ppp_type, ppp_children) {
var self = this;
self.ppp_children = ko.observableArray(ppp_children);
self.ppp_name = ko.observable(ppp_name);
self.ppp_type = ko.observable(ppp_type);
self.show = ko.observable(false);
self.toggle=function() {
self.show(!self.show());
}
}
var tree = [
new FileElement("IT Dept", "folder",[
new FileElement("IT Overview.docx", "file",[]),
new FileElement("IT Server1", "folder",[
new FileElement("IT Server1 Configuration Part 1.docx", "file", []),
new FileElement("IT Server1 Configuration Part 2.docx", "file", []),
]),
new FileElement("IT Server2", "folder",[])
]),
new FileElement("HR Dept", "folder", [])
];
viewModel.treeRoot(tree);
</script>
<script id="FileElement" type="text/html">
<ul>
<li>
<a href="#" data-bind="click: toggle" class="action-link"><br/>
<span data-bind="text: ppp_name"></span>
</a>
<ul data-bind="template: { name: 'FileElement', slideVisible: show, foreach: ppp_children }" ></ul>
</li>
</ul>
</script>
<div id="resources-panel" data-bind="template: { name: 'FileElement', slideVisible: show, foreach: $data.treeRoot }"></div>
Your top level binding context is the treeRoot, and treeRoot doesn't have a "show" property it's just a simple array so you probably want to remove that first show binding altogether
<div id="resources-panel" data-bind="template: { name: 'FileElement', foreach: $data.treeRoot }"></div>
Then within the FileElement template you'll want to move the show binding to the outside of the template binding like f_martinez suggested
<ul data-bind="slideVisible: show, template: { name: 'FileElement', foreach: ppp_children }" ></ul>
Here's an example jsFiddle
Related
How to use unstructured data with knockoutjs mapping plugin?
For example source json:
[
{
"id": 1,
"name": "Store #1",
"address": "City #1"
},
{
"id": 2,
"name": "Store #2"
}
]
Store #2 without address.
My template:
<ul data-bind='foreach: data'>
<li data-bind='with: id'>
<a href data-bind='text: name, click: function () { $parent.view($data, $index()) }'>
</a>
<span data-bind='text: address'></span>
</li>
</ul>
My viewModel
Module.store = function () {
var self = this;
self.data = ko.mapping.fromJS([]);
self.init = function () {
$.getJSON('json/stores.json', function (stores) {
ko.mapping.fromJS(stores, self.data);
});
};
};
If I run this code, I get error:
Uncaught ReferenceError: Unable to process binding "text: function
(){return address }" Message: address is not defined
For Store #2
How can I set null or empty string for Store #2 address property?
If your view shows the address, then your viewmodel must contain that property.
Make a viewmodel for the individual stores:
Module.Store = function (data) {
this.id = null;
this.name = null;
this.address = null;
ko.mapping.fromJS(data, {}, this);
}
and a use mapping definition (see documentation) in your store list:
Module.StoreList = function () {
var self = this,
mappingDefinition = {
create: function (options) {
return new Module.Store(options.data);
}
};
self.stores = ko.observableArray();
self.viewStore = function (store) {
// ...
};
self.init = function () {
$.getJSON('json/stores.json', function (stores) {
ko.mapping.fromJS(stores, mappingDefinition, self.stores);
});
};
};
Modified view (as a general rule, try to avoid inline functions in the view definition):
<ul data-bind='foreach: stores'>
<li>
<a href data-bind='text: name, click: $parent.viewStore'></a>
<span data-bind='text: address'></span>
</li>
</ul>
It doesn't seems to me that knockout-mapping-plugin has that functionality out of the box. Probably you should consider to try another workaround for that issue, I can see at least two:
1) returning json with null from server
2) displaying that span conditionally like:
<ul data-bind='foreach: data'>
<li data-bind='with: id'>
<a href data-bind='text: name, click: function () { $parent.view($data, $index()) }'>
</a>
<!-- ko if: address -->
<span data-bind='text: address'></span>
<!-- /ko -->
</li>
</ul>
I want to clik ul li items and collapse and open them. Working code is here
var viewModel = {
treeRoot: ko.observableArray()
};
var TreeElement = function(name, children) {
var self = this;
self.children = ko.observableArray(children);
self.name = ko.observable(name);
}
var tree = [
new TreeElement("Russia", [
new TreeElement("Moscow")
]),
new TreeElement("Germany"),
new TreeElement("United States",
[
new TreeElement("Atlanta"),
new TreeElement("New York", [
new TreeElement("Harlem"),
new TreeElement("Central Park")
])
]),
new TreeElement("Canada", [
new TreeElement("Toronto")
])
];
viewModel.treeRoot(tree);
ko.applyBindings(viewModel);
html like this
<script id="treeElement" type="text/html">
<li>
<span data-bind="text: name"></span>
<ul data-bind="template: { name: 'treeElement', foreach: children }">
</ul>
</li>
</script>
<ul data-bind="template: { name: 'treeElement', foreach: $data.treeRoot }"></ul>
You need to introduce a flag isCollapsed on your TreeElement which you can toogle from a click binding event handler.
And based on that isCollapsed you need to filter out your children collection with a help of a computed observable:
var TreeElement = function(name, children) {
var self = this;
self.children = ko.observableArray(children);
self.isCollapsed = ko.observable();
self.collapse = function() {
self.isCollapsed(!self.isCollapsed());
}
self.visibleChildren = ko.computed(function(){
if (self.isCollapsed())
return [];
return children;
});
self.name = ko.observable(name);
}
And you need to update your template with the click binding handler and use the visibleChildren instead of the children collection:
<script id="treeElement" type="text/html">
<li>
<span data-bind="text: name, click: collapse"></span>
<ul data-bind="template: { name: 'treeElement', foreach: visibleChildren }">
</ul>
</li>
</script>
Demo JSFiddle.
I have a list of products where the name is a link to the product's details view. The list of products is the "Results" view
Samsumg
iPhone
When the user clicks on a product, the "Details" template is shown, and the "Results" template is not shown; at least that is the behavior that I want.
I am using the following code to accomplish this behavior, and have the jsFiddle here http://jsfiddle.net/justinnafe/mLf5G/:
<div data-bind="template: displayMode"></div>
<script type="text/html" id="Result">
<ul data-bind="foreach: products">
<li></li>
</ul>
</script>
<script type="text/html" id="Details">
<p data-bind="text: name"></p>
<p data-bind="text: description"></p>
</script>
and the javascript:
var view = {
name: "Result"
};
var initialProducts = [{
name: "Samsumg",
description: "The best phone"
},{
name: "iPhone",
description: "The other best phone"
}];
var viewModel = (function (){
var products = ko.observableArray(initialProducts),
displayMode = ko.observable(view),
switchDisplayMode = function(item){
if (displayMode() == 'Result') {
displayMode({ name: "Details", data: item });
}
else {
displayMode({ name: "Result", data: item });
}
};
return {
products: products,
displayMode: displayMode,
switchDisplayMode: switchDisplayMode
};
})();
ko.applyBindings(viewModel);
I am trying to pass that product to the Details template, but have been unsuccessful. Any clues or tips would be helpful.
I am currently getting a "ReferenceError: products is not defined" error when I click on a link, but not sure how to fix it. Maybe if I fix that error, the switching views will behave as expected.
In your function to switch the template, you are forgetting that your displayMode observable is holding an object - not a string value.
So inside switchDisplayMode, displayMode() = { name: 'Result' }. Switching that to displayMode().name fixes the problem. See updated fiddle
I have an issue with Knockout.js . What I try to do is filter a select field. I have the following html:
<select data-bind="options: GenreModel, optionsText: 'name', value: $root.selectedGenre"></select>
<ul data-bind="foreach: Model">
<span data-bind="text: $root.selectedGenre.id"></span>
<li data-bind="text: name, visible: genre == $root.selectedGenre.id"></li>
</ul>
And the js:
var ViewModel = function (){
self.selectedGenre = ko.observable();
self.Model = ko.observableArray([{
name: "Test",
genre: "Pop"
}
]);
self.GenreModel = ko.observableArray([
{
name: "Pop",
id: "Pop"
},
{
name: "Alle",
id: "All"
}
]);
};
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
JSFiddle: http://jsfiddle.net/CeJA7/1/
So my problem is now that the select list does not update the binding on the span inside the ul and I don't know why...
The value binding should update the property selectedGenre whenever the select value changes, shouldn't it?
Any ideas are welcome.
There are a lot of issues in your code:
1) self is not a magical variable like this. It's something people use to cope with variable scoping. Whenever you see self somewhere in a JavaScript function be sure there's a var self = this; somewhere before.
2) KnockoutJS observables are not plain variables. They are functions (selectedGenre = ko.observable()). ko.observable() returns a function. If you read the very first lines of documentation regarding observables you should understand that access to the actual value is encapsulated in this retured function. This is by design and due to limitations in what JavaScript can and cannot do as a language.
3) By definition, in HTML, <ul> elements can only contain <li> elements, not <span> or anything else.
Applying the above fixes leads to this working updated sample:
HTML:
<select data-bind="options: GenreModel, optionsText: 'name', value: selectedGenre"></select>
<span data-bind="text: $root.selectedGenre().id"></span>
<ul data-bind="foreach: Model">
<li data-bind="text: name, visible: genre == $root.selectedGenre().name"></li>
</ul>
JavaScript:
var ViewModel = function (){
var self = this;
self.selectedGenre = ko.observable();
self.Model = ko.observableArray([
{
name: "Test",
genre: "Pop"
}
]);
self.GenreModel = ko.observableArray([
{
name: "Pop",
id: "Pop"
},
{
name: "Alle",
id: "All"
}
]);
};
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
The Knockout mapping plugin documentation has a section entitled "Uniquely identifying objects using “keys”". This describes how to update part of an object and then only update that part of the display rather than completely replacing the display of all properties of a partially-modified object. That all works splendidly in their simple example, which I have slightly modified here to make my question more clear. My modifications were to:
Replace the object with a corrected name after a 2 second delay.
Highlight the unchanging part of the display, so you can see that it is actually not replaced when the update happens.
1. Simple object (jsFiddle)
<h1 data-bind="text: name"></h1>
<ul data-bind="foreach: children">
<li><span class="id" data-bind="text: id"></span> <span data-bind="text: name"></span></li>
</ul>
<script>
var data = {
name: 'Scot',
children: [
{id : 1, name : 'Alicw'}
]
};
var mapping = {
children: {
key: function(data) {
console.log(data);
return ko.utils.unwrapObservable(data.id);
}
}
};
var viewModel = ko.mapping.fromJS(data, mapping);
ko.applyBindings(viewModel);
var range = document.createRange();
range.selectNode(document.getElementsByClassName("id")[0]);
window.getSelection().addRange(range);
setTimeout(function () {
var data = {
name: 'Scott',
children: [
{id : 1, name : 'Alice'}
]
};
ko.mapping.fromJS(data, viewModel);
}, 2000);
</script>
But what isn't clear to me is how I would achieve the same behavior for a more complex nested data structure. In the following example, I took the above code and wrapped the data in a list. I would like this to behave the same as above, but it doesn't. The whole display is redone because of the change in one property. You can see this because, unlike the above example, the highlighting is lost after the data is updated.
2. More complex nested object (jsFiddle)
<!-- ko foreach: parents -->
<h1 data-bind="text: name"></h1>
<ul data-bind="foreach: children">
<li><span class="id" data-bind="text: id"></span> <span data-bind="text: name"></span></li>
</ul>
<!-- /ko -->
<script>
var data = {
parents: [
{
name: 'Scot',
children: [
{id : 1, name : 'Alicw'}
]
}
]
};
var mapping = {
children: {
key: function(data) {
console.log(data);
return ko.utils.unwrapObservable(data.id);
}
}
};
var viewModel = ko.mapping.fromJS(data, mapping);
ko.applyBindings(viewModel);
var range = document.createRange();
range.selectNode(document.getElementsByClassName("id")[0]);
window.getSelection().addRange(range);
setTimeout(function () {
var data = {
parents: [
{
name: 'Scott',
children: [
{id : 1, name : 'Alice'}
]
}
]
};
ko.mapping.fromJS(data, viewModel);
}, 2000);
</script>
So basically what I'm asking is, how can I make the second example work like the first, given the more nested data structure? You can assume that ids are unique for each child (so if I added another parent besides Scott, his children would start with id=2, etc.).
Interesting observation there and nice write-up. It appears to work if you define a key on the parent as well as the child. Try this fiddle:
http://jsfiddle.net/8QJe7/6/
It defines instantiable view model functions for the parents and children, where the parent constructor does its child mappings.