blah238
blah238

Reputation: 1856

How to evaluate Angular expression in child directive and expose it on parent directive?

I have a directive that represents a group of objects, let's call it people.

This directive has an ng-repeat in its template that repeats a child directive, e.g. person, which has an expression attribute personGreeting which should evaluate on its scope.

Both people and person use isolate scope.

How can I set up these directives such that I can expose personGreeting on the people directive and have it be evaluated within the scope of the person directive?

Here is an example:

angular.module('app', [])
  .controller('ctrl', function($scope) {
    $scope.myPeople = [{
      id: 1,
      name: 'Bob'
    }, {
      id: 2,
      name: 'Steve'
    }, {
      id: 3,
      name: 'Joe',
    }]
  })
  .directive('people', function() {
    return {
      scope: {
        peopleList: '=',
        eachPersonGreeting: '&'
      },
      template: '<ul><person ng-repeat="currentPerson in peopleList" person-greeting="eachPersonGreeting(currentPerson)"></person></ul>'
    }
  })
  .directive('person', function() {
    return {
      scope: {
        personDetails: '=',
        personGreeting: '&'
      },
      template: '<li>{{personGreeting(personDetails)}}</li>'
    }
  })
  .directive('people2', function() {
    return {
      scope: {
        peopleList: '=',
        eachPersonGreeting: '@'
      },
      template: '<ul><person-2 ng-repeat="currentPerson in peopleList" person-greeting="{{eachPersonGreeting}}"></person-2></ul>'
    }
  })
  .directive('person2', function() {
    return {
      scope: {
        personDetails: '=',
        personGreeting: '@'
      },
      template: '<li>{{personGreeting}}</li>'
    }
  })
<!DOCTYPE html>
<html ng-app="app">

<head>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.5/angular.min.js"></script>
</head>

<body ng-controller="ctrl">
  <h4>Here we are just using an in-line expression with ng-repeat, which works as you'd expect:</h4>
  <ul>
    <li ng-repeat="currentPerson in myPeople">Hello, {{currentPerson.name}}!</li>
  </ul>

  <h4>But what if we have custom directives, `people`, and `person`, and we want to let consumers of our `people` directive specify how each `person` should be greeted without making them override our directive's template, and also have data binding still work?</h4>

  <h4>Unfortunately, this doesn't seem to work:</h4>

  <people people-list="myPeople" each-person-greeting="'Welcome, ' + personDetails.name + '!'"></people>

  <h4>Neither does this:</h4>

  <people-2 people-list="myPeople" each-person-greeting="Welcome, {{personDetails.name}}!"></people-2>
</body>


</html>

I also started delving into the compile, link and controller functions for both of the directives, as well as the $interpolate service, and got it kind of working, but it got really weird and messy, and I couldn't get data binding working, so it felt like wasted effort. I feel like this should be simple, but it doesn't seem to be.

Is there an elegant way to do this?

Upvotes: 1

Views: 398

Answers (2)

blah238
blah238

Reputation: 1856

Okay I managed to put together a simple guestbook app using the same concept that works: http://plnkr.co/edit/R7s6xE?p=info

angular.module('guestbookApp', [])
  .controller('guestbookCtrl', function($scope) {
    $scope.latestGuests = [{
      id: 1,
      name: 'Bob'
    }, {
      id: 2,
      name: 'Steve'
    }, {
      id: 3,
      name: 'Joe',
    }];
    $scope.newGuest = {
      name: ''
    };
    $scope.addGuest = function() {
      $scope.latestGuests.push(angular.extend(angular.copy($scope.newGuest), {
        id: $scope.latestGuests.length + 1
      }));
      $scope.newGuest.name = '';
    };
  })
  .directive('guestList', function($parse) {
    return {
      scope: {
        guests: '='
      },
      template: '<ul><li guest ng-repeat="currentGuest in guests | limitTo: -5" guest-details="currentGuest"></li></ul>',
      controller: function($scope, $element, $attrs) {
        this.greeting = function(scope) {
          return $parse($attrs.greeting)(scope);
        };
      }
    };
  })
  .directive('guest', function($parse) {
    return {
      scope: {
        guestDetails: '='
      },
      template: '{{greeting}}',
      require: '?^guestList',
      link: function(scope, element, attrs, controller) {
        var updateGreeting;
        if (controller) {
          updateGreeting = function() {
            scope.greeting = controller.greeting(scope);
          };
        } else if (attrs.greeting) {
          updateGreeting = function() {
            scope.greeting = $parse(attrs.greeting)(scope);
          };
        }
        scope.$watch('guestDetails.name', function() {
          updateGreeting();
        });
      }
    };
  });
<!DOCTYPE html>
<html ng-app="guestbookApp">

<head>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.5/angular.js"></script>
</head>

<body ng-controller="guestbookCtrl">

  <h4>Latest guests:</h4>

  <guest-list guests="latestGuests" greeting="'Welcome, ' + guestDetails.name + '!'"></guest-list>
  
  <h4>Type your name below to sign the guestbook:</h4>
  <input type="text" ng-model="newGuest.name" />
  <button ng-click="addGuest()" ng-disabled="!newGuest.name">Sign</button>
  <div ng-if="newGuest.name">
    <p guest guest-details="newGuest" greeting="'Hello, ' + guestDetails.name + '! Click \'Sign\' to sign the guestbook!'"></p>
  </div>
</body>

</html>

If anyone has any suggestions on how this could be improved please let me know! I still feel kind of yucky about the use of $parse and $watch, but maybe it's unavoidable?

Upvotes: 1

mkostelac
mkostelac

Reputation: 301

How the greeting depends on the user itself? Is there any way to create a service that knows how to produce customised greeting?

If not, what about Proxy object? https://www.youtube.com/watch?v=AIO2Om7B83s&feature=youtu.be&t=15m1s I've discovered it yesterday and, from my perspective, it seems that it fits here. You should create proxy object for each guest, inject it to the guest directive and during the link phase put the parsed (done by angular) greeting into the that injected proxy object.

Also, I think that something you're doing can be done on much simpler way than setting the attribute from the outside and parsing that.

Upvotes: 1

Related Questions