Ludwig
Ludwig

Reputation: 1811

Angular - Passing an object from one directive to another directive

I am new to angular so apologies up front if a question is too newbie. I am trying to make a custom directive, and since I am already using an angular-youtube-embed directive, inside my new directive, I need to pass a player object from youtube-video directive, to my new directive, for the function playVideo in my scope to use it. I wonder how to do that? This is how my directive looks:

angular.module('coop.directives')
.directive('youtubePlayer', function () {
    return {
        restrict: 'E',
        scope: {
          videoPlaying: '=videoPlaying',
          playVideo: '&playVideo',
          playerVars: '=playerVars',
          article: '=article'
         },
        templateUrl : 'templates/youtube-player.html'
    };
}); 

This is my youtube-player.html:

<img ng-hide='videoPlaying' ng-src='http://i1.ytimg.com/vi/{{ article.external_media[0].video_id }}/maxresdefault.jpg' class='cover'>
<youtube-video ng-if='videoPlaying' video-url='article.external_media[0].original_url' player='player' player-vars='playerVars' class='video'></youtube-video>
<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo({player: player})'>
  <img ng-hide='videoPlaying' class='play' src='icons/play.svg'/>
  <img ng-hide='videoPlaying' class='playButton' src='icons/playRectangle.svg'/>
</div>

And this is the function from the controller that I would like to use in my directive:

  $scope.playVideo = function(player) {
    $scope.videoPlaying = true;
    player.playVideo();
  };

Where player is an object of youtube-video directive that I am using from angular-youtube-embed package. So, whenever a user clicks on an element below, $scope.videoPlaying should become true and a playVideo() function should start the video:

<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo(player)'>

This is how I call my directive in the view:

<youtube-player video-playing="videoPlaying" play-video="playVideo()" player-vars="playerVars" article="article"></youtube-player>

I should somehow pass a player object from youtube video to my new directive because now I get an error of:

ionic.bundle.js:26794 TypeError: Cannot read property 'playVideo' of undefined:

Upvotes: 16

Views: 6749

Answers (8)

illeb
illeb

Reputation: 2947

You can use '&' type for passing function in directives:

angular.module('coop.directives')
  .directive('youtubePlayer', function () {
    return {
        restrict: 'E',
        scope: {
          action: '&', //<- this type of parameter lets pass function to directives
          videoPlaying: '@videoPlaying',
          ...

so you directive will accept a parameter as a function, like this:

<coop.directives action="playVideo" videoPlaying="video" ...> </coop.directives>

and you'll be able to call that function normally:

      article: '=article'
     },
 template : "<img ng-hide='videoPlaying' ng-src='http://i1.ytimg.com/vi/{{ article.external_media[0].video_id }}/maxresdefault.jpg' class='cover'><youtube-video ng-if='videoPlaying' video-url='article.external_media[0].original_url' player='player' player-vars='playerVars' class='video'></youtube-video><div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo(player)'><img ng-hide='videoPlaying' class='play' src='icons/play.svg'/><img ng-hide='videoPlaying' class='playButton' src='icons/playRectangle.svg'/></div>",
    link: function (scope, element) {
      scope.action();
    }

Edit 1:

If none of those suggestions works, you can try to add () brackets to you action parameter action="playVideo()" or use '=' type parameter (but this way, your function will be double binded. In most cases you don't have to worry about it for functions, anyway).

You can find some examples in this old post: just try either solutions and find which one is working for your case.

Upvotes: 5

Joe Enzminger
Joe Enzminger

Reputation: 11190

The best way to pass an object to an angular directive is by using the &.

From the Angular Docs:

The & binding allows a directive to trigger evaluation of an expression in the context of the original scope, at a specific time. Any legal expression is allowed, including an expression which contains a function call

When you use &, angular compiles the string as an expression and sets the scope variable in your directive to a function that, when called, will evaluate the expression in the context of the directive's parent's scope.

I'm going to make a small change to your directive to help clarify my explanation.

angular.module('coop.directives')
.directive('youtubePlayer', function () {
    return {
        restrict: 'E',
        scope: {
          videoPlaying: '=videoPlaying',
          foo: '&playVideo',
          playerVars: '=playerVars',
          article: '=article'
         },
        templateUrl : 'templates/youtube-player.html'
    };
}); 

I changed the name of the directive scope variable from playVideo to foo. From here forward, playVideo is a property of the parent, while foo is the property bound by the & binding to a property of the directive. Hopefully the different names will make things more clear (they are, in fact, completely separate properties/methods.

In your case, the object you are trying to pass is a function. In this case, there are two options, both are subtly different and depend on how you want the consumer of the directive to use it.

Consider this usage:

<youtube-player video-playing="videoPlaying" foo="playVideo()" player-vars="playerVars" article="article"></youtube-player>

In this case, the expression is "playVideo()". The & directive will create a property in your directive scope called "foo" that is a function that, when called, evaluates that expression in the parent scope. In this case, evaluating this expression would result in the parent scope's playVideo method being invoked with no arguments.

In this usage, your directive can only call the parent scope's method as is. No parameters can be overridden or passed to the function.

So:

foo() -> parent.playVideo() 
foo(123) -> parent.playVideo() argument ignored
foo({player: 'xyz'}) -> parent.playVideo() argument ignored

Probably the preferred method if your parent method (playVideo) does not take any arguments.

Now consider a small change to the expression:

<youtube-player video-playing="videoPlaying" foo="playVideo(player)" player-vars="playerVars" article="article"></youtube-player>

Note the introduction of the local variable "player" in the expression. The function that is created in the directive's scope will do exactly the same thing as in the previous example, but it can now be called in two different ways. The variable "player" is considered a local variable in the expression.

The function foo generated by angular takes an argument that allows the directive to override the value of local variables in an expression. If no override is provided, it looks for a property of the parent scope with that name, if no such property exists, it will pass undefined to the function. So in this case:

$scope.foo() -> parent.playVideo(parent.player) 
$scope.foo(123) -> parent.playVideo(parent.player) 
$scope.foo({player: 'xyz'}) -> parent.playVideo('xyz')

If you want to pass the player from the directive to the parent, this is a weird way to do it (IMHO), because you have to know the name of the local variable in the expression. That creates an unnecessary requirement that the directive and the expression agree on the name of the argument.

The final way the playVideo function could be bound is:

<youtube-player video-playing="videoPlaying" foo="playVideo" player-vars="playerVars" article="article"></youtube-player>

In this case, the expression, evaluated against the parent, returns the function playVideo of the parent. In the directive, to call the function, you then have to invoke it.

$scope.foo() -> noop (you now have a pointer to the parent.playVideo function
$scope.foo()() ->  parent.playVideo()
$scope.foo()('xyz') -> parent.playVideo('xyz')

This last way, in my very humble opinion, is the proper way to pass a function pointer that takes an argument to a directive and use it within the directive.

There are some esoteric side effects that can be used (but shouldn't). For instance

$scope.foo({playVideo: function(){
    alert('what????')
})();  

This will not call the parent.playVideo function since you've overriden the expression's local variable "playVideo" with a custom version in the directive. Instead, it will pop up an alert dialog. Weird, but that's the way it works.

So, why not use @ or =?

If you use @, you essentially have to do what & does manually in the directive. Why do that when & will do it for you? '=' actually sets up two way binding, allowing the directive to change the value of the parent's property (potentially changing the function itself!) and vice-versa. Not a desirable side effect. This two-way binding also requires two watches which essentially are doing nothing but taking up cpu cycles since you aren't likely using them to update UI elements.

I hope this helps clear things up.

Upvotes: 0

I think I can code
I think I can code

Reputation: 647

Look at your button in your directive:

<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo({player: player})'>

You are not passing player to the function, you are actually passing player as the value of a property on an object that you are creating within the function call: {player: player}

So when you go to call the function .playVideo() on the player object, you are actually trying to call it on the object you created in the function call: {player: player} which obviously doesn't have a function in it.

To fix it, you need to either change your function, or change the player object being passing into the function. Instead of this:

$scope.playVideo = function(player) {
  $scope.videoPlaying = true;
  player.playVideo();
};

You would need to change it to this:

$scope.playVideo = function(player) {
  $scope.videoPlaying = true;
  player.player.playVideo();
};

Or, alternatively, leave the function alone and change the object you are passing in:

<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo(player)'>

JSFiddle

I've also created a JSFiddle showing the general concept of how your directive should be working.

Upvotes: 2

Vaibhav Shah
Vaibhav Shah

Reputation: 528

You can use $broadcast to achieve this.

Below is the diagram explaining the concept.

enter image description here

In youtubePlayer Directive use broadcast -

$rootscope.$broadcast('player-object', $scope.player);

And receive it in your custom directive.

$scope.$on('player-object', function (event, player) {
    $scope.videoPlaying = true;
    player.playVideo();
 });

Sample Example -http://jsfiddle.net/HB7LU/10364/

Upvotes: 6

Ahmer Khan
Ahmer Khan

Reputation: 767

You can create an angular service for that and use it anywhere in the project. This service contains all type of functionality that you need in multiple directives.

Upvotes: 0

Sheelpriy
Sheelpriy

Reputation: 1745

easiest will be use $rootScope in directive and assign player in rootscope then use it in controller.

or better approach will be using directive.

directive: in action you will assign a function with parameter.

rootApp.directive('ListTemplate', function () {
    return {
        restrict: 'EA',
        replace: true,
        transclude: true,
        scope: {
            list: '=',
            action: '=' 
        },
        template:  ' <div ng-click="bindSelectedGuest(guest.guid)" class="ct-clearfix info" ng-repeat="guest in list track by $index"  data-tag="{{activeUser.guestId}}" ng-class="{ active : guest.guid==activeUser.guestId}">' +
    '<label class="col-md-6 col-lg-7 ct-pull-left" data-tag="{{action}}" title="{{guest.firstName}}">{{guest.firstName}}</label>' +
    '<label class="col-md-6 col-lg-5 ct-pull-right"><span class="fr" ng-if="guest.mobile" title="{{guest.displayMobile}}">{{guest.displayMobile}}</span>' +
    '<span class="fr" ng-if="!guest.mobile">{{"N/A"}}</span>' +
    '</label>' +
    '<div class="info" ng-show="list.length==0"><div class="detail_alert message">No Record found</div></div></div>',
        link: function ($scope, e, a) {
            $scope.$watch('list', function () {
                //console.log(list);
            });
        }
    }
});

controller: you will capture function you defined in action(directive) here.

>  $scope.bindSelectedGuest($scope.selectedGuest.guid);

Upvotes: 0

Lizzy
Lizzy

Reputation: 2173

First of all, your question is contradicting. In your youtube-player.html, you use playVideo({player: player})

<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo({player: player})'>

and just below that you say you use it as playVideo(player).

<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo(player)'>

Assuming it is the second version, the problem here might be that the player reference actually is undefined and hence the youtube-video directive tries to assign values to an object that is not available. In order to solve this, assign an empty object to player in your youtube-player directive's controller.

angular.module('coop.directives').directive('youtubePlayer', function () {
    return {
        restrict: 'E',
        scope: {
          videoPlaying: '=videoPlaying',
          playVideo: '&playVideo',
          playerVars: '=playerVars',
          article: '=article'
        },
        templateUrl : 'templates/youtube-player.html',
        controller: function($scope) {
            $scope.player = {};
        }
    };
}); 

Upvotes: 2

hesa
hesa

Reputation: 451

Change the prefixes like this @videoPlaying to =videoPlaying and @playVideo to &playVideo

The @ before variables is evaluated as string values by angular and you need to use two-way-binding in this case.

Upvotes: 2

Related Questions