Search code examples
javascriptangularjstypescriptangularjs-scopedom-events

Update shared data between 2 controllers with services


I have 2 controllers where I'd like to share data in between. One controller supports a view with a table that often changes it's "selected item" and the other fetches data from an API using said item information. Since those two controllers support views and are on the same page, they don't have a classic parent/child hierarchy, but are more 'siblings'.

Currently, I'm using a simple 'event bus' service which calls an event on the root scope using $emit which I inject in both controllers, where as one listens for changes using $rootScope.$on. Now I heard many times that this is a bad solution and I should use services to share data, but no one really explains how it's possible to watch the data for changes when also

  • using $watch is bad
  • $broadcast is bad

(there seems to be really a war on SO on which solution should more be avoided).

My current solution:

Controller1

export class Controller1 {
    private eventBus : Components.IEventBusService;

    constructor(eventBus: Components.IEventBusService) {
        this.eventBus = eventBus;
    }

    void itemSelected(item : IItemModel) {
        this.eventBus.emit("itemSelected", { "item" : item });
    }
}

Controller 2

export class Controller2 {

    constructor($scope : ng.IScope,
        eventBus : Components.IEventBusService) {

        eventBus.on("itemSelected", (event, data) =>
            this.onItemSelected(data), $scope);
    }

    private onItemSelected(data: any) {
        // do something with data.item!
    }
}

EventBusService

export interface IEventBusService {
    on(event, callback, scope): void;
    emit(event, data): void;
}

class EventBusService implements IEventBusService {

    private rootScope: ng.IRootScopeService;

    constructor($rootScope: ng.IRootScopeService) {
        this.rootScope = $rootScope;
    }

    on(event, callback, scope) : void {
        var unbind = this.rootScope.$on(event, callback);
        if(scope) {
            scope.$on("$destroy", unbind);
        }
    }

    emit(event, data) : void {
        data = data || {};
        this.rootScope.$emit(event, data);
    }

}

Are there any major drawbacks using this solution? Is 'the service way' better regarding to updating data etc?


Solution

  • You are correct, the $on and $emit should be avoided, as they create unnecessary noise. There's actually a pretty simple solution to this situation, I use it quite a bit.

    What you need to have is an object in your service and give one controller a reference to it, and the other that needs to trigger an action can watch the service variable (good job on the TypeScript by the way):

    class ItemSelectionWrapper { //just a wrapper, you could have other variables in here if you want
        itemSelected: IItemModel;
    }
    
    class EventBusService implements IEventBusService {
    
        private myObj: ItemSelectionWrapper;
        ...
    }
    

    Then in your controller that has itemSelected in its scope (let's assume Controller 1) you refer to the same variable:

    export class Controller1 {
        private eventBus : Components.IEventBusService;
    
    
        constructor(eventBus: Components.IEventBusService) {
            this.eventBus = eventBus;
            this.eventBus.myObj = this.eventBus.myObj || {};
            this.eventBus.myObj.itemSelected = $scope.itemSelected; 
        }
    }
    

    Now, since in Controller 2 you'll have the service injected, you'll watch the Service's variable:

    $scope.$watch(function(){
            return busService.myObj,itemSelected;
        }, function (newValue) {
            //do what you need
    });