How do display a collapsible tree in AngularJS + Bootstrap - javascript

I am building a web app where I need to display a tree using lists. My basic structure looks like this:
* Node 1
* Node 1.1
* Node 1.1.1
* Node 1.1.1.1
* Node 1.1.2
* Node 1.2
http://jsfiddle.net/QffFm/1/
I'm trying to find something in angular or bootstrap that I can use such that:
At first view of the list, it is expanded up to the third layer. In my fiddle, I would want to see Node 1, Node 1.1, Node 1.1.1, Node 1.1.2 and Node 1.2 (all but the 4th layer - Node 1.1.1.1)
On clicking on the list-style icon (not the word name of the node) The node collapses or expands
Ideally, I would love for the icon to change also dependent on if the item is expanded. A right arrow if there is more underneath, a down arrow if it is already expanded, and maybe a regular list item if there are no children
I am very new to AngularJS and still quite new to Bootstrap as well. I see that Angular has an accordion function which doesn't seem to quite handle everything I need it to.
I would love some direction on the best approach before I code a lot of logic into my web app that handles the different cases. I think this must be a common problem so perhaps there is something ready made that I can utilize. Any guidance would be much appreciated.
HTML code:
<div ng-app="myApp" ng-controller="controller">
<my-directive></my-directive>
<table style="width: 100%"><tbody><td>
<tree items="tree"></tree>
</td></tbody></table>
</div>
Angular code:
var app = angular.module('myApp', []);
app.controller('controller', function ($scope){
$scope.tree=[{"name":"Node 1","items":[{"name":"Node 1.1","items":[{"name":"Node 1.1.1","items":[{"name":"Node 1.1.1.1","items":[]}]},{"name":"Node 1.1.2","items":[]}]},{"name":"Node 1.2","items":[]}]}];
});
app.directive('tree', function() {
return {
template: '<ul><tree-node ng-repeat="item in items"></tree-node></ul>',
restrict: 'E',
replace: true,
scope: {
items: '=items',
}
};
});
app.directive('treeNode', function($compile) {
return {
restrict: 'E',
template: '<li >{{item.name}}</li>',
link: function(scope, elm, attrs) {
if (scope.item.items.length > 0) {
var children = $compile('<tree items="item.items"></tree>')(scope);
elm.append(children);
}
}
};
});

In followed example I used:
bootstrap
AngularJS recursive ng-include or (see second example) recursive directives
jQuery (will try to avoid in the future)
Demo 1 (ng-include) Plunker
From this model:
$scope.displayTree =
[{
"name": "Root",
"type_name": "Node",
"show": true,
"nodes": [{
"name": "Loose",
"group_name": "Node-1",
"show": true,
"nodes": [{
"name": "Node-1-1",
"device_name": "Node-1-1",
"show": true,
"nodes": []
}, {
"name": "Node-1-2",
"device_name": "Node-1-2",
"show": true,
"nodes": []
}, {
"name": "Node-1-3",
"device_name": "Node-1-3",
"show": true,
"nodes": []
}]
}, {
"name": "God",
"group_name": "Node-2",
"show": true,
"nodes": [{
"name": "Vadar",
"device_name": "Node-2-1",
"show": true,
"nodes": []
}]
}, {
"name": "Borg",
"group_name": "Node-3",
"show": true,
"nodes": []
}, {
"name": "Fess",
"group_name": "Node-4",
"show": true,
"nodes": []
}]
}];
[{
"name": "Android",
"type_name": "Android",
"icon": "icon-android icon-3",
"show": true,
"nodes": []
}];
}
The 2nd example is based on 2 directives:
app.directive('nodeTree', function() {
return {
template: '<node ng-repeat="node in tree"></node>',
replace: true,
transclude: true,
restrict: 'E',
scope: {
tree: '=ngModel'
}
};
});
app.directive('node', function($compile) {
return {
restrict: 'E',
replace:true,
templateUrl: 'the-tree.html',
link: function(scope, elm, attrs) {
// ....
if (scope.node.children.length > 0) {
var childNode = $compile('<ul ><node-tree ng-model="node.children"></node-tree></ul>')(scope)
elm.append(childNode);
}
}
};
});
(Added some checkboxes as well :))
Demo 2 Plunker
How it looks:

Related

Google chart huge organization diagram leaving screen

So i have the following directive:
angular.module('Division').directive('divisionChart', function (divisionListService) {
return {
restrict: 'E',
templateUrl: 'js/helpers/Division/directives/division-chart/division-chart.html',
replace: true,
link: function (scope, element, attr) {
function createChart() {
divisionListService.getList().then(function (result) {
var rows = [];
result.forEach(function (x) {
rows.push({
"c": [
{"v": x.id, "f": x.name},
{"v": x.parent_id >= 0 ? x.parent_id : ''}
]
})
});
var data = {
"cols": [
{"label": "Name", "pattern": "", "type": "string"},
{"label": "Manager", "pattern": "", "type": "string"},
{"label": "ToolTip", "pattern": "", "type": "string"}
],
"rows": rows
};
scope.chartObject = {
type: "OrgChart",
data: data
};
});
}
// Instantiate and draw our chart, passing in some options.
jQuery(document).ready(function () {
createChart();
});
}
}
});
And the following html:
<div class="col-xs-12">
<div id="chart_div" google-chart chart="chartObject"></div>
</div>
The data generated can be fairly large when dealing with greater organizations.
Which makes my diagram leave the screen like the image below shows:
I have tried fixing the issue by setting the width:100% however it does not seem to do anything.
So my question is how can fix this issue?
Google chart does not support it.
I would suggest you to use getorgchart instead

Angular scope.$watch not working in directive

I have an angular directive with a scope.$watch which is not working, but I know the value is changing. Here's the directive:
var StepFormDirective = function ($timeout, $sce, dataFactory) {
return {
replace: false,
restrict: 'AE',
scope: {
currentStep: "=",
title: "="
},
template: '<h3>{{title}}</h3><form class="step-form"></form>',
compile: function (tElem, attrs) {
return function (scope, elem, attrs) {
scope
.$watch(
function(){return scope.currentStep;},
function (newValue) {
var stepFormFields = Array();
stepFormFields.push($sce.trustAsHtml("<p>Fields</p>"));
alert(scope.currentStep);
}
);
};
}
};
};
Here's the tag:
<div title="'test'" currentStep="currentContext.currentStep"></div>
I know the currentContext.currentStep is changing, because I also have this on my page and it updates:
<pre>{{currentContext.currentStep | json}}</pre>
The alert gets called the first time, but then when the value changes (evidenced by the bit in the pre tags) the alert does not get called again and I have no js console errors.
The output for the step (it's data type) is:
{
"identifier": "830abacc-5f88-4f9a-a368-d8184adae70d",
"name": "Test 1",
"action": {
"name": "Approval",
"description": "Approve",
"instructions": "Select 'Approved' or 'Denied'",
"validOutcomes": [
{
"outcome": "Approved",
"display": "Approved",
"id": "Approved"
},
{
"outcome": "Denied",
"display": "Denied",
"id": "Denied"
}
]
...
Try to use $watch method with third argument set by true (objectEquality):
$watch('currentStep', function(newValue){...}, true);
Or use $watchCollection:
$watchCollection('currentStep', function(newValue){...})

Proper display of layered json data

I have json that looks like this:
[
{
"Id": 1,
"Name": "Item1",
"Order": 1,
"Categories": [
{
"Id": 1,
"Name": "Item1-Subitem1",
"Order": 1,
"Subcategories": [
{
"Id": 1,
"Name": "Item1-Subitem1-Subsubitem1",
"Order": 2
},
{
"Id": 2,
"Name": "Item1-Subitem1-Subsubitem2",
"Order": 1
},
...
and with angular I need to display on first page (or route) link on main items. For example:Item1Item2And when clicked on them links should be displayed with names from 'Categories'For example:Item1-Subitem1Item1-Subitem2And when clicked on these links there should be displayed links with name from 'Subcategories'For example:Item1-Subitem1-Subsubitem1Item1-Subitem1-Subsubitem2
Now my first links work but I don't know how to get that nested data from same json according to value from url. When I click on links in first view I go to page I need but without data. No errors is displayed. I belive I need to parse id from url but how to achieve that?
It can be checked here http://plnkr.co/edit/GTfLFQepFcXzXYcIHnP0I am using ui-router
In secondList.js, you've defined your scope with the wrong name. It should be:
scope: {
secondList: '='
},
After you fix that, you're also missing the final factory in service.js:
app.factory('singleList', ['$http', function ($http) {
return {
get: function () {
return $http.get('data.json').then(function (re) {
return re.data;
});
}
};
}])

angularjs directive to build widget according to arbitrary js Object with json schema

I'm trying to build a widget to render arbitrary data (javascript object) on ui. And the schema of the data is defined by a json schema file. And the model must be two-way binding since I want use this widget to input/display data.
Is directive the right way to do this? Is there anyone can give me some clue to achieve this?
Yes, a directive is the right way. You can see that the angular-schema-form mentioned in the comments is using directives.
If you'd like to create your own directive. Please have look at the demo below or this jsfiddle.
The example with the comments is probably not the best because there you wouldn't need the two-way binding of the data.
But it should give you an idea how this could work.
How does the code work?
It loops over schema.items.properties and creates a template with ng-model / ng-restrict and all bindings added to the template.
After the loop is done the $compile service is adding the current scope to the template.
Replace the html with the compiled template.
The demo is probably pretty basic compared to the angular-schema-form code but it's easier to understand and easier to modifiy to your needs.
angular.module('demoApp', [])
.controller('mainController', MainController)
.directive('awDisplayJsonView', DisplayJsonView)
.directive('awDateParser', DateParser);
/**
* Translates unix time stamp into readable dates
* from model/view & view/model
*/
function DateParser() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModelController) {
ngModelController.$parsers.push(function(data) {
//convert data from view format to model format
return new Date(data).getTime(); //converted
});
ngModelController.$formatters.push(function(data) {
//convert data from model format to view format
return new Date(parseInt(data)).toUTCString(); //converted return UTC
});
}
}
}
function MainController() {
angular.extend(this, {
/*jsonView: {
"text": { // name of model
type: "input",
label: "Two way binded input",
}
},*/
jsonModel: [{
"id": 1,
"name": "Test User",
"text": "I am a comment.",
"date": "1435427542904",
}, {
"id": 2,
"name": "Antother User",
"text": "I am the second comment.",
"date": "1435427605064",
}],
jsonSchema: {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Comments",
"type": "array",
"items": {
"title": "Comment",
"type": "object",
"properties": {
"id": {
"description": "The unique identifier for a comment.",
"type": "number",
"format": "hidden" // not sure if this is the right place
},
"name": {
"title": "Name",
"type": "string"
},
"text": {
"title": "Comment",
"type": "string"
},
"date": {
"title": "Date (format y/m/d hh:mm:ss GMT)",
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"name",
"text"]
}
}
});
}
function DisplayJsonView($compile) {
var templatesObj = {
string: function (hidden) {
return $('<input/>')
.attr('type', hidden)
.addClass('form-control');
},
checkbox: function (hidden) {
return $('<input/>')
.attr('type', hidden || 'checkbox')
.addClass('form-control');
},
number: function(hidden) {
return $('<input/>')
.attr('type', hidden)
.addClass('form-control');
}
};
function render(schema, model, index) {
var outTemplate = $(document.createDocumentFragment()),
tempTmpl, hidden; // temporary template, hidden input
angular.forEach(schema.items.properties, function (prop, key) {
//console.log(key, prop.type, prop.format, templatesObj[prop.type]);
hidden = prop.format == 'hidden'? 'hidden': null;
tempTmpl = templatesObj[prop.type](hidden); // get template based on type
tempTmpl.attr({
'ng-model':
'model[' + index + '].' + key, // add current model
'ng-required': schema.items.required.indexOf(key) != -1 // check if it is required
});
if (prop.format == 'date-time')
tempTmpl.attr('aw-date-parser', ''); // add directive if we have a date
outTemplate.append($('<div/>').addClass('form-group')
.append(!hidden ? $('<label/>').text(prop.title || key) : null) //add label if not hidden
.append(tempTmpl));
});
//console.log(model, outTemplate);
return outTemplate;
}
return {
restrict: 'EA',
scope: {
//view: '=', // angular schema form does implement this
model: '=',
schema: '='
},
template: '<div>{{schema |json:2}}</div>',
link: function (scope, element, attrs) {
var out = $('<form/>');
angular.forEach(scope.model, function(item, index) {
//console.log(item);
out.append(render(scope.schema, item, index));
});
var compiled = $compile(out)(scope);
//console.log(scope.model, scope.schema);
element.replaceWith(compiled);
}
};
}
DisplayJsonView.$inject = ['$compile'];
body {
padding: 0.5em;
}
.form-group {
padding-bottom: 0.25em;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
<div ng-app="demoApp" ng-controller="mainController as ctrl">
<h3>Comments list with schema json directive</h3>
<form name="form" aw-display-json-view="" data-model="ctrl.jsonModel" data-schema="ctrl.jsonSchema" ng-cloak=""></form>
<div ng-cloak>
{{form|json}}
{{ctrl.jsonModel}}
</div>
</div>

How do I recursively iterate over an object that has unknown hierarchies with Angular JS?

So take this example:
http://jsfiddle.net/7U5Pt/
Here I've got an object that describes a (very basic) menu. Each item can have multiple children with potentially unlimited levels of hierarchy.
The ng-repeat directives I'm using to turn it into <ul><li> elements work fine for the first two levels, but not for the third or any subsequent level of the hierarchy.
What's the best way to recursively iterate over this object, dealing with unlimited levels of children?
Any help much appreciated!
Here's the code incase the fiddle goes away:
HTML:
<div ng-app="myApp">
<div ng-controller="myCtrl">
<nav class="nav-left">
<ul ng-repeat="item in mytree.items">
<li>NAME: {{ item.name }}
<ul ng-repeat="item in item.children.items">
<li>SUB NAME: {{ item.name }}</li>
</ul>
</li>
</ul>
</nav>
</div>
JS:
var myApp = angular.module('myApp', []);
myApp.controller('myCtrl', function ($scope) {
$scope.mytree = {
"items": [{
"name": "one",
"children": {
"items": [
{
"name": "one sub a",
"children": {
"items": [{
"name": "one sub level two a"
},
{
"name": "one sub level two b"
}]
}
},
{
"name": "one sub b"
}
]
}
},
{
"name": "two"
},
{
"name": "three"
},
{
"name": "four",
"children": {
"items": [{
"name": "four sub a"
},
{
"name": "four sub b"
}]
}
},
{
"name": "five"
}]
};
});
So it turns out that the best way to do this, for anyone interested, is with the angular-recursion helper here:
https://github.com/marklagendijk/angular-recursion
This allows you to call the ng-repeat function recursively. So in your view, something like:
<tree family="mytree"></tree>
and then define a directive for that calls itself like so:
.directive('tree', function(RecursionHelper) {
return {
restrict: "E",
scope: {family: '='},
template:
'{{ family.name }}'+
'<ul ng-if="family.children">' +
'<li ng-repeat="child in family.children">' +
'<tree family="child"></tree>' +
'</li>' +
'</ul>',
compile: function(element) {
return RecursionHelper.compile(element);
}
};
});
Apparently the recursion helper is necessary to stop some sort of infinite loop thing, as discussed here. Thanks to #arturgrzesiak for the link!

Categories