Hubert Grzeskowiak Hubert Grzeskowiak - 4 months ago 19
AngularJS Question

Including components procedurally in AngularJS

I have an array of objects. Each of these objects has a "component" property with a string value. Now I would like to loop through the list and render each of the referenced components. Other properties of the looped objects are supposed to provide parameters for the components (not included in example below).

My solution so far works, but requires stating the allowed elements in a switch-case and creates unwanted wrapper elements:



angular.module('switchExample', [])
.controller('ExampleController', ['$scope',
function($scope) {
$scope.items = [{
component: "alpha"
}, {
component: "beta"
}, {
component: "alpha"
}];
}
])
.component('alpha', {
template: "this is component alpha",
})
.component('beta', {
template: "this is component beta"
})

<!doctype html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Example</title>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
</head>

<body ng-app="switchExample">
<div ng-controller="ExampleController">
<div ng-repeat="s in items" ng-switch="s.component">
<alpha ng-switch-when="alpha"></alpha>
<beta ng-switch-when="beta"></beta>
</div>
</div>
</body>

</html>





Is there a way to include the components procedurally without the round-trip of string comparison and explicit invocation?

Something like this, maybe?:

<div ng-repeat="s in items">
<component ng-component="s.component"></component>
</div>


or even better:

<div ng-repeat="s in items" ng-component="s.component"></div>


or alternatively:

<div ng-component="s.component for s in items"></div>

Answer

There is no such thing as ng-component that would do this seamlessly. This is going to end up as a directive that recompiles its contents (not to mention performance penalty). You can become familiar with the approach by searching something like 'dynamic directive'

estus

Thanks, estus!

I did exactly that!

The custom directive component you can see below creates components based on passed name parameter and takes optional args.

Example input:

<component name="'alpha'"></component>

Output:

<component name="'alpha'">
    <alpha>
        whatever alpha component decides to render in its template
    </alpha>
</component>

With args:

<component name="'alpha'" args="{foo:'bar', num:42, o:{a:1}}"></component>

Output:

<component name="'alpha'" args="{foo:'bar', num:42, o:{a:1}}">
    <alpha foo="'bar'" num="42" o="{a:1}">
        whatever alpha component decides to render in its template with the given args
    </alpha>
</component>

angular.module('switchExample', [])

.directive('component', ['$compile',
  function($compile) {
    return {
      restrict: 'AEM',
      scope: {
        name: '<',
        args: '<'
      },
      controller: function($scope, $element) {
        var args = $scope.args;
        //console.log(args)
        var argsStr = ""
        if (typeof args !== 'undefined' && args.constructor === Object) {
          for (arg in args) {
            argsStr += ' ' + arg + '="args.' + arg + '"';
          }
        }
        var elem = "<" + $scope.name + argsStr + "></" + $scope.name + ">";
        //console.log(elem)
        var component = $compile(elem)($scope)[0];
        $element.append(component);
      }
    }
  }
])

.controller('ExampleController', ['$scope',
    function($scope) {
      $scope.items = [{
        component: "alpha",
        args: {
          foo: "first elem"
        }
      }, {
        component: "alpha",
        args: {
          foo: "the last one has no foo"
        }
      }, {
        component: "alpha",
        args: {}
      }];
    }
  ])
  .component('alpha', {
    bindings: {
      foo: '<?'
    },
    template: "this is component alpha. foo arg: {{ $ctrl.foo }}"
  })
  .component('beta', {
    template: "this is component beta"
  })
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Example</title>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
</head>

<body ng-app="switchExample">
  <h2>Procedural component</h2>
  <component name="'alpha'"></component>
  <br>
  <component name="'alpha'" args="{foo:'this was passed!', num:123, obj:{}}"></component>
  <br>
  <div component name="'alpha'" args="{foo:'as was this'}"></div>
  <hr>
  <h2>Looping</h2>
  <div ng-controller="ExampleController">
    <div ng-repeat="s in items" ng-switch="s.component">
      <component name="s.component" args="s.args"></component>
    </div>
  </div>
</body>

</html>

I couldn't get rid of the extra HTML elements wrapping my components, but at least I don't need to explicitly tell the loop what is allowed and what not.