Searchable Angular Tree for large tree possibly with KendoUI's TreeView - javascript

I'm building a website using Angular JS as the presentation framework talking to a WebApi backend and need to render and tree control with a large amount of data (slightly under 1 MB). The tree needs to support check boxes and provide events when leafs or branches are selected. It is essential for the users that they can search for nodes in the tree by either the name of the node or a know ID associated with the node.
I've been looking at Telerik's Kendo UI however I have the following problems that I've not been able to resolve:
When the tree has loadOnDemand set to false (which it has to be because of the data size) the find methods of the control do not work
findByText only supports exact matching as far as I can tell (not an essential feature but certainly desirable)
No obvious was to find the node based on the second Id
I'd be happy to offload some of the work to our WebApi backend but can't see how the Telerik control would support this. I'd appreciate help in resolving these issues or a recommendation for a performant alternative to Telerik's tree control
I attempted to create a sample snippet on Telerik's dojo however the registration process is not working for me. I've created a small sample to demonstrate the problem but it appears to be runnable here too.
angular.module("KendoDemos", ["kendo.directives"]);
function MyCtrl($scope) {
$scope.treeData = new kendo.data.HierarchicalDataSource({
data: [{
id: 1,
extenalId: "AA",
text: "Item 1"
}, {
id: 2,
extenalId: "BA",
text: "Item 2",
items: [{
id: 21,
extenalId: "BB",
text: "SubItem 2.1"
}, {
id: 22,
extenalId: "BC",
text: "SubItem 2.2"
}]
}, {
id: 3,
extenalId: "CC",
text: "Item 3"
}]
});
$scope.itemTemplate = "<span title='{{dataItem.extenalId}}'>{{dataItem.text}}</span>";
$scope.findNode = findNode;
$scope.findById = findById;
$scope.findByExternalId = findByExternalId;
function findNode(item) {
var treeView = $("#sTree").data("kendoTreeView");
// TODO: How do we find SubItem 2.1 if the tree has not been expanded
// TODO: How do we do a contains search
var node = treeView.findByText($scope.search);
if (node.length) {
console.log("found");
treeView.expandTo(treeView.dataItem(node));
if (node[0].textContent === $scope.search)
treeView.select(node);
}
}
function findById() {
var treeView = $("#sTree").data("kendoTreeView");
//TODO: How do we find 22 if the tree has not been expanded
var dataItem = treeView.dataSource.get($scope.searchId);
var node = treeView.findByUid(dataItem.uid);
if (node.length) {
console.log("found by id");
treeView.expandTo(treeView.dataItem(node));
treeView.select(node);
} else
console.log("Not found by id");
}
function findByExternalId() {
var treeView = $("#sTree").data("kendoTreeView");
//TODO: How do we find BB
var dataItem = treeView.dataSource.get($scope.searchId2);
var node = treeView.findByUid(dataItem.uid);
if (node.length) {
console.log("found by id");
treeView.expandTo(treeView.dataItem(node));
treeView.select(node);
} else
console.log("Not found by id");
}
window.$scope = $scope;
}
<!DOCTYPE html>
<html>
<head>
<base href="http://demos.telerik.com/kendo-ui/treeview/angular">
<style>
html {
font-size: 12px;
font-family: Arial, Helvetica, sans-serif;
}
</style>
<title></title>
<link href="http://cdn.kendostatic.com/2014.2.903/styles/kendo.common.min.css" rel="stylesheet" />
<link href="http://cdn.kendostatic.com/2014.2.903/styles/kendo.default.min.css" rel="stylesheet" />
<link href="http://cdn.kendostatic.com/2014.2.903/styles/kendo.dataviz.min.css" rel="stylesheet" />
<link href="http://cdn.kendostatic.com/2014.2.903/styles/kendo.dataviz.default.min.css" rel="stylesheet" />
<script src="http://cdn.kendostatic.com/2014.2.903/js/jquery.min.js"></script>
<script src="http://cdn.kendostatic.com/2014.2.903/js/angular.min.js"></script>
<script src="http://cdn.kendostatic.com/2014.2.903/js/kendo.all.min.js"></script>
</head>
<body>
<div id="example" ng-app="KendoDemos">
<div class="demo-section k-content" ng-controller="MyCtrl">
<div class="box-col">
<h4>Searchable TreeView</h4>
<input data-ng-model="search" placeholder="Locate node" data-ng-change="findNode()" />
<div id="sTree" kendo-tree-view="tree" k-data-source="treeData" k-template="itemTemplate" k-on-change="selectedItem = dataItem"></div>
</div>
<div class="box-col" ng-show="selectedItem">
<h4>Selected: {{selectedItem.text}}</h4>
</div>
<div class="box-col">
<h4><input data-ng-model="searchId" placeholder="ID" /><button ng-click="findById()">Find </h4>
<h4><input data-ng-model="searchId2" placeholder="Extenal ID" /><button ng-click="findByExternalId()">Find </h4>
</div>
</div>
<style scoped>
.k-treeview .k-in {
padding: 5px;
}
</style>
</div>
</body>
</html>

Related

Implementing CRUD functions in JavaScript with an object

So I've been trying to implement the CRUD functions into my application (which is a quote generator). Quotes are stored inside of an object:
const citati = [
{
id: 1,
name: "First, solve the problem. Then, write the code. - John Johnson",
},
{
id: 2,
name: "Java is to JavaScript what car is to Carpet. - Chris Heilmann",
},
{
id: 3,
name: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand. - Martin Fowler",
},
{
id: 4,
name: "Code is like humor. When you have to explain it, it's bad. - Cory House",
},
{
id: 5,
name: "Optimism is an occupational hazard of programming: feedback is the treatment. - Kent Beck",
},
{ id: 6, name: "Simplicity is the soul of efficiency. - Austin Freeman" },
{
id: 7,
name: "Before software can be reusable it first has to be usable. - Ralph Johnson",
},
{ id: 8, name: "It's harder to read code than to write it. — Joel Spolsky" },
{ id: 9, name: "Deleted code is debugged code. - Jeff Sickel" },
];
And the HTML code is as follows:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="main.css" />
<title>Quote Generator</title>
</head>
<body>
<div class="container">
<div class="title">- Quote Generator -</div>
<div class="list">
<h4>Search your quote:</h2>
<input
autofocus
placeholder="search"
class="input"
type="text"
autocomplete="off"
name="search"
id="search"
/>
<ul class="list-group" id="list"></ul>
</div>
<div id="quote-generator">
<button id="btn" class="btn randomQuote">Press for a new quote!</button>
<div id="output" class="txt">Press the button to generate a new quote!</div>
<button id="btnCreate">Add</button>
<button id="btnUpdate" class="update">Update</button>
<button class="check">Check</button>
</div>
</div>
<script src="main.js"></script>
</body>
</html>
I've tried implementing the Create function:
btnCreate.addEventListener("click", function () {
citati.push({
name: prompt("Unesite citat i autora u obliku: citat - autor"),
});
});
But I still need to add the id in the function (tips for that are welcome). Now, I think I should be using the id's in the object for other functions but I am kind of lost and don't know how. The main idea is to update/delete the currently displayed quote. How do I get the current quote that is displayed? I tried with findIndex:
const outputTxt = document.querySelector(".txt").textContent;
function objIndex(citat) {
return citat.name === outputTxt;
}
document.querySelector(".check").addEventListener("click", function () {
console.log(citati.findIndex(objIndex));
});
But it doesn't work the right way.
If anyone could help with this matter I would be very grateful :)
Cheers!
P.S. here is the whole code if anyone needs to see it: https://github.com/msostaric-hub/app1
--SOLUTION--
Update: I figured it out! Here is the code that did the trick:
document.querySelector(".check").addEventListener("click", function () {
const outputTxt = document.querySelector(".txt").textContent;
const index = citati.findIndex((x) => x.name === outputTxt);
citati[index] = {
name: prompt("Unesite citat i autora u obliku: citat - autor"),
};
});

Uncaught ReferenceError: characters is not defined

I am creating a simple rest api in javascript, I want upon initialization, the widget must display a list of all characters.
here is folder structure :
├───book
└───book.js
├───store
│ └───store.js
here is my store.js
window.Store = {
create: function() {
var self = {};
var props = {
name: 'string',
species: 'string',
picture: 'string',
description: 'string'
};
var listProps = ['name', 'species'];
var detailProps = ['name', 'species', 'picture', 'description'];
var characters = [
{
id: makeID(),
name: 'Ndiefi',
species: 'Wookie',
picture: 'store/img/ndiefi.png',
description: 'A legendary Wookiee warrior and Han Solo’s co-pilot aboard the Millennium Falcon, Chewbacca was part of a core group of Rebels who restored freedom to the galaxy. Known for his short temper and accuracy with a bowcaster, Chewie also has a big heart -- and is unwavering in his loyalty to his friends. He has stuck with Han through years of turmoil that have changed both the galaxy and their lives.',
_delay: 500
},
];
}
}
here is index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Character Book</title>
<!-- 3rd party vendor libraries -->
<link rel="stylesheet" href="vendor/font-awesome-4.6.3/css/font-awesome.min.css">
<script src="vendor/jquery-3.1.0.min.js"></script>
<script src="vendor/underscore-1.8.3.min.js"></script>
<!-- 1st party internal libraries -->
<script src="store/store.js"></script>
<script src="tests/start-test.js"></script>
<script src="tests/test-book.js"></script>
<!-- The source of the 'Book' widget -->
<link href="book/book.css" rel="stylesheet">
<script src="book/book.js"></script>
<script>
$(function() {
var frame = $('#test-frame');
var run = $('#test-run');
var results = $('#test-results');
var store = Store.create();
run.click(function() {
run.prop('disabled', true).text('Running Tests');
results.removeClass('test-pass test-fail').text('');
testBook(frame).then(
function success() {
run.prop('disabled', false).text('Run Tests');
results.addClass('test-pass').text('All tests passed');
},
function failure(err) {
run.prop('disabled', false).text('Run Tests');
results.addClass('test-fail').text('Test failed, see console');
}
);
});
Book.init(frame, store);
});
</script>
</head>
<body>
<button id="test-run">Run Tests</button>
<span id="test-results"></span>
<div id="test-frame">
</div>
</body>
</html>
here is what I have tried :
books.js
var data = JSON.parse(characters);
data.forEach(characters => {
console.log(characters.name)
});
so when I run the app in my browser I see the following error :
Uncaught ReferenceError: characters is not defined
what is wrong with my code ? any suggestion or help will be helpfull thanks

Angular - can I use a single function on multiple objects within a nested ng-repeat that triggers the ng-show of just that object?

SUMMARYI have a list of brands and a list of products. I am using an ng-repeat to show the list of brands, and an ng-repeat with a filter to show the list of products within their respective brands. I want each brand and each product to have a button that shows more about that brand/product. All of these buttons should use the same function on the controller.
PROBLEMThe button that shows more about the brand also shows more about each of that brand's products, UNLESS (this is the weird part to me) I click the button of a product within that brand first, in which case it will work correctly.
CODEPlease see the Plunker here, and note that when you click on 'show type' on a brand, it also shows all the types of the products within that brand: http://plnkr.co/edit/gFnq3O3f0YYmBAB6dcwe?p=preview
HTML
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
</head>
<body>
<div ng-app="myApp">
<div ng-controller="MyController as vm">
<div ng-repeat="brand in brands">
<h1>
{{brand.name}}
</h1>
<button ng-click="showType(brand)">
Show Brand Type
</button>
<div ng-show="show">
{{brand.type}}
</div>
<div ng-repeat="product in products
| filter:filterProducts(brand.name)">
<h2>
{{product.name}}
</h2>
<button ng-click="showType(product)">
Show Product Type
</button>
<div ng-show="show">
{{product.type}}
</div>
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
<script src="script.js"></script>
</body>
</html>
JAVASCRIPT
var app = angular.module('myApp', []);
app.controller('MyController', function($scope) {
$scope.brands = [{
name: 'Kewl',
type: 'Cereal'
}, {
name: 'Joku',
type: 'Toy'
}, {
name: 'Loko',
type: 'Couch'
}]
$scope.products = [{
name: 'Kewlio',
type: 'Sugar Cereal',
brand: 'Kewl'
}, {
name: 'Kewliano',
type: 'Healthy Cereal',
brand: 'Kewl'
}, {
name: 'Jokurino',
type: 'Rattle',
brand: 'Joku'
}, {
name: 'Lokonoko',
type: 'Recliner',
brand: 'Loko'
}, {
name: 'Lokoboko',
type: 'Love Seat',
brand: 'Loko'
}]
$scope.showType = function(item) {
this.show = !this.show;
}
$scope.filterProducts = function(brand) {
return function(value) {
if(brand) {
return value.brand === brand;
} else {
return true;
}
}
}
});
IMPORTANT NOTE: I realize I could add an attribute to the object (brand.show) and pass the object into the function, then change that attribute to true/false, but I don't want to do this because in my actual application, the button will show a form that edits the brand/product and submits the info to Firebase, and I don't want the object to have a 'show' attribute on it. I would rather not have to delete the 'show' attribute every time I want to edit the info in Firebase.
ng-repeat directive create own scope, when you do
this.show = !this.show
you create/change show property in current scope, if click brand button - for brand scope, that global for product, and when click in product button - for scope concrete product.
To avoid this, you should create this property before clicking button, for example with ng-init, like
ng-init="show=false;"
on element with `ng-repeat" directive
Sample
var app = angular.module('myApp', []);
app.controller('MyController', function($scope) {
$scope.brands = [{
name: 'Kewl',
type: 'Cereal'
}, {
name: 'Joku',
type: 'Toy'
}, {
name: 'Loko',
type: 'Couch'
}]
$scope.products = [{
name: 'Kewlio',
type: 'Sugar Cereal',
brand: 'Kewl'
}, {
name: 'Kewliano',
type: 'Healthy Cereal',
brand: 'Kewl'
}, {
name: 'Jokurino',
type: 'Rattle',
brand: 'Joku'
}, {
name: 'Lokonoko',
type: 'Recliner',
brand: 'Loko'
}, {
name: 'Lokoboko',
type: 'Love Seat',
brand: 'Loko'
}]
$scope.showType = function(item) {
this.show = !this.show;
}
$scope.filterProducts = function(brand) {
return function(value) {
if (brand) {
return value.brand === brand;
} else {
return true;
}
}
}
});
/* Styles go here */
h1 {
font-family: impact;
}
h2 {
font-family: arial;
color: blue;
margin-bottom: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.js"></script>
<div ng-app="myApp">
<div ng-controller="MyController as vm">
<div ng-repeat="brand in brands" ng-init="show=false">
<h1>
{{brand.name}}
</h1>
<button ng-click="showType(brand)">
Show Brand Type
</button>
<div ng-show="show">
{{brand.type}}
</div>
<div ng-repeat="product in products
| filter:filterProducts(brand.name)" ng-init="show=false">
<h2>
{{product.name}}
</h2>
<button ng-click="showType(product)">
Show Product Type
</button>
<div ng-show="show">
{{product.type}}
</div>
</div>
</div>
</div>
</div>
The easiest fix for this, if you don't mind putting temporary properties in your data is the following changes:
<div ng-show="product.show">
{{product.type}}
</div>
and
<div ng-show="brand.show">
{{brand.type}}
</div>
and then in your controller
$scope.showType = function(item) {
item.show = !item.show;
}
Alternatively, if you don't want to touch the object properties, you can create an $scope.shownTypes array and have your click either push the object into or remove the object from the shown array. THen you can check for the object's existence in the array and show or not show the type appropriately. Let me know if you need a sample of that.
Your show boolean attribute same for whole tree (is in same scope). Using angular directive with child scope scope:true in ng-repeat helps to isolate each show property. I have forked your plunker code:
http://plnkr.co/edit/cMSvyfeCQOnTKG8F4l55?p=preview

checkboxes that append to a model in Angular.js

I have a model, which will be related to a number of other models. Think of a stack overflow question, for example, where it is a question related to tags. The final Object might look as follows before a POST or a PUT:
{
id: 28329332,
title: "checkboxes that append to a model in Angular.js",
tags: [{
id: 5678,
name: "angularjs"
}, {
id: 890,
name: "JavaScript"
}]
}
So far, I have the following controller:
.controller('CreateQuestionCtrl',
function($scope, $location, Question, Tag) {
$scope.question = new Question();
$scope.page = 1;
$scope.getTags = function() {
Tag.query({ page: $scope.page }, function(data) {
$scope.tags = data;
}, function(err) {
// to do, error when they try to use a page that doesn't exist
})
};
$scope.create = function() {
$scope.question.$save(function(data) {
$location.path("/question/" + data.id);
});
};
$scope.$watch($scope.page, $scope.getTags);
}
)
So I display all of the tags, paginated, on the page. I want them to be able to select the given tags and append it to my model so that it can be saved.
How can I create a checkbox interface where it updates the $scope.question with the selected other models?
EDIT: think I might be part of the way there
<div class="checkbox" ng-repeat="tag in tags.objects">
<label><input
type="checkbox"
ng-change="setTag(tag.id)"
ng-model="tag"
> {{ tag.name }}
</div>
Then on the controller
$scope.setTag = function(id) {
Tag.get({id: id}, function(data) {
// don't know what now
})
}
Basically, it takes a directive to approach your goal Take a look at the plunker I wrote for you. As you can see, in the list of selected tags the text property of each tag is displayed, it means that the object structure is kept. In your case, you would bind the $scope.question.tags array as the collection attribute and each tag from the $scope.tags as the element attribute.
Here a codepen for multiple check-boxes bound to the same model.
HTML
<html ng-app="codePen" >
<head>
<meta charset="utf-8">
<title>AngularJS Multiple Checkboxes</title>
</head>
<body>
<div ng:controller="MainCtrl">
<label ng-repeat="tag in model.tags">
<input type="checkbox" ng-model="tag.enabled" ng-change="onChecked()"> {{tag.name}}
</label>
<p>tags: {{model.tags}}</p>
<p> checkCount: {{counter}} </p>
</body>
</html>
JS
var app = angular.module('codePen', []);
app.controller('MainCtrl', function($scope){
$scope.model = { id: 28329332,
title: "checkboxes that append to a model in Angular.js",
tags: [{
id: 5678,
name: "angularjs",
enabled: false
}, {
id: 890,
name: "JavaScript",
enabled: true
}]
};
$scope.counter = 0;
$scope.onChecked = function (){
$scope.counter++;
};
});
I found a great library called checklist-model worth mentioning if anyone is looking up this question. All I had to do was this, more or less:
<div class="checkbox" ng-repeat="tag in tags">
<label>
<input type="checkbox" checklist-model="question.tags" checklist-value="tags"> {{ tag.name }}
</label>
</div>
Found this on googling "directives for angular checkbox".

Angular scope and Kendo UI controls

Suppose I have following html file:
<!DOCTYPE html>
<html>
<head>
<title>LOG</title>
</head>
<body>
<div class="panel panel-success">
<!-- Default panel contents -->
<div class="panel-heading">Log data</div>
<div class="panel-body">
<!-- List group -->
<ul class="list-group">
<li class="list-group-item">Start processing at {{StartProcessing }}</li>
<li class="list-group-item">Finished processing at {{EndProcessing }}</li>
</ul>
<div id="logTvId" kendo-tree-view
k-data-source="treeData">
</div>
</div>
</div>
and following controller code:
Arch.LogController = function ($scope, $resource, $routeParams)
{
var LogResource = $resource('log/:markerId', {}, {
get: {method: "GET", isArray: false}
});
LogResource.get({markerId: $routeParams.markerId}, function (data1)
{
$scope.StartProcessing = new Date(data1.StartProcessing).toLocaleString();
$scope.EndProcessing = new Date(data1.EndProcessing).toLocaleString();
$scope.treeData = new kendo.data.HierarchicalDataSource({ data: [
{ text: "Item 1" },
{ text: "Item 2", items: [
{ text: "SubItem 2.1" },
{ text: "SubItem 2.2" }
] },
{ text: "Item 3" }
]});
});
};
After page load I can see StartProcessing and EndProcessing on page, but I can't see treeview. If I take out code related to $scope.treeData from resource load (say the next instruction after)
then everything works as expeteced. If I add $scope.$apply() to my initial controller code it throws exception...
What I'm doing wrong? Should I deal with promises ($q ??) and wait after resource is loaded?
Thanks in advance.
Ok, actually problem is solved. In resource handler I create kendo treeview by "hand" (via jquery, as usual), so I'm not using kendo-tree-view directive. Simple div with id + jquery.

Categories

Resources