Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular nested directive not displaying new entities in model

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.

like image 752
Ian Wood Avatar asked Jan 14 '15 03:01

Ian Wood


1 Answers

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, controllers are run before any inner directives are parsed, and links 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:

  1. outer-directive controller
  2. inner-directive controller
  3. inner-directive link
  4. outer-directive link

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.

like image 89
Mosho Avatar answered Nov 15 '22 18:11

Mosho