Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Dependency Injection in Angular 1 vs Angular 2

DZone's Guide to

Dependency Injection in Angular 1 vs Angular 2

Your migration to AngularJS 2 could be a lot less tricky if you take a look at how these common dependency injection scenarios translate from Angular 1 to 2.

· Web Dev Zone
Free Resource

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

In this article I will show how common scenarios of using dependency injection in Angular 1 can be implemented in Angular 2. 

Code Samples in TypeScript 

I wrote the code samples in TypeScript because I am a big fan of the language. This does not mean you have to use it when building applications in Angular 2. The framework works great with ES5 and ES6.

Let’s Start with a Simple Component

Let’s start by implementing a simple login component.

// Angular 1: Basic Implementation of Login Component

class Login {
    formValue: { login: string, password: string } = {login:'', password:''};

    onSubmit() {
        const service = new LoginService();
        service.login(this.formValue);
    }
}

module.directive("login", () => {
    return {
        restrict: 'E',
        scope: {},
        controller: Login,
        controllerAs: 'ctrl',
        template: `
            <form ng-submit="ctrl.onSubmit()">
                Text <input type="text" ng-model="ctrl.formValue.login">
                Password <input type="password" ng-model="ctrl.formValue.password"><button>Submit</button>
            </form>
        `
  };
});

Now, the same component implemented in Angular 2.

// Angular 2: Basic Implementation of Login Component

@Component({ selector: 'login' })
@View({
  template: `
        <form>
            Text <input type="text" ng-control="login">
            Password <input type="password" ng-control="password"><button>Submit</button>
        </form>
    `
})
class Login {
    onSubmit(formValue: { login: string, password: string }) {
        const service = new LoginService();
        service.login(formValue);
    }
}

Experienced developers know that coupling of the login component to the login service is problematic. It is hard to test this component in isolation. And it also makes it less reusable. If we have two applications, with two login services, we will not be able to reuse our login component.

We can remedy it by monkey patching the system loader, so we can replace the login service, but this is not the right solution. We have a fundamental problem with our design, which DI can help us fix.

Using Dependency Injection

We can solve our problem by injecting an instance of LoginService into the constructor instead of creating it directly.

// Angular 1: Login Component Using DI

class Login {
    formValue: { login: string, password: string } = { login: '', password: '' };

    constructor(public service: LoginService) { }

    onSubmit() {
        this.service.login(this.formValue);
    }
}

module.directive("login", () =&gt; {
    return {
        restrict: 'E',
        scope: {},
        controller: ["login", Login],
        controllerAs: 'ctrl',
        template: `
            <form ng-submit="ctrl.onSubmit()">
                Text <input type="text" ng-model="ctrl.formValue.login">
                Password <input type="password" ng-model="ctrl.formValue.password"><button>Submit</button>
            </form>
        `
  };
});

Now, we need to tell the framework how to create an instance of the service.

module.service("login", LoginService);

All right, let’s port this example to Angular 2.

// Angular 2: Login Component Using DI

@Component({ selector: 'login' })
@View({
  template: `
        <form>
            Text <input type="text" ng-control="login">
            Password <input type="password" ng-control="password"><button>Submit</button>
        </form>
    `
})
class Login {
  constructor(public service: LoginService) {}

    onSubmit(formValue: { login: string, password: string }) {
    this.service.login(formValue);
    }
}

As with Angular 1, we need to tell the framework how to create the service. In Angular 2, the directive and component decorators are the place where you configure dependency injection bindings. In this example, the App component makes LoginService available for itself and all its descendants, including the login component. An instance of the login service will be created next to the App component. So if multiple children depend on it, all of them will get the same instance.

@Component({
  selector: 'app',
  bindings: [LoginService]
})
@View({template: `<login></login>`, directives: [Login]})
class App {}

We separated the two concerns: the login component now depends on some abstract login service, and the app component creates a concrete implementation of the service. As a result, the login component no longer cares what implementation of the login service it will get. This means that we can test our component in isolation. And we can use it in multiple applications.

Note, that Angular 1 relies on strings to configure dependency injection. Angular 2 by default uses type annotations, but there is a way to fall back on strings when more flexibility is required.

Using Different Login Service

We can configure our application to use another implementation of the login service.

// Angular 2: Using SomeOtherLoginService

@Component({
  selector: 'app',
  bindings: [bind(LoginService).toClass(SomeOtherLoginService)]
})
@View({template: `<login></login>`, directives: [Login]})
class SomeOtherApp {}

Configuring Login Service

One of the great things about dependency injection is that we do not have to worry about the dependencies of our dependencies. The login component depends on the login service, but it does not need to know what the service itself depends upon.

Let’s say the service requires some configuration object. In Angular 1, it can be done as follows:

// Angular 1: Configuring LoginService

class LoginService {
    constructor(public config: {url: string}) {}
  //...
}

module.value("LoginServiceConfig", { url: LOGIN_URL });
module.service("login", ["LoginServiceConfig", LoginService]);

Now, the Angular 2 version:

// Angular 2: Configuring LoginService

@Injectable() class LoginService {
  constructor(@Inject("LoginServiceConfig") public config: {url: string}) {}
  //...
}

@Component({
  selector: 'app',
  bindings: [
        LoginService,
        bind("LoginServiceConfig").toValue({url: 'myurl')
    ]
})
@View({template: `<login></login>`, directives: [Login]})
class App {}

Injecting the Component’s Element

There is often a need for a component to interact with its DOM element. This is how it can be done in Angular 1:

// Angular 1: Injecting Element

class NeedsElement {
    element;
}

module.directive("needsElement", () =&gt; {
    return {
        restrict: 'E',
        scope: {},
        controller: NeedsElement,
        controllerAs: 'ctrl',
        template: `some template`,
        link: (scope, el, attrs, controllers) =&gt; {
            scope.ctrl.element = el;
        }
    };
});

Angular 2 does a much better job here. It uses the same dependency injection mechanism to inject the element into the component’s constructor.

// Angular 2: Injecting Element

@Component({selector: 'needs-element'})
@View({template: 'some template'})
class NeedsElement {
  constructor(el: ElementRef) {
    el.nativeElement // the DOM element
  }
}

Injecting Other Directives

It is also quite common to have multiple directives working together. For instance, if you have an implementation of tabs and panes, the tab component will need to know about the pane components.

This is how it can be done in Angular 1.

// Angular 1: Injecting Directives

class Tab {
    addPane(pane) { }
    //...
}

class Pane {
  tab: Tab;

  setTab(tab: Tab) {
    this.tab = tab;
    tab.addPane(this);
  }
    //...
}

module.directive('tab', function() {
  return {
    restrict: 'E',
    scope: {},
    controller: Tab,
    templateUrl: 'tab.html'
  };
});

module.directive('pane', function() {
  return {
    require: '^tab',
    restrict: 'E',
        controller: Pane,
        controllerAs: 'ctrl',
    link: function(scope, element, attrs, tabCtrl) {
      scope.ctrl.setTab(tabCtrl);
    },
    templateUrl: 'pane.html'
  };
});

We use the require property to get access to the tab controller. Then, we use scope to get access to the pane controller. And, finally, we use the link function to connect the two. This is quite a bit of work for such a simple scenario.

And, once again, Angular 2 does a much better job here.

// Angular 2: Injecting Directives

@Component({selector: 'tab'})
@View({template: 'tab.html'})
class Tab {
    addPane(pane) { }
    //...
}

@Component({selector: 'pane'})
@View({template: 'pane.html'})
class Pane {
  constructor(public tab: Tab) {
    tab.addPane(this);
  }
    //...
}

But we can do even better than that! Instead of panes registering themselves with the closest tab, the tab component can query for panes.

// Angular 2: Injecting Directives (Using Query)

@Component({selector: 'tab'})
@View({template: 'tab.html'})
class Tab {
    constructor(@Query(Pane) panes: QueryList<Pane>) {}
    //...
}

@Component({selector: 'pane'})
@View({template: 'pane.html'})
class Pane {
}

Query takes care of many of the problems developers face when implementing this in Angular 1:

  • The panes are always in order.
  • The query will notify the tab component about changes.
  • There is no need for Pane to know about Tab. The Pane component is easier to test and reuse.

Single API

Angular 1 has several APIs for injecting dependencies into directives. Who hasn’t been confused by the difference between factory, service, provider, constant and value? Some objects are injected by position (e.g., element), some by name (e.g., LoginService). Some dependencies are always provided (e.g., element in link), some have to be configured using require, and some are configured using parameter names.

Angular 2 provides a single API for injecting services, directives, and elements. All of them get injected into the component’s constructor. As a result, there is a lot less API to learn. And your components are much easier to test.

But how does it work? How does it know what element to inject when a component asks for one? The way it works is as follows:

The framework builds a tree of injectors that matches the DOM.

<tab><pane title="one"></pane><pane title="two"></pane></tab>

The matching injector tree:

Injector matching <tab>
  |
  |__Injector matching <pane title="one">
  |
  |__Injector matching <pane title="two">

Since there is an injector for every DOM element, the framework can provide contextual or local information, such as an element, attributes, or nearby directives.

This is how the dependency resolution algorithm works.

// this is pseudocode.
var inj = this;
while (inj) {
  if (inj.has(requestedDependency)) {
        return inj.get(requestedDependency);
    } else {
        inj = inj.parent;
    }
}
throw new NoBindingError(requestedDependency);

So if Pane depends on Tab, Angular will start by checking if the pane element happens to have an instance of Tab. If it does not, it will check the parent’s element. It will repeat the process until either it finds an instance of Tab or it reaches the root injector.

You can point to any element on the page, and by using ngProbe get its injector. You can also see an element’s injector when an exception is thrown.

Screenshot: Exception

I know that it may seem a little bit complicated, but the truth is Angular 1 already has a similar mechanism. You can inject nearby directives using require. But this mechanism is undeveloped in Angular 1, and that is why we cannot not fully take advantage of it.

Angular 2 takes this mechanism to its logical conclusion. And turns out we do not need the other mechanisms any more.

Advanced Examples

So far, we have looked at examples that worked in both Angular 1 and Angular 2. Now, I want to show you a few advanced examples that just cannot be expressed in Angular 1.

Optional Dependencies

To mark a dependency as optional, use the Optional decorator.

class Login {
  constructor(@Optional() service: LoginService) {}
}

Controlling Visibility

You can be more specific where you want to get dependencies from. For instance, you can ask for another directive on the same element.

class CustomInputComponent {
  constructor(@Self() f: FormatterDirective) {}
}

Or you can ask for a directive in the same view.

class CustomInputComponent {
  constructor(@Host() f: CustomForm) {}
}

You can read more about it here.

Providing Two Implementations of the Same Service

Since Angular 1 has only one injector object, you cannot have two implementations of LoginService in the same app. In Angular 2, where every element has an injector, this is not a problem.

@Component({
  selector: 'sub-app',
  bindings: [bind(PaymentService).toClass(CustomPaymentService1)]
})
@View({templateUrl: `subapp1.html`})
class SubApp1 {}

@Component({
  selector: 'sub-app',
  bindings: [bind(PaymentService).toClass(CustomPaymentService2)]
})
@View({templateUrl: `subapp2.html`})
class SubApp2 {}

@Component({
  selector: 'app'
})
@View({template: `<sub-app-1></sub-app-1><sub-app-2></sub-app-2>`})
class App {}

The services and directives created under SubApp1 will use CustomPaymentService1, and the ones created under SubApp2 will use CustomPaymentService2, even though all of them declare a dependency on PaymentService.

View Bindings

The following makes LoginService available for injection in both the public children (a.k.a. light dom children) of App and its view.

@Component({
  selector: 'app',
  bindings: [LoginService]
})
@View({templateUrl: 'app.html'})
class App {}

Sometimes, you just want ot make a binding available in the component’s view. This is how you can do it:

@Component({
  selector: 'app',
  viewBindings: [LoginService]
})
@View({templateUrl: 'app.html'})
class App {}


Summary

  • Dependency injection is one of the core parts of Angular 2.
  • It allows you to depend on interfaces, not concrete types.
  • This results in more decoupled code.
  • This improves testability.
  • Angular 2 has one API for injecting dependencies into components.
  • Dependency injection in Angular 2 is more powerful.

Read More


Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:
angularjs ,angularjs 2

Published at DZone with permission of Victor Savkin, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}