xvdiff
xvdiff

Reputation: 2229

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

(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?

Upvotes: 1

Views: 650

Answers (2)

basarat
basarat

Reputation: 276017

Is 'the service way' better regarding to updating data etc?

Yes. Reason is that events are like "throw a ball in the air and hope someone catches it". Whereas service is a defined contract on how two controllers would interact.

If you use TypeScript you clearly care about Type Safety, services can give you that.


FYI We have the following guidance internally:

Shared Information between controllers

When a screen is highly nested with various placeholders with their own "html + controller" we need a way to share information (single source of truth) between these controllers.

1 RootController

The main controller for the section. Responsible for exposing the SharedClass instance on the scope (allows nested html segments to have direct access to this model) and any top level controller functions.

2 SharedClass

An angular service as it is easily inject-able into individual controllers / directives. Contains the information / functions that are shared in various portions of the screen e.g. multi select mode, Displayed / selected objects, operations for mutating these objects etc.

3 Individual Html + Controller

These subControllers can request the SharedClass using simple Angular Dependency Injection. The Controller HTML should automatically get access to the SharedClass instance (as that is exposed to scope by RootController).

Upvotes: 0

Omri Aharon
Omri Aharon

Reputation: 17064

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
});

Upvotes: 2

Related Questions