Knockoutjs: Keep track of recursion depth? - javascript

I'm using a recursive template which can shortly be summarized as follows:
<script id="template-name" type="text/html">
<div data-bind="foreach: $data">
<span data-bind="text: title"></span>
<div data-bind="template: { name: 'template-name', data: children }"></div>
</div>
</script>
<div data-bind="template: { name: 'template-name', data: $data }"></div>
What I would like to do, however, is to keep track of the current recursion depth somehow. How would I approach this using Knockout?
I've tried a number of approaches, but I'm not familiar enough with Knockout to figure out where I'm making a mistake.
<script id="template-name" type="text/html">
<div data-bind="foreach: $data[0]">
<span data-bind="text: $data[1].toString()"></span>:<span data-bind="text: title"></span>
<div data-bind="template: { name: 'template-name', data: ko.observableArray([children, ++$data[1]]) }"></div>
</div>
</script>
<div data-bind="template: { name: 'template-name', data: ko.observableArray([$data, 0]) }"></div>

The problem was easy to figure out once I found a way to debug the current context for Knockout. You can use either the Knockout Context Debugger or just throw raw json to your document using
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
In the end, the syntax I went with was
<script id="template-name" type="text/html">
<div data-bind="foreach: $data.children">
<span data-bind="text: $parent.depth"></span>:<span data-bind="text: title"></span>
<div data-bind="template: { name: 'template-name', data: { children: children, depth: ($parent.depth + 1) } }"></div>
</div>
</script>
<div data-bind="template: { name: 'template-name', data: { children: $data, depth: 0 } }"></div>

Related

Knockout JS show hide div based on page URL

I want to split two different section based on page URL. I'm not aware how to do it using knockout.
The below example I have tried so far.
<!-- ko if: attr: { href: https://stackoverflow.com } -->
<p>Div 1</p>
<!-- /ko -->
<!-- ko if: attr: { href: https://getbootstrap.com } -->
<p>Div 2</p>
<!-- /ko -->
Any clarification please drop a comment. Thanks in Advance.
You can use the template system to perform this kind of switch :
ko.applyBindings({
stackoverflowData : {
totalQuestions : 321
},
bootstrapData : {
version : '5.0.2'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js"></script>
<div data-bind="template: { name: 'stackoverflow-template', data: stackoverflowData }"></div>
<hr />
<div data-bind="template: { name: 'bootstrapData-template', data: bootstrapData }"></div>
<script type="text/html" id="stackoverflow-template">
stackoverflow specific view <br />
Questions: <span data-bind="text: totalQuestions"></span>
</script>
<script type="text/html" id="bootstrapData-template">
bootstrapData specific view <br />
Version <span data-bind="text: version"></span>
</script>
If you want something dynamic you can create a function that returns the template to use based on the viewmodel.
function getTemplate(url) {
// use reg ex
if (url == 'https://stackoverflow.com')
return 'stackoverflow';
if (url == 'https://getbootstrap.com')
return 'bootstrap';
return null;
}
var websites = [{
url: 'https://stackoverflow.com',
specificData: {
totalQuestions: 321
}
},
{
url: 'https://getbootstrap.com',
specificData: {
version: '5.0.2'
}
}
];
websites.map(function(website) {
website.template = ko.computed(function() {
return getTemplate(this.url);
}, website);
return website;
})
ko.applyBindings({
websites: websites
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js"></script>
<div data-bind="foreach: websites">
<div data-bind="template: { name: (template()+'-template'), data: specificData }">
<hr />
</div>
</div>
<script type="text/html" id="stackoverflow-template">
stackoverflow specific view <br /> Questions: <span data-bind="text: totalQuestions"></span>
</script>
<script type="text/html" id="bootstrap-template">
bootstrapData specific view <br /> Version <span data-bind="text: version"></span>
</script>
To make this work, you'll need to bring the url into your viewmodel. You could do this with a simple custom url bindinghandler. When initialized, it stores the current url in the bound observable (in this case myUrl) and adds an event listener for storing a changed url (in this case I use hash change to be able to make the example work).
Now, in HTML you can show or hide divs based on this observable value. Have a look at the example below.
ko.bindingHandlers.url = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
valueAccessor()(location.href);
window.addEventListener('hashchange', function(ev) {
valueAccessor()(ev.newURL);
});
}
};
ko.applyBindings({
myUrl: ko.observable(),
showDiv1: function() {
window.location.hash = 'div1'
},
showDiv2: function() {
window.location.hash = 'div2'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p>
Current url: <em data-bind="text: myUrl"></em>
</p>
<button data-bind="click: showDiv1">Show Div 1</button>
<button data-bind="click: showDiv2">Show Div 2</button>
<div data-bind="url: myUrl">
<!-- ko if: myUrl() == "https://stacksnippets.net/js#div1" -->
<p>Div 1</p>
<p data-bind="text: myUrl"></p>
<!-- /ko -->
<!-- ko if: myUrl() == "https://stacksnippets.net/js#div2" -->
<p>Div 2</p>
<p data-bind="text: myUrl"></p>
<!-- /ko -->
</div>

Knockout.js dynamically selecting template error: "Cannot find template with ID''"

My target is to show "noSets" template if observableArray length less 0 and render template with item details - "showSets", if observableArray length greater 0. I'd like to use templates for this purpose, but FireBug show error: Cannot find template with ID "templatename".
Here is ViewModel:
function SetsViewModel() {
var self = this;
self.usersets = ko.observableArray();
self.getTemplate = function () {
return self.usersets().length > 0 ? "showSets" : "noSets";
}
}
$(document).ready(function () {
ko.applyBindings(new SetsViewModel(), document.getElementById('user_sets'));
});
And here is HTML markup:
<div data-bind="template: { name: $root.getTemplate, foreach: usersets }" id="user_sets">
<script type="text/html" id="noSets">
<p>You do not have items yet.</p>
</script>
<script type="text/html" id="showSets">
<div class="block">
<input type="hidden" data-bind="value: $data.SetId" />
<div class="fav" data-bind="css: { fullop: $data.IsFavorite == true }">
<img alt="" src="img/fav.png" data-bind="click: $root.setFavorite">
</div>
<div>
<img alt="" data-bind="attr: { src: $data.SetImg }">
</div>
<div class="txt">
<h3 data-bind="text: $data.SetName, click: $root.go"></h3>
<p><span data-bind="text: $data.ItemsNumber + ' вещей,'"></span><span data-bind=" text: ' общая цена ' + $data.SetPrice + ' руб'"></span></p>
</div>
</div>
</script>
</div>
How can I fix it?
You can declare the templates outside the div that you're binding to as a work around. As #JeffMercado states:
The actual problem was that since the user_sets uses a template binding, the body is discarded (and the templates along with it)
function SetsViewModel() {
var self = this;
self.usersets = ko.observableArray([{SetId: 1, SetName: 'Name 1'}]);
self.getTemplate = function () {
return self.usersets().length > 0 ? "showSets" : "noSets";
}
}
$(document).ready(function () {
ko.applyBindings(new SetsViewModel(), document.getElementById('user_sets'));
});
<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.2.0/knockout-min.js"></script>
<script type="text/html" id="noSets">
<p>You do not have items yet.</p>
</script>
<script type="text/html" id="showSets">
<div class="block">
<input data-bind="value: $data.SetId" />
<div class="txt">
<h3 data-bind="text: 'Set name:' + $data.SetName"></h3>
</div>
</div>
</script>
<div data-bind="template: { name: getTemplate, foreach: usersets }" id="user_sets">
</div>
I think named templates are potentially the wrong approach to this specific issue - they're more designed for when you want a different template for each item, which isn't the case here. Instead, it would be more appropriate to show an entirely different div when there are 0 items:
<div data-bind="visible: usersets().length == 0">
You have no sets
</div>
<div data-bind="visible: usersets().length > 0, foreach: usersets" id="user_sets">
<div class="block">
<input type="hidden" data-bind="value: $data.SetId" />
<div class="fav" data-bind="css: { fullop: $data.IsFavorite == true }">
<img alt="" src="img/fav.png" data-bind="click: $root.setFavorite">
</div>
<div>
<img alt="" data-bind="attr: { src: $data.SetImg }">
</div>
<div class="txt">
<h3 data-bind="text: $data.SetName, click: $root.go"></h3>
<p><span data-bind="text: $data.ItemsNumber + ' вещей,'"></span><span data-bind=" text: ' общая цена ' + $data.SetPrice + ' руб'"></span></p>
</div>
</div>
</div>

As binding knockout undefined nested foreach

So I have html:
<div class="body" data-bind="foreach: { data: Sections }">
<span data-bind="text: '(' + OrderQualifier + ') ' + Text">
</span>
<p data-bind="foreach: { data: Children, as: 'child' }">
<fieldset class="section-edit" data-bind="visible: IsEditing">
<input type="text" data-bind="attr: {value: child.EditedText}" /><!-- child is undefined here even though I have it as my as binding on the above foreach-->
<button data-bind="event: {click: $root.addEdit}">Submit</button>
</fieldset>
</p>
</div>
I tried to do this without the as binding but it was pulling the value from the parent section which also has an EditedText property and get the same result using $data.
The data (Sections) i'm trying to bind looks like:
[
{
"SectionID":1,
"Text":"Parent text",
"Html":null,
"OrderQualifier":"1",
"IsUserCreated":false,
"Children":[
{
"SectionID":2,
"Text":"Child text",
"Html":null,
"OrderQualifier":"1",
"IsUserCreated":false,
"EditCount":0,
"ExplanationCount":0,
"EvidenceCount":0,
"IsEditing":true,
"EditedText":"Child text"
}
],
"EditCount":0,
"ExplanationCount":0,
"EvidenceCount":0,
"IsEditing":true,
"EditedText":"Parent text"
}
]
Any ideas?
Or use virtual elements if you want to keep using the <p> tags
<!-- ko foreach: { data: Children, as: 'child' } -->
<p>
...
</p>
<!-- /ko -->
http://jsfiddle.net/cvtw3b2h/2/
Pretty weird, but try changing
<p data-bind="foreach: { data: Children, as: 'child' }">
....
</p>
to
<div data-bind="...">...</div>
http://jsfiddle.net/cvtw3b2h/1/

Knockout binding, REUSEABLE code for JSON data

I am not able to figure out how to continue for the following data-bind.
I am having JSON data want to display in form of vertical grid. So I am fetching all the key values in one array(say columnName) and using to display column names. Now I am trying to display the values of JSON data using the array(columnName).
Please check the code, and let me know the solution.
JavaScript Code:
var _data = new Array({ firstname: 'Name1', lastname: 'LastName1' }, { firstname: 'Name2', lastname: 'Lastname2' });
var getColumnNames = new Array();
for (key in _data[0]) {
getColumnNames.push(key);
}
// Here's my data model
var ViewModel = function () {
this.coulmnNames = ko.observableArray(getColumnNames);
this.keyValue = ko.observableArray(_data);
};
ko.applyBindings(new ViewModel()); // This makes Knockout get to work
The Actual code is below:
<div data-bind="foreach: coulmnNames" style="display: inline-block;">
<div data-bind="text: $data"></div>
</div>
<div data-bind="foreach: keyValue" style="display: inline-block;">
<div style="display: inline-block;">
<div data-bind="text: firstname"></div>
<div data-bind="text: lastname"></div>
</div>
</div>
Want to code like this:
<div data-bind="foreach: coulmnNames" style="display: inline-block;">
<div data-bind="text: $data"></div>
</div>
<div data-bind="foreach: keyValue" style="display: inline-block;">
<div style="display: inline-block;">
<div data-bind="foreach: $parent.coulmnNames">
<div data-bind="text: ???????"></div>
</div>
</div>
</div>
I am just trying to write a reuse-able code, just have to vary the JSON data.
Thanks in advance.
You are almost there:
you need to reference the "current data" from the outer loop in the inner loop with $parent
you can use the indexer syntax to dynamically access a property in your inner loop with writing $parent[$data] where $data is the actual column name
So your binding should look like:
<div data-bind="foreach: keyValue" style="display: inline-block;">
<div style="display: inline-block;">
<div data-bind="foreach: $parent.coulmnNames">
<div data-bind="text: $parent[$data]"></div>
</div>
</div>
</div>
Demo JSFiddle.
You can read more about the $parent and $data binding context properties in the documentation.

Knockoutjs: dynamic content and applyBindings

I am "dynamically" populating my page like this:
<script type="text/html" id="ContainerTemplate">
<span data-bind="template: {
name: contentTemplate,
data: contentData }"></span>
</script>
<script type="text/html" id="fooTemplate">
<span data-bind="text: barAttribute"></span>
</script>
<button data-bind="click: complete">complete</button>
Hello
<span data-bind="template: { name: 'ContainerTemplate', foreach: myContents }"></span>
!
ViewModel:
var viewModel = {
myContents: ko.observableArray([]),
complete: function() {
viewModel.myContents.push({
contentTemplate:'fooTemplate',
contentData:{barAttribute:'world'}});
}
};
ko.applyBindings(viewModel);
A particularity is that template names are dynamic. It seems to work like this (you can try it on http://jsfiddle.net/hPQNx/ ), but I wonder if I'm doing things correctly. Some template features like root or parent don't seem to be working.
Should I manually re-call applyBindings at some point ? I have seen this must be done on the related DOM nodes, but how can I access those nodes in my setup ?
I added a property to your view model and showed how you can add a root property and reference it with $root and $parent can work here in this fiddle.
var viewModel = {
a: ko.observable('foo'),
myContents: ko.observableArray([]),
complete: function() {
viewModel.myContents.push({
contentTemplate: 'fooTemplate',
b: 'goo',
contentData: {
barAttribute: 'world'
}
});
}
};
ko.applyBindings(viewModel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.0.0/knockout-min.js"></script>
<script type="text/html" id="ContainerTemplate">
<span data-bind="template: {
name: contentTemplate,
data: contentData }"></span>
</script>
<script type="text/html" id="fooTemplate">
<span data-bind="text: barAttribute"></span>
<div data-bind="text: $root.a"></div>
<div data-bind="text: $parent.b"></div>
</script>
<button data-bind="click: complete">complete</button>
Hello
<span data-bind="template: { name: 'ContainerTemplate', foreach: myContents }"></span>
!

Categories

Resources