This module is designed for the Strongloop Loopback framework. It provides a mixin that makes it easy to transform a model into a tree using the Array of Ancestors pattern, more info here
npm install loopback-ds-tree-mixin
In your server/server.js
file add the following line before the
boot(app, __dirname);
line.
...
var app = module.exports = loopback();
...
// Add Readonly Mixin to loopback
require('loopback-ds-tree-mixin')(app);
...
To use with your Models add the mixins
attribute to the definition object of your model config.
{
"name": "Categories",
"properties": {
"category": "String",
"description": "String"
},
"mixins": {
"Tree": {}
}
}
The module returns a promise for each call.
Simple example, get the entire tree
var Category = app.models.Category;
Category.asTree({})
.then(function (results) {
console.log(results);
})
.catch(function (err) {
console.log(err);
});
Get all children of a given parent. We can pass a query in the form of an object
{id : 1}
or {category:'books'}
or any valid loopback query. The
where
property is added to your object. You can also pass a string like
1
which has to be the parent ID or you can pass an object containing the
property id (like the entire parent model)
var Category = app.models.Category;
Category.asTree({category : 'Books'})
.then(function (results) {
console.log(results);
});
var Category = app.models.Category;
Category.asTree('5616204f1c6a443c256c7a2f')
.then(function (results) {
console.log(results);
});
var Category = app.models.Category;
Category.findOne({where : {category : 'Books'}})
.then(function(Parent) {
//note, that we will use the Parent.id in our query to locate children
Category.asTree(Parent)
.then(function (results) {
console.log(results);
});
});
There are times where you want the children attached to the parent.
In this case we can use the withParent
option
var Category = app.models.Category;
Category.asTree('5616204f1c6a443c256c7a2f',{withParent:true})
.then(function (results) {
//the results will contain the parent
/*{
"category" : "Parent category",
"children" : [....]
}*/
console.log(results);
});
In some (not so rare) cases you may need to perform complex operations
on the tree, or you may need a quick reference to all nodes. A quick
solution is to have the entire tree in a flat form where each element
is connected by reference (because javascript) to the appropriate node.
To achieve this we use the returnEverything
option. We will get back
an object containing both the tree and a flat array
var Category = app.models.Category;
Category.asTree('5616204f1c6a443c256c7a2f',{returnEverything:true})
.then(function (results) {
//the results will contain the parent
/*
returns {tree: treeStructuredArray, flat: aFlatArrayOfTheTree}
*/
console.log(results);
});
The mixin adds a /asTree
end point to your model. It takes 2 parameters,
the parent query ({"category":"Books"}
) and the options object.
Only the parent query is required
To add a node you need to provide the parent and the node.
//Example providing a query object as parent
var Category = app.models.Category;
Category.addNode({permalink: 'a-category'},
{
category : 'A sub category',
permalink : 'a-sub-category',
active : true
})
.then(function (newItem) {
console.log(newItem);
})
.catch(function (err) {
console.log('Err', err);
});
Again, you can provide an object, a string or a loopback model as a
parent, just like the asTree()
method.
//Example providing a string ID as parent
var Category = app.models.Category;
Category.addNode('5616204f1c6a443c256c7a2f',
{
category : 'A sub category',
permalink : 'a-sub-category',
active : true
})
.then(function (newItem) {
console.log(newItem);
})
.catch(function (err) {
console.log('Err', err);
});
//Example providing a loopback model as parent
var Category = app.models.Category;
Category.findOne({where : {category : 'Books'}})
.then(function(Parent) {
Category.addNode(Parent,
{
category : 'A sub category',
permalink : 'a-sub-category',
active : true
})
.then(function (newItem) {
console.log(newItem);
})
});
The mixin adds a /addNode
end point to your model. It takes 2 parameters,
the parent query ({"category":"Books"}
) and the new node object.
Both parameters are required
You can safely delete a node via the deleteNode
method. By safely
we mean that you can be sure that the children will be handled accordingly.
If a node has X amount of children, and you just delete it via the
loopback API, then all the children still point to that parent. By using
the deleteNode
method, you ensure that these children will become
orphaned so that you can take care of them later on as you please.
You can also delete the parent along with the children in one call
by adding the withChildren
option.
Just as above, you can locate the node you want to delete either
as an ID string, a loopback query or a loopback model.
//Default operation. all children will become orphaned
//set withChildren to true to delete the children along with the parent
var Category = app.models.Category;
Category.deleteNode({category : 'Books'},{withChildren:false})
.then(function(result) {
//True if all went well
})
.catch(function(err){
});
The mixin adds a /deleteNode
end point to your model. It takes 2 parameters,
the node query ({"category":"Books"}
) and the options object.
Only the first parameter is required
Moving a node consists of the following actions :
var Category = app.models.Category;
//move category sci-fi under the category books
Category.moveNode({permalink: 'sci-fi'},{permalink: 'Books'})
.then(function(result) {
//get the sci-fi category back with the path updated
})
.catch(function(err){
});
The mixin adds a /moveNode
end point to your model. It takes 2 parameters,
the node query ({"category":"Books"}
) and the parent query object (same as node).
Both parameters are required
In some cases you might need to update the entire tree based on a json
representation. Maybe your front-end sends the json of the tree after
the user has finished re-ordering it. Assuming that the tree is formed
more or less as it is in your model, and that the children nodes live
in a children
array, you can pass this json and the mixin will update
your DB accordingly.
var Category = app.models.Category;
//req.body.tree is the JSON representation
//req.body.prependRoot is the root i want to append
Category.saveJsonTree(req.body.tree,{prependRoot : req.body.prependRoot || false})
.then(function (result) {
res.send(result);
})
.catch(function (err) {
});
The mixin adds a /saveJsonTree
end point to your model. It takes 2 parameters,
the JSON tree (valid Array tree) and the options object.
Only the first parameter is required.
The mixin emits events on the loopback Event bus for certain operations.
lbTree.add.success
when a node was added. Returns the new nodelbTree.move.before
Just before start the moving operation. Return the previous parent, the new parent and the node.lbTree.move.after
After the moving operation. Return the previous parent, the new parent and the node.lbTree.move.childrenFound
During the move when we have found all the children. Return the children.lbTree.move.parent
During the move we have the parent. Returns the parent.lbTree.move.newPath
The new path after the move ended. Returns the node.lbTree.delete
Node deleted. Returns {success : true/false}lbTree.saveJsTree
When a JSTree operations is done. Returns the treevar Category = app.models.Category;
Category.on('lbTree.move.childrenFound',function(children){
console.log("childrenFound",children);
});
Category.on('lbTree.move.parent',function(children){
console.log("parent",children);
});
Install dev dependencies:
npm i -D
Run tests:
npm test