3

I'm writing a directive which creates an mp3/audio player. The issue is that you can have many audio players in one page. What I would like to do is when one is playing and you start an other, that the one currently playing pauses. How can I achieve this with angular directives?

Thanks in advance!

tereško
  • 58,060
  • 25
  • 98
  • 150
Jeanluca Scaljeri
  • 26,343
  • 56
  • 205
  • 333

4 Answers4

8

Make a service that each directive uses and hold the state in there.

Something like this:

angular.module('MyPlayer' [])
.factory('playerState', function() {
    var players = [];
    return {
        registerPlayer: function(player) {
            players.add(player);
        },
        unregisterPlayer: function(player) {
            var i = players.indexOf(player);
            (i>-1) && players.splice(i,1);
        },
        stopAllPlayers: function() {
            for(var i=0;i<players.length;i++) {
                players[i].stop();
            }
        }
    }
})
.directive('player', function(playerState) {
    return {
        ...
        link: function(scope, elem, attr) {
            var player = {
                stop: function() {
                    /* logic to stop playing */
                },
                play = function(song) {
                    playerState.stopAllPlayers();
                    /* logic to start playing */
                }
            }

            playerState.registerPlayer(player);
            scope.$on("$destroy", function() {
                playerState.unregister(player);
            });

            scope.play = player.play;
            scope.stop = player.stop;

            ...
        }
    }
})
georgeawg
  • 48,608
  • 13
  • 72
  • 95
Jussi Kosunen
  • 8,277
  • 3
  • 26
  • 34
  • 2
    I'm not a huge fan of this answer as you're basically building an event system on top of the existing event system, why not just use the built in? Props for providing code though. – George Yates Sep 13 '13 at 07:48
  • Players should be unregistered at some point, otherwise you will be leaking memory when changing views/routes, etc. – garst Sep 13 '13 at 10:03
  • is there a way to trigger the unregistration when the route changes ? – Jeanluca Scaljeri Sep 13 '13 at 18:03
  • 1
    @JeanlucaScaljeri there is an event triggered when the scope is destroyed: `scope.$on('$destroy', ...);` – garst Sep 14 '13 at 10:05
  • What about moving the logic of the service to the directive block above the return statement. The variables and methods would still be visible to the link function, and as far as I understand, the actual directive function is only invoked once? – chrismarx Dec 11 '13 at 17:32
  • @GeorgeYates: The advantage here is the encapsulation inside the module and imho a clearer separation of concerns. – hugo der hungrige Dec 16 '13 at 00:02
5

Just to make the answers complete, next to broadcasting events, and exposing a service, you can also use directive controllers. These controllers are set through the controller property of a directive definition object and are shared between directives that require the same controller. This means you can have one controller for all the media players, where you can implement the logic you mentioned. See the documentation on directives (search for controller:) for more information.

I would recommend the service approach if you think there will be more consumers of the logic, or the directive controller approach if only the directives consume the logic. I would advise against broadcasting events on the root scope because of the uncoupled and global nature of it. Just my two cents! HTH

Steve Klösters
  • 9,427
  • 2
  • 42
  • 53
  • 2
    Be careful, I would say that controllers are generally shared between directives that share the same DOM element (default behavior) or in nested constructs (^ notation) like tabs for instance. What you advice might be possible with the ? notation but can very tricky, bug-prone and difficult to maintain. Service / Broadcast seems to be more adapted. – ovmjm Sep 13 '13 at 09:50
  • I tried to get your solution working in jsfiddle. I'm not sure how to do this with controllers. Here is my [jsfiddle](http://jsfiddle.net/w7BMQ/1/). Also, I still don't understand what to put in the link function and what in the controller – Jeanluca Scaljeri Sep 13 '13 at 13:39
  • [here](http://jsfiddle.net/w7BMQ/3/) is my working fiddle. Any suggestions how to update the other directives using a controller ? – Jeanluca Scaljeri Sep 13 '13 at 17:29
  • here it is working for you with this method. http://jsfiddle.net/joshkurz/B8x3Q/ I think overall this is the cleanest way to go about doing something like this. A little more difficult than broadcasting, but the end result is 100% control of the players in whatever mannar needed. – joshkurz Jan 27 '14 at 01:46
1

How are your directives setup? Please provide some code.

This depends on the scope of your directives, I'm going to assume a child scope. To communicate between the directives, when a user clicked to start a player, I would call a $scope.$parent.$broadcast() - or $rootScope.$broadcast() if the directives are in different controllers or using isolated scopes, but then you need to inject $rootScope into your directive - to send an event to all child scopes. My directives would be watching for this event using $on and any players that were playing would stop. After this broadcast the player clicked would start.

$broadcast() and $on() scope documentation

George Yates
  • 1,237
  • 9
  • 16
0

You can also do $rootScope.$broadcast events like playerStarted. This event can be subscribed by all directives and they can react to this event by stopping themselves. The one thing that you need to do would be pass in the data about the player which is starting so that the new player does not stop itself as it too would subscribe to such event.

Chandermani
  • 42,589
  • 12
  • 85
  • 88
  • 1
    Bah, you got here first. My only difference is I would start the player playing after the event broadcast rather than send more information and have every player check to see if it's the event originator. – George Yates Sep 13 '13 at 07:37
  • Actually, now that I think about it, I'm not sure if that would create a race condition or not. Do you know if $broadcast() is blocking until after the digest cycle is done? – George Yates Sep 13 '13 at 07:45
  • 1
    `$on` callbacks are executed immediately when `$broadcast` is called. See [the source](https://github.com/angular/angular.js/blob/v1.2.0-rc.2/src/ng/rootScope.js#L934). This means they are called synchronously/blocking. – Steve Klösters Sep 13 '13 at 09:43
  • 1
    Yup, I would use $broadcast and try and limit the scope as much as possible, so **[here](http://jsfiddle.net/GYatesIII/w7BMQ/5/)** I added in a controller and broadcast on the controller scope rather than the rootScope for efficiency. – George Yates Sep 13 '13 at 18:33
  • Don't use broadcast on the rootscope, just don't. The idea about a service to isolate your communication and possibly state in an explicit and, more importunately, encapsulated way. This will make debugging a whole lot easier and will allow you to intercept / mock the communication during testing. It will also make the live of the next developer ( that is you to ) a lot easier when he has to refactor stuff. – Willem D'Haeseleer Feb 25 '14 at 14:52
  • Use of the scope/rootScope event bus with `$broadcast`, `$emit`, and `$on` is deprecated and will make migration to Angular 2+ more difficult. See [this answer](https://stackoverflow.com/a/51687407/5535245) for a different approach. – georgeawg Aug 04 '18 at 16:37