Jak mogę dynamicznie dodać dyrektywę w AngularJS?

212

Mam bardzo spartaczoną wersję tego, co robię, przez co problem się pojawia.

Mam prosty directive. Każde kliknięcie elementu powoduje dodanie kolejnego. Jednak najpierw trzeba go skompilować, aby poprawnie wyrenderować.

Doprowadziły mnie moje badania $compile. Ale wszystkie przykłady wykorzystują skomplikowaną strukturę, której tak naprawdę nie wiem, jak się tutaj zastosować.

Fiddles są tutaj: http://jsfiddle.net/paulocoelho/fBjbP/1/

A JS jest tutaj:

var module = angular.module('testApp', [])
    .directive('test', function () {
    return {
        restrict: 'E',
        template: '<p>{{text}}</p>',
        scope: {
            text: '@text'
        },
        link:function(scope,element){
            $( element ).click(function(){
                // TODO: This does not do what it's supposed to :(
                $(this).parent().append("<test text='n'></test>");
            });
        }
    };
});

Rozwiązanie Josh David Miller: http://jsfiddle.net/paulocoelho/fBjbP/2/

PCoelho
źródło

Odpowiedzi:

259

Masz tam wiele bezsensownych jQuery, ale usługa kompilacji $ jest w tym przypadku bardzo prosta :

.directive( 'test', function ( $compile ) {
  return {
    restrict: 'E',
    scope: { text: '@' },
    template: '<p ng-click="add()">{{text}}</p>',
    controller: function ( $scope, $element ) {
      $scope.add = function () {
        var el = $compile( "<test text='n'></test>" )( $scope );
        $element.parent().append( el );
      };
    }
  };
});

Zauważysz, że zreformowałem również twoją dyrektywę, aby zastosować się do najlepszych praktyk. Daj mi znać, jeśli masz pytania dotyczące któregokolwiek z nich.

Josh David Miller
źródło
34
Niesamowite. To działa. Zobacz, te proste i podstawowe przykłady są tymi, które powinny być pokazane w dokumentach angulars. Zaczynają od skomplikowanych przykładów.
PCoelho,
1
Dzięki, Josh, to było naprawdę przydatne. Zrobiłem narzędzie w Plnkr, którego używamy w nowym CoderDojo, aby pomóc dzieciom nauczyć się kodować, i właśnie go rozszerzyłem, aby móc teraz korzystać z dyrektyw Angular Bootstrap, takich jak datepicker, alert, tab itp. Najwyraźniej coś zgadłem i teraz działa tylko w Chrome: embed.plnkr.co/WI16H7Rsa5adejXSmyNj/preview
JoshGough
3
Josh - jaki jest łatwiejszy sposób na osiągnięcie tego bez użycia $compile? Nawiasem mówiąc, dziękuję za odpowiedź!
doubleswirve
3
@doubleswirve W tym przypadku o wiele łatwiej byłoby po prostu użyć ngRepeat. :-) Ale zakładam, że masz na myśli dynamiczne dodawanie nowych dyrektyw do strony, w którym to przypadku odpowiedź brzmi „nie” - nie ma prostszego sposobu, ponieważ $compileusługa jest tym, co drąży dyrektywy i zaczepia je w cyklu zdarzeń. W $compiletakiej sytuacji nie można obejść tego problemu, ale w większości przypadków inna dyrektywa, taka jak ngRepeat, może wykonać to samo zadanie (więc ngRepeat wykonuje dla nas kompilację). Czy masz konkretny przypadek użycia?
Josh David Miller
2
Czy kompilacja nie powinna odbywać się na etapie prelinkowania? Myślę, że kontroler powinien zawierać tylko kod, który nie jest DOM, testowalny przez jednostkę, ale jestem nowy w koncepcji linku / kontrolera, więc nie jestem pewien. Ponadto jedną podstawową alternatywą jest ng-include + częściowy + ng-kontroler, ponieważ będzie działał jak dyrektywa o dziedziczonym zakresie.
Marcus Rådell
77

Oprócz idealnego przykładu Riceball LEE dodania nowej dyrektywy elementowej

newElement = $compile("<div my-directive='n'></div>")($scope)
$element.parent().append(newElement)

Dodanie nowej dyrektywy atrybutowej do istniejącego elementu można wykonać w następujący sposób:

Powiedzmy, że chcesz dodać on-the-fly my-directivedo spanelementu.

template: '<div>Hello <span>World</span></div>'

link: ($scope, $element, $attrs) ->

  span = $element.find('span').clone()
  span.attr('my-directive', 'my-directive')
  span = $compile(span)($scope)
  $element.find('span').replaceWith span

Mam nadzieję, że to pomaga.

martwy
źródło
3
Nie zapomnij usunąć oryginalnej dyrektywy, aby zapobiec przekroczeniu błędu maksymalnego rozmiaru stosu wywołań.
SRachamim
Cześć, czy mógłby Pan przedstawić pomysły dotyczące mojego nowego proponowanego interfejsu API, aby programowo dodawanie dyrektyw było prostszym procesem? github.com/angular/angular.js/issues/6950 Dzięki!
trusktr
Żałuję, że w 2015 roku nie będziemy mieć limitów wielkości stosu wywołań. :(
psycho brm
3
Maximum call stack size exceededBłąd zawsze się dzieje z powodu nieskończonej rekurencji. Nigdy nie widziałem przypadku, w którym zwiększenie rozmiaru stosu rozwiązałoby to.
Gunchars,
Podobny problem, przed którym
stoję
45

Dynamiczne dodawanie dyrektyw do angularjs ma dwa style:

Dodaj dyrektywę angularjs do innej dyrektywy

  • wstawianie nowego elementu (dyrektywy)
  • wstawienie nowego atrybutu (dyrektywy) do elementu

wstawianie nowego elementu (dyrektywy)

To proste. I możesz użyć w „link” lub „kompilacji”.

var newElement = $compile( "<div my-diretive='n'></div>" )( $scope );
$element.parent().append( newElement );

wstawianie nowego atrybutu do elementu

To trudne i sprawia mi ból głowy w ciągu dwóch dni.

Użycie „$ compile” spowoduje krytyczny błąd rekursywny !! Może powinien zignorować obecną dyrektywę podczas ponownej kompilacji elementu.

$element.$set("myDirective", "expression");
var newElement = $compile( $element )( $scope ); // critical recursive error.
var newElement = angular.copy(element);          // the same error too.
$element.replaceWith( newElement );

Muszę więc znaleźć sposób na wywołanie funkcji „link” dyrektywy. Bardzo trudno jest znaleźć przydatne metody ukryte głęboko w zamknięciach.

compile: (tElement, tAttrs, transclude) ->
   links = []
   myDirectiveLink = $injector.get('myDirective'+'Directive')[0] #this is the way
   links.push myDirectiveLink
   myAnotherDirectiveLink = ($scope, $element, attrs) ->
       #....
   links.push myAnotherDirectiveLink
   return (scope, elm, attrs, ctrl) ->
       for link in links
           link(scope, elm, attrs, ctrl)       

Teraz działa dobrze.

Riceball LEE
źródło
1
Chciałbym zobaczyć demonstrację dodawania nowego atrybutu do elementu, jeśli to możliwe, w waniliowym JS - czegoś mi brakuje
Patrick
prawdziwy przykład wstawienia nowego atrybutu do elementu znajduje się tutaj (patrz mój github): github.com/snowyu/angular-reactable/blob/master/src/…
Riceball LEE
1
Nie pomaga szczerze. W ten sposób jednak rozwiązałem mój problem: stackoverflow.com/a/20137542/1455709
Patrick
Tak, ten przypadek to wstawienie dyrektywy atrybutowej do innej dyrektywy, a nie wstawienie elementu do szablonu.
Riceball LEE,
Jaki jest powód robienia tego poza szablonem?
Patrick
9
function addAttr(scope, el, attrName, attrValue) {
  el.replaceWith($compile(el.clone().attr(attrName, attrValue))(scope));
}
użytkownik1212212
źródło
5

Odpowiedź zaakceptowana przez Josha Davida Millera działa świetnie, jeśli próbujesz dynamicznie dodać dyrektywę, która używa wbudowanego template. Jednak jeśli Twoja dyrektywa skorzysta z templateUrljego odpowiedzi, nie zadziała. Oto, co zadziałało dla mnie:

.directive('helperModal', [, "$compile", "$timeout", function ($compile, $timeout) {
    return {
        restrict: 'E',
        replace: true,
        scope: {}, 
        templateUrl: "app/views/modal.html",
        link: function (scope, element, attrs) {
            scope.modalTitle = attrs.modaltitle;
            scope.modalContentDirective = attrs.modalcontentdirective;
        },
        controller: function ($scope, $element, $attrs) {
            if ($attrs.modalcontentdirective != undefined && $attrs.modalcontentdirective != '') {
                var el = $compile($attrs.modalcontentdirective)($scope);
                $timeout(function () {
                    $scope.$digest();
                    $element.find('.modal-body').append(el);
                }, 0);
            }
        }
    }
}]);
ferics2
źródło
5

Josh David Miller ma rację.

PCoelho, jeśli zastanawiasz się, co $compiledzieje się za kulisami i jak generowany jest wynik HTML z dyrektywy, spójrz poniżej

$compileSerwis kompiluje fragment HTML ( "< test text='n' >< / test >"), który zawiera dyrektywy ( „test” jako element) i wywołuje funkcję. Tę funkcję można następnie wykonać z zakresem, aby uzyskać „wynik HTML z dyrektywy”.

var compileFunction = $compile("< test text='n' > < / test >");
var HtmlOutputFromDirective = compileFunction($scope);

Więcej szczegółów z pełnymi przykładami kodu tutaj: http://www.learn-angularjs-apps-projects.com/AngularJs/dynamically-add-directives-in-angularjs

Danial Lokman
źródło
4

Zainspirowany wieloma wcześniejszymi odpowiedziami wymyśliłem następującą dyrektywę „stroman”, która zastąpi się innymi dyrektywami.

app.directive('stroman', function($compile) {
  return {
    link: function(scope, el, attrName) {
      var newElem = angular.element('<div></div>');
      // Copying all of the attributes
      for (let prop in attrName.$attr) {
        newElem.attr(prop, attrName[prop]);
      }
      el.replaceWith($compile(newElem)(scope)); // Replacing
    }
  };
});

Ważne: Zarejestruj dyrektywy, których chcesz używać restrict: 'C'. Lubię to:

app.directive('my-directive', function() {
  return {
    restrict: 'C',
    template: 'Hi there',
  };
});

Możesz użyć w ten sposób:

<stroman class="my-directive other-class" randomProperty="8"></stroman>

Aby uzyskać to:

<div class="my-directive other-class" randomProperty="8">Hi there</div>

Protip Jeśli nie chcesz używać dyrektyw opartych na klasach, możesz zmienić '<div></div>'na coś, co lubisz. Np. Mają stały atrybut, który zawiera nazwę żądanej dyrektywy zamiast class.

Gábor Imre
źródło
Podobny problem, z
jakim się zmagam
O MÓJ BOŻE. znalezienie kompilacji $ zajęło 2 dni ... dzięki, przyjaciele .. działa najlepiej ... AJS, ty rock ....
Srinivasan