AngularJS: как передать объект из директивы во включенный шаблон

У меня есть директива, которая создает пользовательский интерфейс, позволяющий пользователю выполнять поиск. Директива оборачивает содержимое, которое будет включено и станет шаблоном для каждого отдельного результата поиска. Что-то вроде этого:

<search>
  <div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
</search>

Я бы хотел, чтобы ng-click вызывал функцию selectResult в области действия контроллера, но чтобы объект result исходил из директивы. Как я могу выполнить это с изолированной областью действия в директиве?




Ответы (3)


Вместо использования ng-transclude вы можете создать собственную директиву для поиска, которую можно использовать для помещения результата в включенную область. Например, ваша директива поиска может выглядеть примерно так с ng-repeat и директивой search-transclude, где вы хотите включить контент:

.directive("search", function (SearchResults) {
    return {
        restrict: "AE",
        transclude: true,
        scope: {},
        template: '<div ng-repeat="result in results">Search Relevance:' +
        '{{result.relevance}}' +
        //the most important part search-transclude that receives the current
        //result of ng-repeat
        '<div search-transclude result="result"></div></div>',
        link: function (scope, elem, attrs) {
            //get search results
            scope.results = SearchResults.results;
        }
    }
})

Создайте директиву поиска transclude следующим образом:

.directive("searchTransclude", function () {
    return {
        restrict: "A",
        link: function (scope, elem, attrs, ctrl, $transclude) {
            //create a new scope that inherits from the parent of the
            //search directive ($parent.$parent) so that result can be used with other
            //items within that scope (e.g. selectResult)
            var newScope = scope.$parent.$parent.$new();
            //put result from isolate to be available to transcluded content
            newScope.result = scope.$eval(attrs.result);
            $transclude(newScope, function (clone) {
                elem.append(clone);
            });
        }
    }
})

Включенный контент теперь сможет видеть функцию selectResult, если он существует в области действия, в которой была создана директива поиска. Пример здесь.

person Patrick    schedule 03.12.2014
comment
Я пытаюсь использовать ваш пример в своем коде, но $transclude не определен. Любая идея, что я могу делать неправильно? - person adam0101; 04.12.2014
comment
Я использую контроллер в директиве поиска, может ли это вызвать проблемы? - person adam0101; 04.12.2014
comment
Я также использую вложенные повторы ng. Может из-за этого, но я не знаю, как это исправить. - person adam0101; 04.12.2014
comment
Нет, то, что вы предположили, было правильным, просто у меня также есть контроллер в директиве, и я подумал, что, возможно, это вызывает проблему, но я удалил его, и 5-й параметр все еще входит как «неопределенный». Я использую версию 1.2.15. Как вы думаете, поскольку я использую вложенные повторы ng, мне также нужно будет использовать вложенные директивы searchTransclude? - person adam0101; 04.12.2014
comment
Возможно, добавьте transclude: true в директиву search-transclude. У меня было transclude: true в самой директиве поиска в моем примере. Это необходимо установить, чтобы предоставить файл transcludeFn. - person Patrick; 04.12.2014
comment
Добавление transclude: true в search-transclude сработало, чтобы $transclude не было неопределенным, но я все еще не вижу свой контент. Я не понимаю, почему ваш jsfiddle работал без transclude: true для search-transclude, а мой код - нет. - person adam0101; 04.12.2014
comment
Когда я просматриваю код и смотрю на elem, это <div search-transclude result="result"></div>, но clone также <div search-transclude result="result"></div>. Так что это как бы добавляет себя к себе. Я весьма озадачен. - person adam0101; 04.12.2014
comment
Можете ли вы разместить то, что вы делаете, на jsfiddle, чтобы я мог видеть, что происходит. Пример, который я привел, работает, но не могу подробнее остановиться на вашей настройке. - person Patrick; 04.12.2014
comment
Я наконец-то понял. transclude:true не требуется при поиске-переводе. Проблема была вызвана ng-show на внешнем ng-repeat! Выражение было верным, само его существование вызывало ошибку! Я попытался переместить его в родительский div, но все еще были проблемы. Только после того, как я изменил его на ng-if, появилась функция $transclude и контент. Я понятия не имею, почему это было проблемой, но, по крайней мере, я заработал. Спасибо. - person adam0101; 05.12.2014
comment
Также мне пришлось добавить еще один .$parent в search-transclude из-за вложенного ng-repeat. - person adam0101; 05.12.2014

Включенный контент всегда будет использовать область, в которой находится элемент директивы, то есть область действия вашего контроллера. Вот почему, если вы хотите, чтобы аргумент result функции selectResult получал значение из изолированной области, вам необходимо установить двустороннюю привязку между свойствами result изолированной области и области контроллера. После установки свойства result на желаемое значение в изолированной области свойство области result контроллера будет обновлено до того же значения. Таким образом, включенный контент будет использовать result контроллера, который синхронизирован с result изолированной области.

1) добавить атрибут resultAttr='result' в элемент директивы.

<search resultAttr='result'> <div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div> </search>

2) установить двустороннюю привязку для свойства result, когда вы определяете изолированную область в директиве:

scope: { result: "=resultAttr" }

3) установить result в какое-то значение в директиве

person Suren Aznauryan    schedule 03.12.2014
comment
Я хочу сделать ng-repeat в шаблоне директивы и использовать включенный контент в качестве шаблона для каждого результата. В вашем примере не будут ли все они использовать один и тот же объект результата? - person adam0101; 03.12.2014
comment
для случая ng-repeat это не будет работать, потому что ng-repeat создаст свою собственную область в качестве дочерней области области действия директивы, и все свойства result из каждой итерации ng-repeat будут в новой дочерней области действия директивы. - person Suren Aznauryan; 03.12.2014
comment
у вас есть причина использовать здесь трансклюзию? - person Suren Aznauryan; 03.12.2014
comment
Потому что я хочу повторно использовать директиву поиска, но разрешить потребителю директивы форматировать результаты по своему усмотрению. - person adam0101; 03.12.2014
comment
вы можете попытаться избавиться от изолированной области действия директивы и установить область действия директивы, которая будет унаследована от контроллера (установите scope: true в вашей директиве). Затем измените <div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div> на <div class="someStyle" ng-click="selectResult($$child.$$child.result)">{{$$child.$$child.result.Name}}</div> - person Suren Aznauryan; 04.12.2014

Я бы хотел, чтобы ng-click [в директиве] вызывал функцию selectResult в области действия контроллера...

  1. Чтобы передать функции (или свойства) в изолированную область, вы используете атрибуты тега директивы.

... но получить объект результата из директивы [scope].

  1. Если вы хотите, чтобы содержимое тега директивы имело доступ к области действия директивы, вы НЕ используете transclude. Указание transclude: true сообщает angular НЕ разрешать содержимому тега директивы иметь доступ к области действия директивы - противоположное тому, что вы хотите.

Чтобы выполнить пункт 1, вы можете попросить пользователя указать шаблон следующим образом:

  <div ng-controller="MainCtrl">

    <search external-func='selectResult'>
      <div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
    </search>

  </div>

Обратите внимание, что пользователю необходимо добавить дополнительный атрибут к тегу <search>. Тем не менее, этот html может лучше соответствовать философии angular, согласно которой html должен давать разработчику подсказки о том, какой javascript будет работать с элементами.

Затем вы указываете область изоляции следующим образом:

      scope: {
        selectResult: '=externalFunc'
      },

Для выполнения #2 не указывайте transclude: true в директиве:

var app = angular.module('myApp',[]);

app.controller('MainCtrl', ['$scope', function($scope) {

  $scope.selectResult = function(result) {
    console.log("In MainCtrl: " + result.Name);
  };

}]);

app.controller('DirectiveCtrl', ['$scope', function($scope) {

  $scope.results = [ 
    {Name: "Mr. Result"},
    {Name: "Mrs. Result"}
  ]

}]);

app.directive('search', function() {

  return {
      restrict: 'E',

      scope: {
        selectResult: '=externalFunc'
      },

      template: function(element, attrs) {
      //                    ^       ^
      //                    |       |
      //    directive tag --+       +-- directive tag's attributes

        var inner_div = element.children();
        inner_div.attr('ng-repeat', 'result in results')

        //console.log("Inside template func: " + element.html());

        return element.html();  //Must return a string.  The return value replaces the innerHTML of the directive tag.
      },

      controller: 'DirectiveCtrl'
  }

}]);

HTML может обеспечить еще лучшую запись того, что делает javascript, если вы заставите пользователя указать свой шаблон более подробно:

<search external-func='selectResult'>
  <div class="someStyle" 
    ng-click="selectResult(result)"
    ng-repeat="result in results">{{result.Name}}
  </div>
</search>

Но если вы настаиваете на минималистичном html:

<search>
  <div class="someStyle" ng-click="selectResult(result)">{{result.Name}}</div>
</search>

... затем вы можете динамически добавить атрибут ng-repeat (как показано выше), а также можно динамически сопоставить внешнюю функцию с областью изоляции:

var app = angular.module('myApp',[]);

app.controller('MainCtrl', ['$scope', function($scope) {

  $scope.selectDog = function(result) {
    console.log("In MainCtrl: you clicked " + result.Name);
  };

  $scope.greet = function(result) {
    console.log('MainCtrl: ' + result.Name);
  };

}]);

app.controller('DirectiveCtrl', ['$scope', function($scope) {

  $scope.results = [ 
    {Name: "Mr. Result"},
    {Name: "Mrs. Result"}
  ]

}]);

app.directive('search', function() {

  return {
    restrict: 'E',

    scope: {
      externalFunc: '&externalFunc'  //Cannot write => externalFunc: '&'
    },                               //because the attribute name is
                                     //'external-func', which means
                                     //the left hand side would have to be external-func.
    template: function(element, attrs) {
      //Retrieve function specified by ng-click:
      var inner_div = element.children();
      var ng_click_val = inner_div.attr('ng-click'); //==>"selectResult(result)"

      //Add the outer_scope<==>inner_scope mapping to the directive tag:
      //element.attr('external', ng_click_val); //=> No worky! Angular does not create the mapping.
      //But this works:
      attrs.$set('externalFunc', ng_click_val) //=> external-func="selectResult(result)"
      //attrs.$set('external-func', ng_click_val); //=> No worky!

      //Change ng-click val to use the correct call format:
      var func_args = ng_click_val.substring(ng_click_val.indexOf('(')); //=> (result)
      func_args =  func_args.replace(/[\(]([^\)]*)[\)]/, "({$1: $1})"); //=> ({result: result})
      inner_div.attr('ng-click', 'externalFunc' + func_args); //=> ng-click="externalFunc({result: result})"

      //Dynamically add an ng-repeat attribute:
      inner_div.attr('ng-repeat', 'result in results')

      console.log("Template: " + element[0].outerHTML);
      return element.html();
    },

    controller: 'DirectiveCtrl'
  }
})

Если вы хотите вызвать внешнюю функцию с более чем одним аргументом, вы можете сделать это:

var app = angular.module('myApp',[]);

app.controller('MainCtrl', ['$scope', function($scope) {

  $scope.selectResult = function(result, index) {
    console.log("In MainCtrl: you clicked " 
                 +  result.Name 
                 + " " 
                 + index);
  };

}]);

app.controller('DirectiveCtrl', ['$scope', function($scope) {

  $scope.results = [ 
    {Name: "Mr. Result"},
    {Name: "Mrs. Result"}
  ]

}]);

app.directive('search', function() {
  return {
    restrict: 'E',

    scope: {
      external: '='
    },

    template: function(element, attrs) {
      //Extract function name specified by ng-click:
      var inner_div = element.children();
      var ng_click_val = inner_div.attr('ng-click'); //=>"selectResult(result, $index)"
      var external_func_name =  ng_click_val.substring(0, ng_click_val.indexOf('(') ); //=> selectResult
      external_func_name = external_func_name.trim();

      //Add the outer_scope<==>inner_scope mapping to the directive tag:
      //element.attr('externalFunc', ng_click_val); => No worky!
      attrs.$set('external', external_func_name);  //=> external="selectResult"

      //Change name of ng-click function to 'external':
      ng_click_val = ng_click_val.replace(/[^(]+/, 'external');
      inner_div.attr('ng-click', ng_click_val);

      //Dynamically add ng-repeat to div:
      inner_div.attr('ng-repeat', 'result in results');

      console.log("Template: " + element[0].outerHTML);
      return element.html();
    },

    controller: 'DirectiveCtrl'
  }
});
person 7stud    schedule 20.12.2014
comment
Что мне не нравится в этом подходе, так это то, что директива должна знать реализацию шаблона, чтобы работать. Что, если бы я хотел вызвать две разные функции? Мне не нужно менять директиву, чтобы сделать это. Может есть способ передать в директиву саму область видимости, чтобы из шаблона можно было вызывать любую функцию? - person adam0101; 22.12.2014
comment
@ adam0101, директиве не нужно ничего знать о шаблоне - к сожалению, я испортил имена своих переменных, поэтому изменение имени функции в шаблоне не работает. Я опубликую необходимые исправления. - person 7stud; 23.12.2014
comment
Но вы жестко закодировали его только для одной функции, и это должен быть ng-щелчок. Что, если я хочу, чтобы одна функция выбирала, а другая отображала более подробную информацию при наведении курсора? Я должен изменить директиву. Но я думаю, ты на что-то наткнулся. Если бы я передал external объект, инкапсулирующий все функции, которые я хотел бы вызвать, не мог бы я получить доступ к этим функциям в шаблоне, выполнив ng-click="external.mySelectFunction()"? - person adam0101; 23.12.2014
comment
Но вы жестко запрограммировали его только на одну функцию. Нет. В шаблоне (т. е. ‹div›) можно указать любое имя функции, и тогда соответствующая функция будет вызываться во внешнем контроллере. и это должен быть ng-click. Можете ли вы указать на что-либо, что вы опубликовали, в котором говорилось (или подразумевалось), что вы хотите, чтобы имя функции, НЕ указанное ng-click, вызывалось во внешней области? - person 7stud; 23.12.2014
comment
Нет, директива, которую вы написали, может принимать только одну функцию. Что делать, если я хочу 6 разных кнопок в шаблоне? Я не должен заявлять или подразумевать, что директива не должна требовать явного знания того, как потребитель этой директивы собирается реализовать шаблон. Принятый ответ не имеет этого ограничения, и я думаю, что ваш ответ тоже не будет, если вы измените external на объект, который инкапсулирует любую возможную функцию, которую шаблон может захотеть вызвать. - person adam0101; 23.12.2014