Let's start an open source Angular 2 project: ng2-drag-drop-tree
There are 2 things that I really enjoy, salsa lessons in Bogota and open source projects. Unfortunately, I haven't had a lot of time for the later one :s.
I had a look at the Angular 2 libraries that are missing and ... there are quite a lot of them to transpose from Angular 1. So let's create a "new" one : ng2-drag-drop-tree.
The end result will look like:
http://angular-ui-tree.github.io/angular-ui-tree/#/basic-example
Since I've already done it in the past with Angular 1 #goodolddays, I pretty much know what are the minimum functionalities and it would be cool to post the progression here in order to show to everyone that helping the Open Source Community is not only for people named Linus Torvald.
In this first post we will see how to create tree nodes and use PrimeNG's drag/drop to manipulate them.
Why PrimeNG?
Simply because it's simple and they have a good documentation. Compared to what I used in the past, some features are not built-in but we will deal with it.
If you think that your Angular2/TypeScript knowledge is not solid yet, go Here to start learning first.
The Base Html
Something very basic, a directive "tree-node" with children that consist of:
- Some text
- Some subNodes for the recursivity
- A boolean variable "expanded" to see if the node's subNodes need to be displayed
<tree-node [children]="[{text:'First node', subNodes:[],expanded:false},
{text:'Fourth node', subNodes:[],expanded:false}]"></tree-node>
<tree-node [children]="[{text:'Second node', subNodes:[],expanded:false}]"></tree-node>
<tree-node [children]="[{text:'Third node', subNodes:[],expanded:false}]"></tree-node>
One Service To Rule Them All
Let's create a service that will help us handle the actions on those nodes, for now something very simple, we just need to know which node has been selected:
import { Injectable } from "@angular/core";
@Injectable()
export class TreeManager {
selectedNode;
constructor() {
this.selectedNode = "";
}
getSelectedNode() {
return this.selectedNode;
}
setSelectedNode(node) {
this.selectedNode = node;
}
}
The whole TreeNode component is here, we inject the drag-drop libs, our TreeManager, and create some Angular 2 features.
import { Component, Input } from "@angular/core";
import { Draggable, Droppable } from "primeng/primeng";
import { TreeManager } from "./tree-manager";
@Component({
selector: "tree-node",
directives: [Draggable, Droppable, TreeNode],
template: require("./tree-node.html")
})
export class TreeNode {
@Input() children;
treeManager: TreeManager;
constructor(treeManager: TreeManager) {
this.treeManager = treeManager;
}
onDragStart(event, child) {
this.treeManager.setSelectedNode(child);
}
onDragEnd(event, child) {
this.children.splice(this.children.indexOf(child), 1);
}
onDrop(event, node) {
node.subNodes = [...node.subNodes, this.treeManager.getSelectedNode()];
}
toggle(child) {
child.expanded = !child.expanded;
}
}
Let's break it down. We init our component TreeNode:
- 'tree-node' selector for the template
- We pass the Draggable, Droppable directives from PrimeNG AND the TreeNode itself so that we can use it recursively in the template
And finally the component's template
@Component({ selector: 'tree-node', directives: [Draggable,Droppable, TreeNode], template: require('./tree-node.html') })
We declare children as an Input and treeManager that we instantiate right after in the constructor:
@Input() children; treeManager:TreeManager; constructor(treeManager:TreeManager) { this.treeManager = treeManager; }
When we start dragging, we set the current node:
onDragStart(event,child){ this.treeManager.setSelectedNode(child); }
When we stop dragging, we delete the node from the children without verification for now:
onDragEnd(event,child){ this.children.splice(this.children.indexOf(child), 1); }
When a drop is triggered, instead of doing a push in the subNodes, we recreate the subNode using the spread operator so that we don't have troubles in the future with filtering (thanks EggHead):
onDrop(event,node){ node.subNodes = [...node.subNodes, this.treeManager.getSelectedNode()]; }
Finally the toggle method which is the most difficult one, we pass a child and we toggle the value of expanded (it took 5 developers and 2 days to do this one).
toggle(child) { child.expanded = !child.expanded; }
A KickAss Template
Basically the Node template is a list, just like pretty much every applications on the web.
We loop on every children of a node, display the text and if the node is expanded, we display a new node recursively, this new node will get populated using the subNodes that are pushed.
If we click on the node's text, we toggle expanded.
When we start/end dragging an element of the list we pass $event and child.
When we drop a node on the text, we use the previously described method 'onDrop' with the child and $event.
<ul>
<li style='padding:40px' *ngFor="#child of children"
pDraggable="dd" (onDragEnd)='onDragEnd($event,child)'
(onDragStart)='onDragStart($event,child)'>
<div pDroppable="dd" (click)='toggle(child)'
(onDrop)='onDrop($event,child)'
style='border:1px solid black'> {{child.text}}</div>
<div *ngIf="child.expanded">
<tree-node [children]='child.subNodes'></tree-node>
</div>
</li>
</ul>
As you can see the CSS is pretty basic, no need to complicate things ... for now.
Conclusion
A good start, trying to make it simple and flexible for the incoming features which will consist of changing the text of the nodes, add a delete feature, filtering, handling different type of drag/drop actions and handling actions on multiple nodes.
The project has been pushed here :
https://github.com/matthieu-D/ng2-drag-drop-tree
Would love some ideas on integration with SystemJS and WebPack since it's my first Angular 2 plugin ;).