Personnalisation du modèle dans une directive

98

J'ai un formulaire qui utilise le balisage de Bootstrap, comme le suivant:

<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>

Il y a beaucoup de code standard là-dedans, que j'aimerais réduire à une nouvelle directive - form-input, comme suit:

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

génère:

   <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>

J'ai beaucoup de travail via un modèle simple.

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>'

        }
    })

Cependant, c'est lorsque j'ajoute des fonctionnalités plus avancées que je suis bloqué.

Comment puis-je prendre en charge les valeurs par défaut dans le modèle?

Je voudrais exposer le paramètre "type" comme attribut facultatif sur ma directive, par exemple:

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

Cependant, si rien n'est spécifié, j'aimerais utiliser par défaut "text". Comment puis-je soutenir cela?

Comment puis-je personnaliser le modèle en fonction de la présence / absence d'attributs?

J'aimerais également pouvoir prendre en charge l'attribut "requis", s'il est présent. Par exemple:

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

Si requiredest présent dans la directive, je voudrais l'ajouter au généré <input />dans la sortie et l'ignorer autrement. Je ne sais pas comment y parvenir.

Je soupçonne que ces exigences ont peut-être dépassé un simple modèle et doivent commencer à utiliser les phases de pré-compilation, mais je ne sais pas par où commencer.

Marty Pitt
la source
Suis-je le seul à voir l'éléphant dans la pièce :) -> Et si typeest défini dynamiquement via une liaison par exemple. type="{{ $ctrl.myForm.myField.type}}"? J'ai vérifié toutes les méthodes ci-dessous et je n'ai trouvé aucune solution qui fonctionnera dans ce scénario. On dirait que la fonction de modèle verra les valeurs littérales des attributs, par exemple. tAttr['type'] == '{{ $ctrl.myForm.myField.type }}'au lieu de tAttr['type'] == 'password'. Je suis perplexe.
Dimitry K

Réponses:

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
la source
6
C'est un peu tard, mais si htmlTextvous en ajoutiez ng-clickquelque part, la seule modification serait-elle de remplacer element.replaceWith(htmlText)par element.replaceWith($compile(htmlText))?
jclancy le
@Misko, vous avez mentionné de se débarrasser de la portée. Pourquoi? J'ai une directive qui ne compile pas lorsqu'elle est utilisée avec une portée isolée.
Syam
1
cela ne fonctionne pas si htmlTextcontient une directive ng-transclude
Alp
3
Malheureusement, j'ai trouvé que la validation de formulaire ne semble pas fonctionner avec cela, les $errorindicateurs sur l'entrée insérée ne sont jamais définis. Je devais le faire dans la propriété link d'une directive: $compile(htmlText)(scope,function(_el){ element.replaceWith(_el); });pour que le contrôleur du formulaire reconnaisse sa nouvelle existence et l'inclue dans la validation. Je ne pouvais pas le faire fonctionner dans la propriété compile d'une directive.
meconroy
5
D'accord, nous sommes en 2015 et je suis presque sûr qu'il y a quelque chose qui ne va pas dans la génération manuelle du balisage dans les scripts .
BorisOkunskiy
38

J'ai essayé d'utiliser la solution proposée par Misko, mais dans ma situation, certains attributs, qui devaient être fusionnés dans mon template html, étaient eux-mêmes des directives.

Malheureusement, toutes les directives référencées par le modèle résultant ne fonctionnaient pas correctement. Je n'ai pas eu assez de temps pour plonger dans le code angulaire et découvrir la cause principale, mais j'ai trouvé une solution de contournement, qui pourrait potentiellement être utile.

La solution était de déplacer le code, qui crée le modèle html, de la compilation vers une fonction modèle. Exemple basé sur le code ci-dessus:

    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
la source
Cela a résolu mon problème avec un ng-click intégré dans le modèle
joshcomley
Merci, cela a fonctionné pour moi aussi. Voulait encapsuler une directive pour appliquer certains attributs par défaut.
martinoss
2
Merci, je ne savais même pas que le modèle acceptait une fonction!
Jon Snow
2
Ce n'est pas une solution de contournement. C'est la bonne réponse au PO. La création conditionnelle d'un modèle en fonction des attributs de l'élément est le but exact d'une fonction de modèle de directive / composant. Vous ne devez pas utiliser compile pour cela. L'équipe Angular encourage fortement ce style de codage (sans utiliser la fonction de compilation).
jose.angel.jimenez
Cela devrait être la bonne réponse, même si je ne
savais
5

Les réponses ci-dessus ne fonctionnent malheureusement pas tout à fait. En particulier, l'étape de compilation n'a pas accès à l'étendue, vous ne pouvez donc pas personnaliser le champ en fonction d'attributs dynamiques. L'utilisation de l'étape de liaison semble offrir le plus de flexibilité (en termes de création asynchrone de dom, etc.) L'approche ci-dessous aborde que:

<!-- 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));
      }
    }
  }
})

J'ai créé un résumé avec un code plus complet et une description de l'approche.

JoeS
la source
belle approche. Malheureusement, lors de l'utilisation avec ngTransclude, j'obtiens l'erreur suivante:Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found.
Alp
et pourquoi ne pas utiliser une portée isolée avec 'field: "="'?
IttayD
Très gentil merci! Malheureusement, votre approche écrite est hors ligne :(
Michiel
L'essentiel et l'écriture sont des liens morts.
binki
4

Voici ce que j'ai fini par utiliser.

Je suis très nouveau sur AngularJS, j'aimerais donc voir de meilleures solutions / alternatives.

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);
            }
        }
    })

Exemple d'utilisation:

<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
la source
10
Une meilleure solution est de: (1) utiliser une fonction de compilation au lieu de la fonction de liaison et faire le remplacement là-bas. Le modèle ne fonctionnera pas dans votre cas puisque vous souhaitez le personnaliser. (2) se débarrasser de la portée:
Misko Hevery
@MiskoHevery Merci pour les commentaires - cela vous dérangerait-il d'expliquer pourquoi une fonction de compilation est préférée à une fonction de lien ici?
Marty Pitt
4
Je pense que c'est la réponse, de docs.angularjs.org/guide/directive : "Toute opération qui peut être partagée entre l'instance de directives [par exemple, la transformation du modèle DOM] doit être déplacée vers la fonction de compilation pour des raisons de performances."
Mark Rajcok
@Marty Êtes-vous toujours en mesure de lier l'une de vos entrées personnalisées à un modèle? (ie. <form-input ng-model="appName" label="Application Name" form-id="appName" required/></form-input>)
Jonathan Wilson
1
@MartyPitt Extrait du livre "AngularJS" d'O'Reilly: "Nous avons donc la compilephase, qui traite de la transformation du modèle, et la linkphase, qui traite de la modification des données dans la vue. Dans ce sens, la principale différence entre les compileet les linkfonctions dans les directives est que les compilefonctions traitent de la transformation du modèle lui-même, et les linkfonctions traitent de l'établissement d'une connexion dynamique entre le modèle et la vue. C'est dans cette deuxième phase que les portées sont attachées aux linkfonctions compilées , et la directive devient active grâce à la liaison de données "
Julian