Personalizzazione del modello all'interno di una direttiva


98

Ho un modulo che utilizza il markup di Bootstrap, come il seguente:

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

C'è un sacco di codice boilerplate lì dentro, che vorrei ridurre a una nuova direttiva - form-input, come segue:

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

genera:

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

Ho lavorato così tanto tramite un semplice modello.

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

        }
    })

Tuttavia è quando vengo ad aggiungere funzionalità più avanzate che rimango bloccato.

Come posso supportare i valori predefiniti nel modello?

Vorrei esporre il parametro "tipo" come attributo opzionale sulla mia direttiva, ad esempio:

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

Tuttavia, se non viene specificato nulla, vorrei impostare l'impostazione predefinita "text". Come posso supportarlo?

Come posso personalizzare il template in base alla presenza / assenza di attributi?

Mi piacerebbe anche poter supportare l'attributo "required", se presente. Per esempio:

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

Se requiredè presente nella direttiva, vorrei aggiungerlo al file generato<input /> nell'output e ignorarlo altrimenti. Non sono sicuro di come ottenerlo.

Sospetto che questi requisiti possano essere andati oltre un semplice modello e debbano iniziare a utilizzare le fasi di precompilazione, ma non sono in grado di iniziare.


Sono l'unico a vedere l'elefante nella stanza? -> E se typefosse impostato dinamicamente tramite associazione, ad es. type="{{ $ctrl.myForm.myField.type}}"? Ho controllato tutti i metodi di seguito e non sono riuscito a trovare alcuna soluzione che funzioni in questo scenario. Sembra che la funzione template vedrà i valori letterali degli attributi es. tAttr['type'] == '{{ $ctrl.myForm.myField.type }}'invece di tAttr['type'] == 'password'. Sono confuso.
Dimitry K

Risposte:


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

6
Questo è un po 'tardi, ma se htmlTextaggiungessi un ng-clickda qualche parte, l'unica modifica sarebbe la sostituzione element.replaceWith(htmlText)con element.replaceWith($compile(htmlText))?
jclancy

@Misko, hai detto di sbarazzarti dell'ambito. Perché? Ho una direttiva che non si compila se usata con ambito isolato.
Syam

1
questo non funziona se htmlText contiene una direttiva ng-transclude
Alp

3
Sfortunatamente ho scoperto che la convalida del modulo non sembra funzionare con questo, i $errorflag sull'input inserito non vengono mai impostati. Ho dovuto farlo all'interno della proprietà link di una direttiva: $compile(htmlText)(scope,function(_el){ element.replaceWith(_el); });in modo che il controllore del modulo riconoscesse la sua esistenza appena formata e la includesse nella convalida. Non sono riuscito a farlo funzionare nella proprietà di compilazione di una direttiva.
meconroy

5
Ok, è il 2015 là fuori e sono abbastanza sicuro che ci sia qualcosa di terribilmente sbagliato nel generare manualmente il markup negli script .
BorisOkunskiy

38

Ho provato a utilizzare la soluzione proposta da Misko, ma nella mia situazione, alcuni attributi, che dovevano essere fusi nel mio template html, erano essi stessi delle direttive.

Sfortunatamente, non tutte le direttive a cui fa riferimento il modello risultante funzionavano correttamente. Non ho avuto abbastanza tempo per immergermi nel codice angolare e scoprire la causa principale, ma ho trovato una soluzione alternativa, che potrebbe potenzialmente essere utile.

La soluzione era spostare il codice, che crea il template html, dalla compilazione a una funzione template. Esempio basato sul codice dall'alto:

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

Questo ha risolto il mio problema con un ng-click incorporato nel modello
joshcomley

Grazie, anche per me ha funzionato. Voleva racchiudere una direttiva per applicare alcuni attributi predefiniti.
martinoss

2
Grazie, non sapevo nemmeno che il modello accettasse una funzione!
Jon Snow

2
Questa non è una soluzione alternativa. È la risposta giusta all'OP. La creazione condizionale di un modello in base agli attributi dell'elemento è lo scopo esatto di una funzione di modello di direttiva / componente. Non dovresti usare la compilazione per quello. Il team Angular sta fortemente incoraggiando questo stile di codifica (non utilizzando la funzione di compilazione).
jose.angel.jimenez

Questa dovrebbe essere la risposta corretta, anche se non sapevo che il modello ha una funzione :)
NeverGiveUp161

5

Le risposte di cui sopra purtroppo non funzionano del tutto. In particolare, la fase di compilazione non ha accesso all'ambito, quindi non è possibile personalizzare il campo in base agli attributi dinamici. L'utilizzo della fase di collegamento sembra offrire la massima flessibilità (in termini di creazione asincrona di dom, ecc.) L'approccio seguente si rivolge a quanto segue:

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

Ho creato una sintesi con un codice più completo e una descrizione dell'approccio.


bel approccio. sfortunatamente quando utilizzo con ngTransclude ricevo il seguente errore:Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found.
Alp

e perché non utilizzare un ambito isolato con 'field: "="'?
IttayD

Molto carino, grazie! Sfortunatamente il tuo approccio scritto è offline :(
Michiel

Sia l'essenza che la recensione sono collegamenti morti.
binki

4

Ecco cosa ho finito per usare.

Sono molto nuovo su AngularJS, quindi mi piacerebbe vedere soluzioni migliori / alternative.

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

Utilizzo di esempio:

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

10
Una soluzione migliore è: (1) utilizzare una funzione di compilazione invece di collegare la funzione e fare la sostituzione lì. Il modello non funzionerà nel tuo caso poiché desideri personalizzarlo. (2) sbarazzarsi dell'ambito:
Misko Hevery

@MiskoHevery Grazie per il feedback - ti dispiacerebbe spiegare perché una funzione di compilazione è preferita a una funzione di collegamento qui?
Marty Pitt

4
Penso che questa sia la risposta, da docs.angularjs.org/guide/directive : "Qualsiasi operazione che può essere condivisa tra le istanze delle direttive [ad esempio, trasformare il modello DOM] dovrebbe essere spostata nella funzione di compilazione per motivi di prestazioni."
Mark Rajcok

@Marty Sei ancora in grado di associare uno dei tuoi input personalizzati a un modello? (cioè. <form-input ng-model="appName" label="Application Name" form-id="appName" required/></form-input>)
Jonathan Wilson

1
@MartyPitt Dal libro "AngularJS" di O'Reilly: "Quindi abbiamo la compilefase, che si occupa della trasformazione del modello, e la linkfase, che si occupa della modifica dei dati nella vista. In questo senso, la differenza principale tra compilee le linkfunzioni nelle direttive è che le compilefunzioni si occupano della trasformazione del modello stesso e le linkfunzioni si occupano di creare una connessione dinamica tra il modello e la vista. È in questa seconda fase che gli ambiti sono associati alle linkfunzioni compilate e la direttiva diventa attiva tramite l'associazione dei dati "
Julian
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.