Using an Angular Shared Service

An Angular shared service is a powerful tool to allow cross component communication. If you’ve used other Javascript frameworks like Backbone or Vue then you are already probably familiar with the concept of a service bus. In Angular, a shared service is what provides what is essentially a pub/sub messaging pattern. Before Typescript and the componentized approach that we see in modern Javascript libraries, the service bus could become a source of problems. This was due to the fact that you could have any number of scripts publishing and subscribing to messages, but no easy to way to find which scripts were doing it other than to do a simple search. Debugging was especially painful, at least my experience with Backbone was.

That has all changed though. Typescript makes it easy to find which components are referencing the shared service, and the use of components helps to keep the Javascript more structured and manageable. If you aren’t using shared services in Angular then you are missing out on an important and powerful tool in your arsenal. When you have components that aren’t related to each other, but need to communicate, then the shared service is your weapon of choice. Lets take a simple but common use case. The picture below is from a CRM that I built, and the data you see is fake test data:

Angular shared service CRM

Notice the green dot with a number in it. This indicates a sales task is due today. Whenever I add a task that is due today, this number needs to update. Likewise, when I complete the task it needs to update the green dot and decrement the count, and when it’s zero, it will remove the green dot. The due today tab is simple enough since the functionality is all contained in one parent component with numerous child components. We can just pass it up through the component hierarchy in that case. What about the app top bar though? That is a completely unrelated component that we don’t have access to from within the contact details.

Defining the Shared Service

Let’s start off by defining the shared service and wiring it up for dependency injection.


import { Injectable, EventEmitter } from "@angular/core";

@Injectable()
export class AppTopBarSharedService {
    onUpdateProfilePicture: EventEmitter = new EventEmitter();
    onUpdateLogo: EventEmitter = new EventEmitter();
    onCompleteOverdue: EventEmitter = new EventEmitter();
    onAddDueToday: EventEmitter = new EventEmitter();
    onCompleteDueToday: EventEmitter = new EventEmitter();
    onRefreshCounts: EventEmitter = new EventEmitter();
    onPatientFollowUpAdded: EventEmitter = new EventEmitter();
    onPatientFollowUpCompleted: EventEmitter = new EventEmitter();
}

All we have done here is created an injectable class and defined events on it that can now be published and subscribed to. Next we need to go ahead and wire it up so it can be injected into our components. Add the shared service to your providers.


import { AppTopBarSharedService } from './services/apptopbar.shared.service';
@NgModule({
    declarations: [
],
providers: [
AppTopBarSharedService
]

Using the Shared Service

Now that the shared service is defined and injectable it is time to begin subscribing and publishing with it. In the AppTopBar component we’ll inject it in the constructor and then subscribe to the events.


import { AppTopBarSharedService } from '../../services/apptopbar.shared.service';
export class AppTopbarComponent implements OnInit {

    currentUserId: string = localStorage.getItem('userId') as string;
    dueToday: number = 0;
    patientModeEnabled: boolean = localStorage.getItem('patientModeEnabled') === 'true' ? true : false;
    patientHubConnection: HubConnection;
    salesTaskHubConnection: HubConnection;
    overdue: number = 0;
    patientFollowUp: number = 0;
    @Input() user: ApplicationUser;
    userHubConnection: HubConnection;
    @Output() showCalendar = new EventEmitter();
    @Input() tenant: Tenant;
    logoutUrl: string;

    constructor(public app: AppComponent, private appTopBarSharedService: AppTopBarSharedService, private sanitizer: DomSanitizer, private authService: AuthenticationService,
        private userApiService: UserApiService, @Inject('API_URL') private apiUrl: string, @Inject('IDENTITY_URL') private identityUrl: string, @Inject('BASE_URL') private baseUrl: string,
        private router: Router, private contactSharedService: ContactSharedService) {
        this.logoutUrl = `${this.identityUrl}/Account/Logout`;
        appTopBarSharedService.onUpdateProfilePicture.subscribe((image: string) => {
            this.user.picture.data = image;
        });
        appTopBarSharedService.onUpdateLogo.subscribe((logo: string) => {
            this.tenant.logo = logo;
        });
        appTopBarSharedService.onAddDueToday.subscribe((count: number) => {
            this.dueToday = this.dueToday + count;
        });
        appTopBarSharedService.onCompleteOverdue.subscribe((count: number) => {
            if (this.overdue != 0) {
                this.overdue = this.overdue - count;
            }
        });
        appTopBarSharedService.onCompleteDueToday.subscribe((count: number) => {
            if (this.dueToday != 0) {
                this.dueToday = this.dueToday + count;
            }
        });
        appTopBarSharedService.onRefreshCounts.subscribe((any: any) => {
            this.userApiService.getCounts(this.currentUserId).subscribe(data => {
                this.dueToday = data.dueToday;
                this.overdue = data.overdue;
            });
        });
        appTopBarSharedService.onPatientFollowUpAdded.subscribe((count: number) => {
            if (this.patientFollowUp != 0) {
                this.patientFollowUp = this.patientFollowUp + count;
            }
        });
        appTopBarSharedService.onPatientFollowUpCompleted.subscribe((count: number) => {
            if (this.patientFollowUp != 0) {
                this.patientFollowUp = this.patientFollowUp - count;
            }
        });
    }
}

As you can see in the code above, we are subscribing to the events and processing the message. In this case, we are adjusting the counts on the app top bar display whenever an event is passed. Let’s take a look at what the publish looks like from the contact detail component. This is just an abbreviation of the code in this component.


import { AppSharedService } from '../../services/app.shared.service';
@Component({
    selector: 'contact-detail',
    templateUrl: './contact-detail.component.html'
})
@Injectable()
export class ContactDetailComponent implements OnInit {
constructor(private appTopBarService: AppTopBarSharedService) {
}

changeSalesTaskDateScheduled() {
        this.appSharedService.onLoading.emit(true);
        this.contactApiService.changeTaskScheduleDate(this.selectedSalesTaskToChangeDate).subscribe(data => {
            this.currentContactDetail = data;
            this.currentContactDetail.isFullyLoaded = true;
            this.salesTasksViewModel = this.salesTaskMapper.mapSalesTasksToSalesTaskTableViewModel(data.salesTasks, this.campaigns);
            this.overdueCount = this.currentContactDetail.overdueSalesTasks.length;
            this.dueTodayCount = this.currentContactDetail.dueTodaySalesTasks.length;
            this.appTopBarService.onRefreshCounts.emit();
            this.onContactUpdated.emit(this.currentContactDetail);
            this.appSharedService.onLoading.emit(false);
            this.showChangeScheduleDate = false;
        });
    }
}
 

In the code above, when the sales task date scheduled is changed, the onRefreshCounts event is emitted which causes the AppTopBar component to update the counts. Simple, clean, and effective. That’s all there really is to it.

Sean Leitzinger

Solutions Architect at Edgeside Solutions
.NET and C# aficionado with an interest in architecture, patterns, practices, and more. Microsoft fanatic.

Latest posts by Sean Leitzinger (see all)

Leave a Reply

Your email address will not be published. Required fields are marked *