AngularJS1.5を使っていて、transcludeで中に要素を埋め込むようなコンポーネントで、その埋め込まれる要素内からコンポーネントが提供する機能にアクセスする、ということをしたかった。例えばダイアログ的な枠組みを提供するコンポーネントでtranscludeで内容を埋め込めて、その中のボタンからダイアログを閉じる、みたいな。

しかしtranscludeされる要素は枠組みのコンポーネントとは別の孤立したスコープになってしまい、コンポーネントにはアクセスできないようだった。アクセスさせるにはディレクティブのlinktranscludeの制御をする必要があるようだ。

利用側

// Application
angular.module('app')
  .component('app', {
    template: (
      '<h1>App</h1>' +
      '<my-dialog>' +
      '  <div style="border:1px solid blue; margin:4px">' +
      '    <h3>Transcluded contents</h3>' +
      '    <button ng-click="closeDialog()">ok</button>' +
      '  </div>' +
      '</my-dialog>' +
      '<h2>Outside of dialog</h2>' +
      '<button ng-click="closeDialog()">not worked (as expected)</button>'
      ),
  })
  • my-dialog がダイアログを提供するディレクティブだとして、その中にレイアウトを記述したものが埋め込まれる、という想定
  • transcludeされる要素内からダイアログが提供するcloseDialog()という関数を呼び出して、ディレクティブ側に通知する
  • transclude外からはその機能は呼べない

ディレクティブ側

// Directive using transclude
angular.module('app')
  .directive('myDialog', function() {
    return {
      scope: {},  // ディレクティブが独立したスコープを持つようにする
      link: function(scope, element, _attrs, _controller, transcludeFn) {
        scope.message = '.'
        // トランスクルードする要素を生成
        transcludeFn(scope.$parent.$new(), function(clone, transcludeScope) {
          var count = 0
          transcludeScope.closeDialog = function() {
            scope.message = 'Dialog closed: ' + (++count)
          }
          // クローンした要素を埋め込む
          var root = element[0].querySelector('#transclude-point')
          root.innerHTML = ''
          angular.forEach(clone, e => {root.appendChild(e)})
        })
      },
      template: (
        '<div style="border:1px solid black">' +
        '  <h2>Dialog</h2>' +
        '  <div ng-bind="message"></div>' +
        '  <div id="transclude-point"></div>' +  // ng-transcludeを使う代わりに埋め込むポイントを探せるようにしておく
        '</div>'
        ),
      transclude: true,
    }
  })
  • angularcomponentだとスコープやコントローラにアクセスする方法がわからなかったので、directiveとして作成
  • transclude: trueでトランスクルードを有効にする
  • template内でng-transcludeを使って埋め込むと、孤立したスコープでtransclude要素が埋め込まれてしまってアクセスする方法がないため、link関数の第5引数のtranscludeFnを使用してtranscludeする要素をクローンして自前で目的の場所に埋め込む
    • その際のスコープはディレクティブのスコープの親から$newして、ディレクティブとは別のスコープにしておく
    • その新しいスコープに提供したい関数(closeDialog)を定義してやることでtransclude内から参照できるようになる

デモ

http://plnkr.co/edit/ADbo0K?p=preview

その他

  • スコープは廃止される、ということなのであまりよくない方法かも…
  • 試してないけどAngular2の場合には外側のコンポーネントから@ViewChildで内側のコンポーネントのコントローラが取得できるので、それ経由でアクセスすればそさそう

リンク