Dostosowywanie szablonu w ramach dyrektywy

98

Mam formularz, który używa znaczników z Bootstrap, na przykład:

<form class="form-horizontal">
  <fieldset>
    <legend>Legend text</legend>
    <div class="control-group">
      <label class="control-label" for="nameInput">Name</label>
      <div class="controls">
        <input type="text" class="input-xlarge" id="nameInput">
        <p class="help-block">Supporting help text</p>
      </div>
    </div>
  </fieldset>
</form>

Jest tam dużo gotowego kodu, który chciałbym zredukować do nowej dyrektywy - form-input, na przykład:

<form-input label="Name" form-id="nameInput"></form-input>

generuje:

   <div class="control-group">
      <label class="control-label" for="nameInput">Name</label>
      <div class="controls">
        <input type="text" class="input-xlarge" id="nameInput">
      </div>
    </div>

Tyle pracuję dzięki prostemu szablonowi.

angular.module('formComponents', [])
    .directive('formInput', function() {
        return {
            restrict: 'E',
            scope: {
                label: 'bind',
                formId: 'bind'
            },
            template:   '<div class="control-group">' +
                            '<label class="control-label" for="{{formId}}">{{label}}</label>' +
                            '<div class="controls">' +
                                '<input type="text" class="input-xlarge" id="{{formId}}" name="{{formId}}">' +
                            '</div>' +
                        '</div>'

        }
    })

Jednak kiedy dodam bardziej zaawansowaną funkcjonalność, utknęłam.

Jak mogę obsługiwać wartości domyślne w szablonie?

Chciałbym ujawnić parametr „type” jako opcjonalny atrybut w mojej dyrektywie, np .:

<form-input label="Password" form-id="password" type="password"/></form-input>
<form-input label="Email address" form-id="emailAddress" type="email" /></form-input>

Jeśli jednak nic nie zostanie określone, chcę domyślnie "text". Jak mogę to wesprzeć?

Jak mogę dostosować szablon na podstawie obecności / braku atrybutów?

Chciałbym również móc obsługiwać atrybut „wymagany”, jeśli jest obecny. Na przykład:

<form-input label="Email address" form-id="emailAddress" type="email" required/></form-input>

Jeśli requiredjest obecny w dyrektywie, chciałbym dodać go do wygenerowanego <input />wyniku i zignorować w przeciwnym razie. Nie jestem pewien, jak to osiągnąć.

Podejrzewam, że te wymagania mogły wyjść poza prosty szablon i muszę zacząć korzystać z faz prekompilacji, ale nie wiem, od czego zacząć.

Marty Pitt
źródło
Czy tylko ja widzę słonia w pokoju :) -> Co jeśli typejest ustawiane dynamicznie poprzez wiązanie np. type="{{ $ctrl.myForm.myField.type}}"? Sprawdziłem wszystkie poniższe metody i nie mogłem znaleźć żadnego rozwiązania, które zadziała w tym scenariuszu. Wygląda na to, że funkcja szablonu zobaczy dosłowne wartości atrybutów, np. tAttr['type'] == '{{ $ctrl.myForm.myField.type }}'zamiast tAttr['type'] == 'password'. Jestem zdziwiony.
Dimitry K

Odpowiedzi:

211
angular.module('formComponents', [])
  .directive('formInput', function() {
    return {
        restrict: 'E',
        compile: function(element, attrs) {
            var type = attrs.type || 'text';
            var required = attrs.hasOwnProperty('required') ? "required='required'" : "";
            var htmlText = '<div class="control-group">' +
                '<label class="control-label" for="' + attrs.formId + '">' + attrs.label + '</label>' +
                    '<div class="controls">' +
                    '<input type="' + type + '" class="input-xlarge" id="' + attrs.formId + '" name="' + attrs.formId + '" ' + required + '>' +
                    '</div>' +
                '</div>';
            element.replaceWith(htmlText);
        }
    };
})
Misko Hevery
źródło
6
To jest trochę późno, ale jeśli w htmlTextty dodaje się ng-clickgdzieś, to jedyna modyfikacja być zastąpienie element.replaceWith(htmlText)ze element.replaceWith($compile(htmlText))?
jclancy,
@Misko, wspomniałeś o pozbyciu się zakresu. Czemu? Mam dyrektywę, która nie kompiluje się, gdy jest używana z izolowanym zakresem.
Syam
1
to nie działa, jeśli htmlTextzawiera dyrektywę ng-transclude
Alp,
3
Niestety stwierdziłem, że walidacja formularza nie wydaje się działać z tym, $errorflagi na wstawionym wejściu nigdy nie są ustawiane. Musiałem to zrobić w ramach właściwości łącza dyrektywy: $compile(htmlText)(scope,function(_el){ element.replaceWith(_el); });aby kontroler formularza mógł rozpoznać jego nowo utworzone istnienie i uwzględnić go w walidacji. Nie mogłem zmusić go do działania we właściwości kompilacji dyrektywy.
meconroy,
5
Okej, jest rok 2015 i jestem prawie pewien, że jest coś strasznie nie tak w ręcznym generowaniu znaczników w skryptach .
BorisOkunskiy
38

Próbowałem skorzystać z rozwiązania zaproponowanego przez Misko, ale w mojej sytuacji niektóre atrybuty, które musiały zostać włączone do mojego szablonu html, same były dyrektywami.

Niestety, nie wszystkie dyrektywy, do których odwołuje się wynikowy szablon, działały poprawnie. Nie miałem wystarczająco dużo czasu, aby zagłębić się w kod kątowy i znaleźć główną przyczynę, ale znalazłem obejście, które może być potencjalnie pomocne.

Rozwiązaniem było przeniesienie kodu, który tworzy szablon html, z kompilacji do funkcji szablonu. Przykład na podstawie kodu z góry:

    angular.module('formComponents', [])
  .directive('formInput', function() {
    return {
        restrict: 'E',
        template: function(element, attrs) {
           var type = attrs.type || 'text';
            var required = attrs.hasOwnProperty('required') ? "required='required'" : "";
            var htmlText = '<div class="control-group">' +
                '<label class="control-label" for="' + attrs.formId + '">' + attrs.label + '</label>' +
                    '<div class="controls">' +
                    '<input type="' + type + '" class="input-xlarge" id="' + attrs.formId + '" name="' + attrs.formId + '" ' + required + '>' +
                    '</div>' +
                '</div>';
             return htmlText;
        }
        compile: function(element, attrs)
        {
           //do whatever else is necessary
        }
    }
})
Janusz Gryszko
źródło
To rozwiązało mój problem z osadzonym ng-click w szablonie
joshcomley,
Dzięki, dla mnie to też zadziałało. Chciałem opakować dyrektywę, aby zastosować pewne domyślne atrybuty.
martinoss
2
Dzięki, nawet nie wiedziałem, że szablon przyjął funkcję!
Jon Snow
2
To nie jest obejście. To właściwa odpowiedź na PO. Warunkowe tworzenie szablonu w zależności od atrybutów elementu jest dokładnym celem funkcji dyrektywy / szablonu komponentu. Nie powinieneś używać do tego kompilacji. Zespół Angulara mocno zachęca do tego stylu kodowania (nie używa funkcji kompilacji).
jose.angel.jimenez
To powinna być poprawna odpowiedź, nawet ja nie wiedziałem, że szablon przyjmuje funkcję :)
NeverGiveUp161
5

Powyższe odpowiedzi niestety nie do końca działają. W szczególności etap kompilacji nie ma dostępu do zakresu, więc nie można dostosowywać pola na podstawie atrybutów dynamicznych. Wydaje się, że użycie etapu linkowania zapewnia największą elastyczność (pod względem asynchronicznego tworzenia domeny itp.) Poniższe podejście rozwiązuje następujące kwestie:

<!-- Usage: -->
<form>
  <form-field ng-model="formModel[field.attr]" field="field" ng-repeat="field in fields">
</form>
// directive
angular.module('app')
.directive('formField', function($compile, $parse) {
  return { 
    restrict: 'E', 
    compile: function(element, attrs) {
      var fieldGetter = $parse(attrs.field);

      return function (scope, element, attrs) {
        var template, field, id;
        field = fieldGetter(scope);
        template = '..your dom structure here...'
        element.replaceWith($compile(template)(scope));
      }
    }
  }
})

Utworzyłem GIST z bardziej kompletny kod i writeup podejścia.

JoeS
źródło
miłe podejście. niestety podczas korzystania z ngTransclude pojawia się następujący błąd:Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found.
Alp
i dlaczego nie użyć izolowanego zakresu z „field:" = "”?
IttayD
Bardzo fajnie, dziękuję! Niestety twoje pisemne podejście jest offline :(
Michiel
Zarówno treść, jak i zapis są martwymi linkami.
binki
4

Oto, czego ostatecznie użyłem.

Jestem bardzo nowy w AngularJS, więc chciałbym zobaczyć lepsze / alternatywne rozwiązania.

angular.module('formComponents', [])
    .directive('formInput', function() {
        return {
            restrict: 'E',
            scope: {},
            link: function(scope, element, attrs)
            {
                var type = attrs.type || 'text';
                var required = attrs.hasOwnProperty('required') ? "required='required'" : "";
                var htmlText = '<div class="control-group">' +
                    '<label class="control-label" for="' + attrs.formId + '">' + attrs.label + '</label>' +
                        '<div class="controls">' +
                        '<input type="' + type + '" class="input-xlarge" id="' + attrs.formId + '" name="' + attrs.formId + '" ' + required + '>' +
                        '</div>' +
                    '</div>';
                element.html(htmlText);
            }
        }
    })

Przykładowe użycie:

<form-input label="Application Name" form-id="appName" required/></form-input>
<form-input type="email" label="Email address" form-id="emailAddress" required/></form-input>
<form-input type="password" label="Password" form-id="password" /></form-input>
Marty Pitt
źródło
10
Lepszym rozwiązaniem jest: (1) użycie funkcji kompilującej zamiast łączenia funkcji i wykonanie tam zamiany. Szablon nie będzie działał w Twoim przypadku, ponieważ chcesz go dostosować. (2) pozbyć się zakresu:
Misko Hevery
@MiskoHevery Dzięki za informację zwrotną - czy mógłbyś wyjaśnić, dlaczego funkcja kompilująca jest tutaj preferowana do funkcji linku?
Marty Pitt
4
Myślę, że to jest odpowiedź z docs.angularjs.org/guide/directive : "Każda operacja, która może być współużytkowana przez instancje dyrektyw [np. Transformacja szablonu DOM], powinna zostać przeniesiona do funkcji kompilacji ze względu na wydajność."
Mark Rajcok,
@Marty Czy nadal możesz powiązać jedno ze swoich niestandardowych danych wejściowych z modelem? (tj. <form-input ng-model="appName" label="Application Name" form-id="appName" required/></form-input>)
Jonathan Wilson
1
@MartyPitt Z książki „AngularJS” O'Reilly'ego: „Mamy więc compilefazę, która zajmuje się transformacją szablonu, oraz linkfazę, która zajmuje się modyfikowaniem danych w widoku. Zgodnie z tymi liniami podstawowa różnica między compilei linkfunkcje w dyrektywach polega na tym, że compilefunkcje zajmują się przekształcaniem samego szablonu, a linkfunkcje zajmują się tworzeniem dynamicznego połączenia między modelem a widokiem. W tej drugiej fazie zakresy są dołączane do skompilowanych linkfunkcji, a dyrektywa staje się aktywna dzięki powiązaniu danych "
Julian