Services and Dependency Injection - aakash14goplani/FullStack GitHub Wiki

Topics Covered

What is Service
What is a Dependency Injector?
Injecting dependency into a service
Hierarchical Injector
Creating Data Service
Injecting Service into Service
Component Communication using Services


What is Service

  • A service is basically just another class you can add which acts as a central repository, something where you can store, where you can centralize your code in. Services in Angular is used to:
    1. Avoid duplication of code
      • where multiple components are performing same logic, we can share data instead of copying same code to multiple components
    2. Data Storage
      • It provides central storage system to store data. This is useful when multiple components want to access centralized data.

What is a Dependency Injector?

  • Dependency is something a class of ours will depend on, for example the component depends on the logging-service because we want to call a method in that service and the dependency injector simply injects this dependency, injects an instance of this class into our component automatically.

Injecting dependency into a service

  • I have a LoogingService which needs to be injected within other components

    export class LoggingService {
       loggingStatusChange(log: string) {
         console.log(log);
       }
    }
  • To inject a dependency all we need to do is we need to inform Angular that we require such an instance.

  • So how do we inform Angular that we require such an instance? We add a constructor to the class, to the component in this case where we want to use our service and pass the property (e.g. loggingService) with the type assignment of service (e.g. LoggingService which is name of service class to be injected) as an argument

    constructor(private loggingService: LoggingService) { }
  • This simple task here informs Angular that we will need an instance of this loggingService.

  • Why does this matter if we write this in the constructor?

    • Think about who gives us the instance of this component here. The TypeScript class, in the end, needs to get instantiated so that something happens in our app.
    • Who is responsible for creating our components? Angular is of course because we are placing selectors (like @Component, @Directive) in our templates and when Angular comes across these selectors, it gives us instances of our components. Now since Angular is responsible for instantiating our components, Angular will need to construct them correctly.
    • So if we define in the constructor that we require some argument, Angular will recognize this and now it tries to give us that argument, it tries to give us this type in this case.
    • So it knows we want an instance of the loggingService class because we defined a type,
  • Next we need to provide a service. Provide simply means we tell Angular how to create it. All we have to do is add one extra property to the @component decorator - the providers property here. This takes an array and here we just have to specify the type of what we want to be able to get provided e.g.

    providers: [LoggingService]
  • Now with that, Angular when analyzing the component, recognizes that it should be able to give us such a loggingService and it will set itself up to be able to do so and when it then actually builds the component, constructs it, it sees that we want to have such an instance and it will know how to give us such an instance and now we can simply in our component, anywhere in this component, access our loggingService property.

  • This is how our components looks like after injecting dependency

    import { Component } from '@angular/core';
    import { LoggingService } from '../logging.service';
    
    @Component({
      selector: 'app-account',
      templateUrl: './account.component.html',
      styleUrls: ['./account.component.css'],
      providers: [LoggingService]
    })
    export class AccountComponent {
       constructor(private loggingService: LoggingService) { }
       onSetTo(status: string) {
         this.loggingService.loggingStatusChange('Server status: ' + status);
       }
    }

Hierarchical Injector

  • The Angular dependency injector actually is a hierarchical injector, that means that if we provide a service in some place of our app, let's say on one component, the Angular framework knows how to create an instance of that service for this component and important, all its child components and the child components of the child components will receive the same instance of the service.

  • There are other places where we can provide a service too:

    1. AppModule: The highest possible level is the AppModule. If we provide a service there, the same instance of the service is available in our whole app, in all components, in all directives, in all other services where we maybe inject the service.
    2. AppComponent: The next level for example would be the AppComponent, the AppComponent and all its child components do have the same instance of the service
    3. Other Components: This is true for any component, so even if we have a child of the app component, if we provide it on that child, all the children of this child will have the same instance and the child itself but not the app component.
  • The instances don't propagate up - they only go down that tree of components. The lowest level therefore is a single component with no child components. If we provide a service there, this component will have its own instance of this service and well it doesn't have any child components, so this instance will only be available for this component and this will actually even overwrite if we were to provide the same service on a higher level


Creating Data Service

To understand Data service, consider following scenario where we have three components

  1. Service Component: Parent Component. Calls other two Child components to display all Account details
  2. Account Component: Child component. Updates Account status.
  3. New Account Component: Child component. Create new Account.

We have one service Accounts Service which acts as central repository to hold Account data. It has three properties:

  1. accounts array: to hold all account data
  2. addAccount() : to add new Account
  3. updateStatus(): to update existing account

Our aim is to access these properties within all 3 components!

accounts.service.ts

export class AccountsService {
    accounts = [
        {name: 'Master Account',status: 'active'},
        {name: 'Test Account',status: 'inactive'},
        {name: 'Hidden Account',status: 'unknown'}
    ];
    addAccount(name: string, status: string) {
        this.accounts.push({name: name, status: status});
    }
    updateStatus(id: number, status: string) {
        this.accounts[id].status = status;
    }
}
  • To access this service in parent component Services component

    1. The first step is to pass dependency injection to constructor i.e the service we want to access
    2. Second step is to pass service name in providers so Angular enables us to use instance of this service
    3. We access the accounts property of AccountsService and store it in an local array which could be passed to two child components
    4. We initialize local property within OnInit() function

    Services.Component.ts

import { Component, OnInit } from '@angular/core';
import { AccountsService } from './accounts.service';
@Component({
  selector: 'app-services',
  templateUrl: './services.component.html',
  providers: [AccountsService]
})
export class ServicesComponent implements OnInit {
  constructor(private accountsService: AccountsService) { }
  accounts: {name: string, status: string}[] = [];
  ngOnInit() {
    this.accounts = this.accountsService.accounts;
  }
}

Services.Component.html

<div class="container">
    <div class="row">
        <div class="col-xs-12 col-md-8 col-md-offset-2">
            <app-new-account></app-new-account>
            <hr>
            <app-account *ngFor="let acc of accounts; let i = index" [account]="acc" [id]="i"></app-account>
        </div>
    </div>
</div>
  • Lets work on New Account Component, wherein we want to add new account information i.e. we want to use addAccount() property of service to add account data.
    1. The first step is to pass dependency injection to constructor i.e the service we want to access
    2. Here we won't pass service name to providers because doing so we will get new AccountsService instance which would be different one from that which we receive from parent Service component.
    3. We want to use same instance that was passed by parent component, for that we just need to inject dependency in constructor to let angular know that we will be using AccountsService and then directly call AccountsService property addAccount()
import { Component } from '@angular/core';
import { AccountsService } from '../accounts.service';
@Component({
  selector: 'app-new-account',
  templateUrl: './new-account.component.html'
})
export class NewAccountComponent {
  constructor(private accountsService: AccountsService) { }
  onCreateAccount(accountName: string, accountStatus: string) {
    this.accountsService.addAccount(accountName, accountStatus);
  }
}
<div class="row">
    <div class="col-xs-12 col-md-8 col-md-offset-2">
        <div class="form-group">
            <label>Account Name</label>
            <input type="text" class="form-control" #accountName>
        </div>
        <div class="form-group">
            <select class="form-control" #status>
                <option value="active">Active</option>
                <option value="inactive">Inactive</option>
                <option value="hidden">Hidden</option>
            </select>
        </div>
        <button class="btn btn-primary" (click)="onCreateAccount(accountName.value, status.value)">
        Add Account
        </button>
    </div>
</div>
  • Lets work on last child component i.e. Account Component, wherein we want to update existing account information i.e. we want to use updateStatus() property of service to update account data.
    1. The first step is to pass dependency injection to constructor i.e the service we want to access
    2. Here we won't pass service name to providers like before due to same reason.
    3. We want to use same instance that was passed by parent component, for that we just need to inject dependency in constructor to let angular know that we will be using AccountsService and then directly call AccountsService property
import { Component, Input } from '@angular/core';
import { AccountsService } from '../accounts.service';
@Component({
  selector: 'app-account',
  templateUrl: './account.component.html';
})
export class AccountComponent {
  constructor(private accountsService: AccountsService) { }
  @Input() account: {name: string, status: string};
  @Input() id: number;
  onSetTo(status: string) {
    this.accountsService.updateStatus(this.id, status);
  }
}
<div class="row">
    <div class="col-xs-12 col-md-8 col-md-offset-2">
        <h5>{{ account.name }}</h5>
        <hr>
        <p>This account is {{ account.status }}</p>
        <button class="btn btn-default" (click)="onSetTo('active')">Set to 'active'</button>
        <button class="btn btn-default" (click)="onSetTo('inactive')">Set to 'inactive'</button>
        <button class="btn btn-default" (click)="onSetTo('unknown')">Set to 'unknown'</button>
    </div>
</div> 

Injecting Service into Service

Lets say we want to inject one service into another, e.g. inject LoggingService into AccountsService, to achieve this, follow below steps:

  1. Add LoggingService to providers of AppComponent because we want to access this across the application now
    providers: [LoggingService]
  2. Inject LoggingService dependency in constructor of AccountsService
    constructor(private loggingService: LoggingService) {}
  3. Add Injectable() decorator to AccountsService
    @Injectable()
    export class AccountsService { ... }
  4. You can now access LoggingService in AccountsService
    addAccount(name: string, status: string) {
         this.accounts.push({name: name, status: status});
         this.loggingService.loggingStatusChange('Server "' + name + '" status changed to: ' + status);
     }

NOTE

  • If you're using Angular 6+ (check your package.json to find out), you can provide application-wide services in a different way.

  • Instead of adding a service class to the providers[] array in AppModule, you can set the following config in @Injectable():

    @Injectable({providedIn: 'root'})
    export class MyService { ... }

    This is exactly the same as:

    export class MyService { ... }

    and

    import { MyService } from './path/to/my.service'; 
    @NgModule({
      ...
      providers: [MyService]
    })
    export class AppModule { ... }
  • Using this new syntax is completely optional, the traditional syntax (using providers[]) will still work. The "new syntax" does offer one advantage though: Services can be loaded lazily by Angular (behind the scenes) and redundant code can be removed automatically. This can lead to a better performance and loading speed - though this really only kicks in for bigger services and apps in general.


Component Communication using Services

Services can be used to communicate between two components, e.g. here we want to communicate Account component and NewAccount component. To achieve this follow the steps below:

  1. Emit an event in service i.e. AccountsService
    statusUpdated = new EventEmitter<string>();
  2. Make Account component to emit() the property
    this.accountsService.statusUpdated.emit(this.status);
  3. Subscribe this in NewAccount component
    constructor(private loggingService: LoggingService, private accountsService: AccountsService) {
     this.accountsService.statusUpdated.subscribe((status) => console.log('Communication successful! ' + status));
    }
⚠️ **GitHub.com Fallback** ⚠️