Todd Motto의 글 Directive to Directive communication with “require”를 번역한 글이다. 짧은 글이지만 디렉티브의 계층 관계에서 require를 활용해 값을 주고 받는 방법을 살펴볼 수 있다. 다른 디렉티브의 컨트롤러에 정의된 메소드를 어떻게 접근해서 사용할 수 있는지 탭 디렉티브를 작성하는 예제를 통해 설명한다.


디렉티브 간 소통은 여러 방법이 있지만, 계층 관계를 갖고 있는 디렉티브를 다룰 때는 디렉티브 컨트롤러를 활용해 서로 소통할 수 있다.

이 글에서는 탭 디랙티브를 작성한다. 탭을 추가하기 위한 다른 디랙티브의 함수를 활용하며, 디렉티브 정의 개체의 require 프로퍼티를 사용해 만들려고 한다.

HTML을 먼저 정의한다:

<tabs>
  <tab label="Tab 1">
    Tab 1 contents!
   </tab>
   <tab label="Tab 2">
    Tab 2 contents!
   </tab>
   <tab label="Tab 3">
    Tab 3 contents!
   </tab>
</tabs>

이 시점에서 tabstab 두 디렉티브를 만들 것을 예상할 수 있다. tabs를 먼저 만들면:

function tabs() {
  return {
    restrict: 'E',
    scope: {},
    transclude: true,
    controller: function () {
      this.tabs = [];
    },
    controllerAs: 'tabs',
    template: `
      <div class="tabs">
        <ul class="tabs__list"></ul>
        <div class="tabs__content" ng-transclude></div>
      </div>
    `
  };
}

angular
  .module('app', [])
  .directive('tabs', tabs);

tabs 디렉티브에서는 transclude를 사용해 각각의 tab을 전달하고 개별적으로 관리하도록 구성하고 있다.

tabs 컨트롤러 내에서 새로운 탭을 추가할 때 사용할 함수가 필요하다. 이 함수를 사용해 부모/호스트 디렉티브에 동적으로 탭을 추가할 수 있게 된다:

function tabs() {
  return {
    ...
    controller: function () {
      this.tabs = [];
      this.addTab = function addTab(tab) {
        this.tabs.push(tab);
      };
    },
    ...
  };
}

angular
  .module('app', [])
  .directive('tabs', tabs);

이제 컨트롤러에 addTab 메소드가 연결되었다. 하지만 탭을 어떻게 추가할 것인가? 자식 tab 디렉티브를 추가하고, 이 디렉티브가 컨트롤러의 기능으로서 필요로 한다:

function tab() {
  return {
    restrict: 'E',
    scope: {
      label: '@'
    },
    require: '^tabs',
    transclude: true,
    template: `
      <div class="tabs__content" ng-if="tab.selected">
        <div ng-transclude></div>
      </div>
    `,
    link: function ($scope, $element, $attrs) {

    }
  };
}

angular
  .module('app', [])
  .directive('tab', tab)
  .directive('tabs', tabs);

require: '^tabs'를 추가하는 것으로 부모로 tabs 디렉티브의 컨트롤러에 포함했으며 이제 link 함수를 통해 접근할 수 있게 되었다. link 함수의 4번째 인자인 $ctrl을 주입해서 작성한 컨트롤러의 참조를 받아오자:

function tab() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {

    }
  };
}

여기서 console.log($ctrl);을 넣어보면 다음과 비슷한 객체를 볼 수 있다:

{
  tabs: Array,
  addTab: function addTab(tab)
}

addTab 함수를 활용해서 새로운 탭을 생성할 때, 부모 디렉티브의 컨트롤러로 정보를 보낼 수 있게 되었다:

function tab() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {
      $scope.tab = {
        label: $scope.label,
        selected: false
      };
      $ctrl.addTab($scope.tab);
    }
  };
}

이제 새로운 tab 디렉티브를 사용할 때마다 이 $ctrl.addTab 함수를 호출하고 tabs 컨트롤러 내에 있는 this.tabs 배열에 디렉티브 정보를 전달한다.

3개의 탭이 존재한다면 $ctrl.addTab 함수가 3번 호출 될 것이고 배열은 3개의 값을 갖고 있게 된다. 그 후 배열을 반복해서 살펴보고 제목과 선택되어 있는 탭이 있는지 확인한다:

function tabs() {
  return {
    restrict: 'E',
    scope: {},
    transclude: true,
    controller: function () {
      this.tabs = [];
      this.addTab = function addTab(tab) {
        this.tabs.push(tab);
      };
      this.selectTab = function selectTab(index) {
        for (var i = 0; i < this.tabs.length; i++) {
          this.tabs[i].selected = false;
        }
        this.tabs[index].selected = true;
      };
    },
    controllerAs: 'tabs',
    template: `
      <div class="tabs">
        <ul class="tabs__list">
          <li ng-repeat="tab in tabs.tabs">
            <a href="" ng-bind="tab.label" ng-click="tabs.selectTab($index);"></a>
          </li>
        </ul>
        <div class="tabs__content" ng-transclude></div>
      </div>
    `
  };
}

selectTabtabs 컨트롤러에 추가된 것을 확인할 수 있을 것이다. 이 함수는 특정 탭의 컨텐츠를 보여주기 위해 초기 색인을 지정할 수 있게 한다. this.selectTab(0);를 호출하면 작성한 코드에 따라 배열의 인덱스를 확인해 첫번째 탭의 컨텐츠를 표시하게 된다.

Angular의 컴파일링 과정에 따라 controller는 가장 먼저 인스턴스가 생성되고, link 함수는 디렉티브가 컴파일되고 엘리먼트에 연결될 때 한 번 호출된다. 즉, 초기화 된 탭을 볼 수 있을 때, 디렉티브 컨트롤러를 $ctrl와 그 메소드를 사용하기 위해 주입되어야 한다:

function tabs() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {
      // 첫번째 탭을 가장 먼저 보여줌
      $ctrl.selectTab(0);
    },
  };
}

하지만 다음처럼 어트리뷰트로 초기 탭을 지정할 수 있다면, 개발자에게 더 많은 선택권을 제공할 수 있다:

<tabs active="2">
  <tab>...</tab>
  <tab>...</tab>
  <tab>...</tab>
</tabs>

이 코드는 배열 인덱스를 동적으로 2로 지정하며 배열에서 3번째 엘리먼트를 보여주게 된다. link 함수에서는 어트리뷰트의 존재를 $attrs가 포함하고 있는데 이 인덱스를 바로 지정하거나 $attrs.active가 존재하지 않거나 잘못된 값일 경우 (false으로 평가되니 어쨌든 이므로 안전하겠지만) 초기 인덱스를 다음처럼 폴백(fallback)으로 지정할 수 있다.

function tabs() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {
      // `active` 탭 또는 첫번째 탭을 지정
      $ctrl.selectTab($attrs.active || 0);
    },
  };
}

그리고 require를 이용해 새로운 tab 정보를 부모 디렉티브에 전달하는 라이브 데모는 아래에서 확인할 수 있다:

{{< //jsfiddle.net/toddmotto/4comjcdm/embedded/result,js,html >}}

색상을 바꿔요

눈에 편한 색상을 골라보세요 :)

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.