Improve this Doc

Understanding Components

In Angular, a Component is a special kind of directive that uses a simpler configuration which is suitable for a component-based application structure.

This makes it easier to write an app in a way that's similar to using Web Components or using Angular 2's style of application architecture.

Advantages of Components:

When not to use Components:

Creating and configuring a Component

Components can be registered using the .component() method of an Angular module (returned by angular.module()). The method takes two arguments:

  Edit in Plunker
angular.module('heroApp', []).controller('mainCtrl', function() {
  this.hero = {
    name: 'Spawn'
  };
});
function HeroDetailController() {

}

angular.module('heroApp').component('heroDetail', {
  templateUrl: 'heroDetail.html',
  controller: HeroDetailController,
  bindings: {
    hero: '='
  }
});
<!-- components match only elements -->
<div ng-controller="mainCtrl as ctrl">
  <b>Hero</b><br>
  <hero-detail hero="ctrl.hero"></hero-detail>
</div>
<span>Name: {{$ctrl.hero.name}}</span>

It's also possible to add components via $compileProvider in a module's config phase.

Comparison between Directive definition and Component definition

Directive Component
bindings No Yes (binds to controller)
bindToController Yes (default: false) No (use bindings instead)
compile function Yes No
controller Yes Yes (default function() {})
controllerAs Yes (default: false) Yes (default: $ctrl)
link functions Yes No
multiElement Yes No
priority Yes No
require Yes Yes
restrict Yes No (restricted to elements only)
scope Yes (default: false) No (scope is always isolate)
template Yes Yes, injectable
templateNamespace Yes No
templateUrl Yes Yes, injectable
terminal Yes No
transclude Yes (default: false) Yes (default: false)

Component-based application architecture

As already mentioned, the component helper makes it easier to structure your application with a component-based architecture. But what makes a component beyond the options that the component helper has?

Example of a component tree

The following example expands on the simple component example and incorporates the concepts we introduced above:

Instead of an ngController, we now have a heroList component that holds the data of different heroes, and creates a heroDetail for each of them.

The heroDetail component now contains the following new functionality:

  Edit in Plunker
var mode = angular.module('heroApp', []);
function HeroListController($scope, $element, $attrs) {
  var ctrl = this;

  // This would be loaded by $http etc.
  ctrl.list = [
    {
      name: 'Superman',
      location: ''
    },
    {
      name: 'Batman',
      location: 'Wayne Manor'
    }
  ];

  ctrl.updateHero = function(hero, prop, value) {
    hero[prop] = value;
  };

  ctrl.deleteHero = function(hero) {
    var idx = ctrl.list.indexOf(hero);
    if (idx >= 0) {
      ctrl.list.splice(idx, 1);
    }
  };
}

angular.module('heroApp').component('heroList', {
  templateUrl: 'heroList.html',
  controller: HeroListController
});
function HeroDetailController($scope, $element, $attrs) {
  var ctrl = this;

  ctrl.update = function(prop, value) {
    ctrl.onUpdate({hero: ctrl.hero, prop: prop, value: value});
  };
}

angular.module('heroApp').component('heroDetail', {
  templateUrl: 'heroDetail.html',
  controller: HeroDetailController,
  bindings: {
    hero: '=',
    onDelete: '&',
    onUpdate: '&'
  }
});
function EditableFieldController($scope, $element, $attrs) {
  var ctrl = this;
  this.editMode = false;

  this.handleModeChange = function() {
    if (ctrl.editMode) {
      ctrl.onUpdate({value: ctrl.fieldValue});
      ctrl.fieldValueCopy = ctrl.fieldValue;
    }
    ctrl.editMode = !ctrl.editMode;
  };

  this.reset = function() {
    ctrl.fieldValue = ctrl.fieldValueCopy;
  };

  this.$onInit = function() {
    // Make a copy of the initial value to be able to reset it later
    this.fieldValueCopy = this.fieldValue;

    // Set a default fieldType
    if (!this.fieldType) {
      this.fieldType = 'text';
    }
  };
}

angular.module('heroApp').component('editableField', {
  templateUrl: 'editableField.html',
  controller: EditableFieldController,
  bindings: {
    fieldValue: '@',
    fieldType: '@',
    onUpdate: '&'
  }
});
<hero-list></hero-list>
<b>Heroes</b><br>
<hero-detail ng-repeat="hero in $ctrl.list" hero="hero" on-delete="$ctrl.deleteHero(hero)" on-update="$ctrl.updateHero(hero, prop, value)"></hero-detail>
<hr>
<div>
  Name: {{$ctrl.hero.name}}<br>
  Location: <editable-field field-value="{{$ctrl.hero.location}}" field-type="text" on-update="$ctrl.update('location', value)"></editable-field><br>
  <button ng-click="$ctrl.onDelete({hero: $ctrl.hero})">Delete</button>
</div>
<span ng-switch="$ctrl.editMode">
  <input ng-switch-when="true" type="text" ng-model="$ctrl.fieldValue">
  <span ng-switch-default>{{$ctrl.fieldValue}}</span>
</span>
<button ng-click="$ctrl.handleModeChange()">{{$ctrl.editMode ? 'Save' : 'Edit'}}</button>
<button ng-if="$ctrl.editMode" ng-click="$ctrl.reset()">Reset</button>

Components as route templates

Components are also useful as route templates (e.g. when using ngRoute). In a component-based application, every view is a component:

var myMod = angular.module('myMod', ['ngRoute']);
myMod.component('home', {
  template: '<h1>Home</h1><p>Hello, {{ $ctrl.user.name }} !</p>',
  controller: function() {
    this.user = {name: 'world'};
  }
});
myMod.config(function($routeProvider) {
  $routeProvider.when('/', {
    template: '<home></home>'
  });
});


When using $routeProvider, you can often avoid some boilerplate, by assigning the resolved dependencies directly to the route scope:

var myMod = angular.module('myMod', ['ngRoute']);
myMod.component('home', {
  template: '<h1>Home</h1><p>Hello, {{ $ctrl.user.name }} !</p>',
  bindings: {user: '='}
});
myMod.config(function($routeProvider) {
  $routeProvider.when('/', {
    template: '<home user="$resolve.user"></home>',
    resolve: {user: function($http) { return $http.get('...'); }}
  });
});

Intercomponent Communication

Directives can require the controllers of other directives to enable communication between each other. This can be achieved in a component by providing an object mapping for the require property. Here is a tab pane example built from components:

  Edit in Plunker
angular.module('docsTabsExample', [])
.component('myTabs', {
  transclude: true,
  controller: function() {
    var panes = this.panes = [];
    this.select = function(pane) {
      angular.forEach(panes, function(pane) {
        pane.selected = false;
      });
      pane.selected = true;
    };
    this.addPane = function(pane) {
      if (panes.length === 0) {
        this.select(pane);
      }
      panes.push(pane);
    };
  },
  templateUrl: 'my-tabs.html'
})
.component('myPane', {
  transclude: true,
  require: {tabsCtrl: '^myTabs'},
  bindings: {
    title: '@'
  },
  controller: function() {
    this.$onInit = function() {
      this.tabsCtrl.addPane(this);
      console.log(this);
    };
  },
  templateUrl: 'my-pane.html'
});
<my-tabs>
  <my-pane title="Hello">
    <h4>Hello</h4>
    <p>Lorem ipsum dolor sit amet</p>
  </my-pane>
  <my-pane title="World">
    <h4>World</h4>
    <em>Mauris elementum elementum enim at suscipit.</em>
    <p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
  </my-pane>
</my-tabs>
<div class="tabbable">
  <ul class="nav nav-tabs">
    <li ng-repeat="pane in $ctrl.panes" ng-class="{active:pane.selected}">
      <a href="" ng-click="$ctrl.select(pane)">{{pane.title}}</a>
    </li>
  </ul>
  <div class="tab-content" ng-transclude></div>
</div>
<div class="tab-pane" ng-show="$ctrl.selected" ng-transclude></div>

Unit-testing Component Controllers

The easiest way to unit-test a component controller is by using the $componentController that is included in ngMock. The advantage of this method is that you do not have to create any DOM elements. The following example shows how to do this for the heroDetail component from above.

The examples use the Jasmine testing framework.

Controller Test:

describe('component: heroDetail', function() {
  var component, scope, hero;

  beforeEach(module('simpleComponent'));

  beforeEach(inject(function($rootScope, $componentController) {
    scope = $rootScope.$new();
    hero = {name: 'Wolverine'};
  }));

  it('should set the default values of the hero', function() {
    // It's necessary to always pass the scope in the locals, so that the controller instance can be bound to it
    component = $componentController('heroDetail', {$scope: scope});

    expect(component.hero).toEqual({
      name: undefined,
      location: 'unknown'
    });
  });

  it('should assign the name bindings to the hero object', function() {
    // Here we are passing actual bindings to the component

    component = $componentController('heroDetail',
      {$scope: scope},
      {hero: hero}
    );
    expect(component.hero.name).toBe('Wolverine');
  });

  it('should call the onDelete binding when a hero is deleted', function() {
    component = $componentController('heroDetail',
      {$scope: scope},
      {hero: hero, onDelete: jasmine.createSpy('deleteSpy')}
    );

    component.onDelete({hero: component.hero});
    expect(spy('deleteSpy')).toHaveBeenCalledWith(component.hero);
  });

});