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
First to say that I'm new to JQ and JS and I need a little help.
How can I print this array
var menu = [
{
name: 'Item1',
submenuName: 'submenu-1',
subMenu:[
{
name: 'Item1_1',
submenuName: 'submenu-1-1',
subMenu:[
{
name: 'Item1_1_1',
submenuName: 'submenu-1-1-1',
subMenu:[
{
name: 'Item1_1_1_1',
submenuName: 'submenu-1-1-1-1',
subMenu:[
{
name: 'Item1_1_1_1_1',
//submenuName: 'submenu-1-1-2',
},
]
},
{
name: 'Item1_1_1_2',
//submenuName: 'submenu-1-1-2',
}
]
},
{
name: 'Item1_1_2',
//submenuName: 'submenu-1-1-2',
},
]
},
{
name: 'Item1_2',
//submenuName: 'submenu-1-2',
},
{
name: 'Item1_3',
//submenuName: 'submenu-1-3',
}
]
},
{
name: 'Item2',
//submenuName: 'submenu-2',
}
];
to look in the html like that
<ul data-menu="main" class="menu__level">
<li class="menu__item"><a class="menu__link" data-submenu="submenu-1" href="#">Item1</a></li>
<ul data-menu="submenu-1" class="menu__level">
<li class="menu__item"><a class="menu__link" data-submenu="submenu-1-1" href="#">Item1_1</a></li>
<ul data-menu="submenu-1-1" class="menu__level">
<li class="menu__item"><a class="menu__link" data-submenu="submenu-1-1-1" href="#">Item1_1_1</a></li>
<ul data-menu="submenu-1-1-1" class="menu__level">
<li class="menu__item"><a class="menu__link" data-submenu="submenu-1-1-1-1" href="#">Item1_1_1_1</a></li>
<ul data-menu="submenu-1-1-1-1" class="menu__level">
<li class="menu__item"><a class="menu__link" href="#">Item1_1_1_1_1</a></li>
</ul>
<li class="menu__item"><a class="menu__link" href="#">Item1_1_1_2</a></li>
</ul>
<li class="menu__item"><a class="menu__link" href="#">Item1_1_2</a></li>
</ul>
<li class="menu__item"><a class="menu__link" href="#">Item1_2</a></li>
<li class="menu__item"><a class="menu__link" href="#">Item1_3</a></li>
</ul>
<li class="menu__item"><a class="menu__link" href="#">Item2</a></li>
</ul>
I know that my question is stupid, but I can't figure it out how to make it. This is going to be an drop-down menu.
With recursion:
function getMenuHTML(arr, name) {
var result = '<ul data-menu="' + (name || 'main') + '" class="menu__level">';
for (var i = 0; i < arr.length; i++) {
result += '<li class="menu__item"><a class="menu__link"' + (arr[i].submenuName ? 'data-submenu="' + arr[i].submenuName + '"' : '') + ' href="#">' + arr[i].name + '</a></li>';
if (arr[i].subMenu && arr[i].subMenu.length) {
result += getMenuHTML(arr[i].subMenu, arr[i].submenuName);
}
}
result += "</ul>";
return result;
}
var menu=[{name:'Item1',submenuName:'submenu-1',subMenu:[{name:'Item1_1',submenuName:'submenu-1-1',subMenu:[{name:'Item1_1_1',submenuName:'submenu-1-1-1',subMenu:[{name:'Item1_1_1_1',submenuName:'submenu-1-1-1-1',subMenu:[{name:'Item1_1_1_1_1'},]},{name:'Item1_1_1_2'}]},{name:'Item1_1_2'},]},{name:'Item1_2'},{name:'Item1_3'}]},{name:'Item2'}];
var html = getMenuHTML(menu);
document.getElementById('resultplain').value = html;
document.getElementById('result').insertAdjacentHTML('beforeend', html);
<nav id="result"></nav>
<textarea id="resultplain" style="height: 250px;width:100%"></textarea>
This function will constract that menu:
// createMenu takes an array of objects m and a menuname to be set as data-menu attribute
function createMenu(m, menuname){
// create the ul
var ul = document.createElement("ul");
// if we are at depth 0 then the data-menu should be 'main', else it should be 'submenu-???' depending on the depth
ul.setAttribute("data-menu", menuname);
ul.className = "menu__level";
// now for every object in the menu (m array), create an li element
m.forEach(function(e){
// create the li
var li = document.createElement("li");
li.className = "menu__item";
// create the link
var a = document.createElement("a");
// set its data-submen attribute and href and text content as well
/******* ONLY IF IT HAS A SUB-MENU *************/
if(e.subMenu)
a.setAttribute("data-submenu", e.submenuName);
a.setAttribute("href", "#");
a.textContent = e.name;
// append the link to the li
li.appendChild(a);
// append the li to the ul
ul.appendChild(li);
// if this object contains a subMenu the create it using createMenu with e.submenuName as menuname
if(e.subMenu)
// if you want it to be appended to the li instead of its parent change 'ul.ap...' to 'li.ap...'
ul.appendChild(createMenu(e.subMenu, e.submenuName));
});
// return the ul element
return ul;
}
Note: It should be called like this createMenu(menu, "main"); where the first parameter is your array of objects, and the second ("main") is the data-menu of the first ul.
Note2: createMenu returns an ul element, so you can append it to another element (container) like this:
// create the menu
var myMenuUl = createMenu(menu, "main");
// append it to an element (the body for example)
document.body.appendChild(myMenuUl);
This is an alternative solution using two Javascript template engines and template composition (a form of recursion): jsRender and Handlebars. jsRender detects jQuery and loads as a jQuery plugin (they both can be used also without jQuery).
The data-submenu attribute is added to menu__link and the has-submenu class is added to menu__item only when appropriate.
Note: I have modified the original, target HTML not to include the element ul as child of another element ul (see the first <li class="menu__item"> for example), that would be invalid HTML (check the W3C validator).
// Get compiled jsRender template.
var tmpl = $.templates("#entryPointTemplate");
// Get compiled Handlebars template.
var tmplHandlebarsEntryPoint = $("#entryPointTemplate-handlebars").html()
var tmplHandlebarsRenderFunction = Handlebars.compile(tmplHandlebarsEntryPoint);
// Register the subMenuTemplate Handlebars partial.
Handlebars.registerPartial("subMenuTemplate", $("#subMenuTemplate-handlebars").html());
var menu = [ // Source data.
{
name: 'Item1',
submenuName: 'submenu-1',
subMenu: [{
name: 'Item1_1',
submenuName: 'submenu-1-1',
subMenu: [{
name: 'Item1_1_1',
submenuName: 'submenu-1-1-1',
subMenu: [{
name: 'Item1_1_1_1',
submenuName: 'submenu-1-1-1-1',
subMenu: [{
name: 'Item1_1_1_1_1',
//submenuName: 'submenu-1-1-2',
}, ]
}, {
name: 'Item1_1_1_2',
//submenuName: 'submenu-1-1-2',
}]
}, {
name: 'Item1_1_2',
//submenuName: 'submenu-1-1-2',
}, ]
}, {
name: 'Item1_2',
//submenuName: 'submenu-1-2',
}, {
name: 'Item1_3',
//submenuName: 'submenu-1-3',
}]
}, {
name: 'Item2',
//submenuName: 'submenu-2',
}
];
// Wrap the data in this way for convenience with the template engines, see: `{{for data}}` and `{{#each data}}`.
var data = {
data: menu
}
// Render the jsRender template using data.
var htmlJsrender = tmpl.render(data);
// Render the Handlebars template using data.
var htmlHandlebars = tmplHandlebarsRenderFunction(data);
// Insert the rendered (by jsRender) HTML into the DOM.
$("#result-jsrender").html(htmlJsrender);
// Insert the rendered (by Handlebars) HTML into the DOM.
$("#result-handlebars").html(htmlHandlebars);
<!-- Load jQuery -->
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha256-/SIrNqv8h6QGKDuNoLGA4iret+kyesCkHGzVUUV0shc=" crossorigin="anonymous"></script>
<!-- Load JsRender -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrender/0.9.83/jsrender.min.js"></script>
<!-- Load Handlebars -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.6/handlebars.min.js"></script>
<!-- JSRENDER ************************************************************ -->
<!-- Provide container for rendered jsRender template output: -->
<p><strong>jsRender output:</strong>
</p>
<div id="result-jsrender"></div>
<!-- Declare a JsRender template, in a script block: -->
<script id="subMenuTemplate" type="text/x-jsrender">
<li class="menu__item{{if subMenu}} has-submenu{{/if}}">
<a class="menu__link" {{if submenuName && subMenu}}data-submenu="{{:submenuName}}" {{/if}} href="#">{{:name}}</a>
{{if subMenu}}
<ul data-menu="{{:submenuName}}" class="menu__level">
{{for subMenu tmpl='#subMenuTemplate'/}}
</ul>
{{/if}}
</li>
</script>
<!-- Declare a JsRender template, in a script block: -->
<script id="entryPointTemplate" type="text/x-jsrender">
<ul data-menu="main" class="menu__level">
{{for data tmpl='#subMenuTemplate'/}}
</ul>
</script>
<!-- HANDLEBARS ********************************************************** -->
<!-- Provide container for rendered Handlebars template output: -->
<p><strong>Handlebars output:</strong>
</p>
<div id="result-handlebars"></div>
<!-- Declare an Handlebars template, in a script block: -->
<script id="subMenuTemplate-handlebars" type="text/x-jsrender">
<li class="menu__item{{#if subMenu}} has-submenu{{/if}}">
<a class="menu__link" {{#if subMenu}}data-submenu="{{submenuName}}" {{/if}} href="#">{{name}}</a>
{{#if subMenu}}
<ul data-menu="{{submenuName}}" class="menu__level">
{{#each subMenu}} {{> subMenuTemplate}} {{/each}}
</ul>
{{/if}}
</li>
</script>
<!-- Declare an Handlebars template, in a script block: -->
<script id="entryPointTemplate-handlebars" type="text/x-handlebars-template">
<ul data-menu="main" class="menu__level">
{{#each data}} {{> subMenuTemplate}} {{/each}}
</ul>
</script>
This question already has answers here:
How do I check if an array includes a value in JavaScript?
(60 answers)
Closed 7 years ago.
I have two collections:
Group = {
users: [Array_of_User]
}
User = {
name: _string_
}
I'm listing groups ans I'm trying to know in the template if a user is in the groups:
mytemplate.js
Template.mytemplate.helpers({
groups: function(){
return Groups.find();
},
currentUsername: 'test'
});
mytemplate.html
<template name="main">
<ul>
{{#each groups}}
<li>
{{#if [the group contains currentUsername] }}
contains
{{else}}
doesn't contain
{{/if}}
</li>
{{/each}}
</ul>
</template>
The question is: what can I put on the helpers and instead of [the group contains currentUsername] to make it work?
Also, I'm not saying this is the way to do it. I'm open to any suggestions even if it means I have to change a lot.
You could use the Underscore.js function _.findWhere(list, properties) to check whether the group contains the username:
if (Meteor.isClient) {
Template.main.helpers({
groups: function() {
return Groups.find();
},
currentUsername: 'Matthias',
isInGroup: function(username) {
return !!_.findWhere(this.users, {
name: username
});
}
});
}
<template name="main">
<ul>
{{#each groups}}
<li>
{{#if isInGroup currentUsername}}
contains
{{else}}
doesn't contain
{{/if}}
</li>
{{/each}}
</ul>
</template>
if (Meteor.isServer) {
Meteor.startup(function() {
Groups.insert({
users: [{
name: "Matthias"
}, {
name: "Angie"
}]
});
});
}
Here is a MeteorPad.
Within your each block, your data context becomes the current group that is being iterated over. Therefore you can write a helper method that references that current data context like this:
userInGroup: function(username) {
var userInGroup;
this.forEach(function(groupUsername) {
if (username == groupUsername) {
userInGroup = true;
}
};
return userInGroup;
}
'this' within the userInGroup template helper references the current group as long as you use the helper within an a group iteration.
You can then use the helper like this:
<template name="main">
<ul>
{{#each groups}}
<li>
{{#if userInGroup currentUsername}}
contains
{{else}}
doesn't contain
{{/if}}
</li>
{{/each}}
</ul>
</template>
I try to integrate rubaxa:sortable to make my list sortable.
client/helpers.js
Template.getElements.helpers({
children: function() {
return Articles.find({ parent: this._id }, {sort: {order: 1}});
}
});
server/publications.js
Meteor.publish('articles', function() { return Articles.find({}, {sort: {slug: 1}}); });
Sortable.collections = 'articles';
template
<template name="getElements">
<ul class="sortable">
{{#each children}}
{{#sortable items=Articles sortField="order"}}
<li data-id="{{_id}}"><input type="text" name="keyword" value="{{title}}"></li>
{{/sortable}}
{{/each}}
</ul>
</template>
In the documentation (https://atmospherejs.com/rubaxa/sortable) I see the info:
Client:
{{#sortable items=<collection|cursor|array> sortField="order"}}
Server:
Sortable.collections = <collectionName>; // the name, not the variable
So what am I doing wrong? Right now no list-element is beeing shown.
I'm not an expert but based on integration this will fix your issues.
Server JS
Update the declaration to include the collation name in [].
Sortable.collections = ['articles'];
HTML Template
remove the following:
{{#each children}}
{{/each}}
Regards,
Vince
Lets say I have the following JSON and handlebars.js template :
JSON
{
rootPath: '/some/path/',
items:[ {
title: 'Hello World',
href: 'hello-world'
}, {
title: 'About',
href: 'about'
}, {
title: 'Latest News',
href: 'latest-news'
}
}
Template
<script id="test-template" type="text/x-handlebars-template">
<ul class="nav">
{{#each items}}
<li>{{title}}</li>
{{/each}}
</ul>
</script>
The template above works, until I want to filter items - lets say to have 2 lists one odd and the other even, here's a simple template for odd :
<script id="test-template" type="text/x-handlebars-template">
<ul class="nav">
{{#each items}}
{{#isOdd #index}}
<li>{{title}}</li>
{{/isOdd}}
{{/each}}
</ul>
</script>
And the registered helper :
// isOdd, helper to identify Odd items
Handlebars.registerHelper('isOdd', function (rawValue, options) {
if (+rawValue % 2) {
return options.fn(this);
} else {
return options.inverse(this);
}
});
The helpers work as expected and only the Odd items are rendered, however the reference to the parent context becomes lost, so the {{../rootPath}} directive ~~fails to render~~ renders an empty value.
Is there a way to pass the Parent context through the block Helper?
Change this:
<a href="{{../rootPath}}{{href}}">
to this:
<a href="{{../../rootPath}}{{href}}">
Why? because the if statement is in an inner context so first you need to go up a level and that's why you have to add ../
See more details in:
https://github.com/wycats/handlebars.js/issues/196