Developing a angular app which includes a feature to build a directory/nested tree structure...
The issue I'm having is the rendering of nodes isn't quite working as intended.
Products only seem to be rendered when there is already a product node in the list and sections can be created but attempting to add a subsection to one that has been added does not render. The section and product nodes are being inserted into the model as expected - simply that the directives don't seem to function on nodes that were not present in the original model.
Relevant code:
HTML
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="[email protected]" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.7/angular.js" data-semver="1.3.7"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<h1>Menu</h1>
<button ng-click="addSection()">Add</button>
<admin-sections sections="menu.sections"></admin-sections>
</body>
</html>
JS
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.menu = {
sections: [{
name: "NEW SECTION 1",
sections: [{
name: "NEW SECTION",
sections: [],
products: [{
"name": "Product",
"price": "0.00"
}]
}],
products: []
}]
};
$scope.addSection = function() {
$scope.menu.sections.push({
name: "NEW SECTION",
sections: [],
products: []
});
};
});
app
.directive('adminSections', function() {
return {
restrict: "E",
replace: true,
scope: {
sections: '='
},
templateUrl: 'sections.html'
};
})
.directive('adminSection', function($compile) {
return {
restrict: "E",
replace: true,
scope: {
section: '='
},
templateUrl: 'section.html',
link: function(scope, element, attrs, controller) {
if (angular.isArray(scope.section.sections) && scope.section.sections.length > 0) {
element.append($compile('<admin-sections sections="section.sections"></admin-sections>')(scope));
}
if (angular.isArray(scope.section.products) && scope.section.products.length > 0) {
element.append($compile('<admin-products products="section.products"></admin-products>')(scope));
}
scope.addSub = function(section) {
section.sections.push({
"name": "NEW SECTION",
"sections": [],
"products": []
});
};
scope.addProduct = function(section) {
section.products.push({
"name": "Product",
"price": "0.00"
});
};
scope.deleteSection = function(section) {
var idx = scope.$parent.sections.indexOf(section);
scope.$parent.sections.splice(idx, 1);
};
}
};
})
.directive('adminProducts', function() {
return {
restrict: "E",
replace: true,
scope: {
products: '='
},
templateUrl: 'products.html',
link: function(scope, element, attrs, controller) {
scope.editProduct = function(product) {
if (product.price === undefined) {
product.price = 0;
}
element.append($compile('<productform product="product"></productform>')(scope));
};
scope.deleteProduct = function(idx) {
if (confirm('Are you sure you want to delete this product?\n\nClick OK to confirm.')) {
scope.products.splice(idx, 1);
}
};
}
};
})
.directive('adminProduct', function($compile) {
return {
restrict: "E",
replace: true,
scope: {
product: '='
},
templateUrl: 'product.html',
link: function(scope, element, attr, controller) {
scope.editProduct = function(product) {
if (product.price === undefined) {
product.price = 0;
}
element.append($compile('<productform product="product" />')(scope));
};
scope.deleteProduct = function(idx) {
scope.$parent.deleteProduct(idx);
};
}
};
})
.directive('productform', function($compile) {
return {
restrict: "E",
replace: true,
scope: {
product: "="
},
templateUrl: 'productform.html',
link: function(scope, element, attrs, controller) {
scope.orig = angular.copy(scope.product);
scope.ok = function() {
element.remove();
scope.$parent.editMode = false;
};
scope.cancel = function() {
scope.reset();
element.remove();
scope.$parent.editMode = false;
}
scope.reset = function() {
scope.product = angular.copy(scope.orig);
}
}
};
});
Plunker is here: Angular Tree Menu
Hopefully you can see the intent.
The problem is that you add the list when the directive is linked, depending on the section's state when the linking function is called (only once, when angular sees it).
When you add a new subsection, it is linked but its subsection list is empty so it has none, and the resulting element has no subsections, since you add admin-sections
depending on the subsections array state at the time the linking function is called, so no nested directives will be added at all.
Simply removing the if
statements should suffice (or just checking if they are arrays):
element.append($compile('<admin-sections sections="section.sections"></admin-sections>')(scope));
element.append($compile('<admin-products products="section.products"></admin-products>')(scope));
This way, the ng-repeat
in your directives will watch the sections arrays in each section and update the list accordingly, while remaining empty when the array is empty.
Working Plunker
As to how nested directives work, here is a nice article on when nested directives' linking and controller functions are called.
In general, controller
s are run before any inner directives are parsed, and link
s are run after. So if you have nested directives like this:
<outer-directive>
<inner-directive></inner-directive>
</outer-directive>
The order would be like this:
This is why when I tried to add the admin-sections
directive to each section's template, the parser went into an infinite loop. Parsing each sections meant calling another link
of that section's subsections but using $compile
in the outer admin-section
's linking function means it will be parsed after the outer directive is processed.
In addition, inner directives can require
(docs) parent directives to use their controllers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With