Personalización de la plantilla dentro de una directiva

98

Tengo un formulario que usa marcado de Bootstrap, como el siguiente:

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

Hay una gran cantidad de código repetitivo allí, que me gustaría reducir a una nueva directiva: formulario de entrada, como sigue:

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

Tengo tanto trabajo a través de una plantilla 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>'

        }
    })

Sin embargo, es cuando llego a agregar una funcionalidad más avanzada que me atasco.

¿Cómo puedo admitir valores predeterminados en la plantilla?

Me gustaría exponer el parámetro "tipo" como un atributo opcional en mi directiva, por ejemplo:

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

Sin embargo, si no se especifica nada, me gustaría usar de forma predeterminada "text". ¿Cómo puedo apoyar esto?

¿Cómo puedo personalizar la plantilla en función de la presencia / ausencia de atributos?

También me gustaría poder admitir el atributo "obligatorio", si está presente. P.ej:

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

Si requiredestá presente en la directiva, me gustaría agregarlo al generado <input />en la salida e ignorarlo de lo contrario. No estoy seguro de cómo lograrlo.

Sospecho que estos requisitos pueden haberse movido más allá de una simple plantilla y tienen que comenzar a usar las fases de precompilación, pero no tengo por dónde empezar.

Marty Pitt
fuente
¿Soy el único que ve al elefante en la habitación :) -> ¿Qué pasa si typese establece dinámicamente a través de un enlace, por ejemplo? type="{{ $ctrl.myForm.myField.type}}"? Revisé todos los métodos a continuación y no pude encontrar ninguna solución que funcione en este escenario. Parece que la función de plantilla verá valores literales de los atributos, por ejemplo. tAttr['type'] == '{{ $ctrl.myForm.myField.type }}'en lugar de tAttr['type'] == 'password'. Estoy confundido.
Dimitry K

Respuestas:

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
fuente
6
Esto es un poco tarde, pero si htmlTextagregaste un ng-clicklugar, ¿la única modificación sería reemplazar element.replaceWith(htmlText)con element.replaceWith($compile(htmlText))?
jclancy
@Misko, mencionaste deshacerte del alcance. ¿Por qué? Tengo una directiva que no se compila cuando se usa con un alcance aislado.
Syam
1
esto no funciona si htmlTextcontiene una directiva ng-transclude
Alp
3
Desafortunadamente, descubrí que la validación del formulario no parece funcionar con esto, las $errorbanderas en la entrada insertada nunca se configuran. Tuve que hacer esto dentro de la propiedad de enlace de una directiva: $compile(htmlText)(scope,function(_el){ element.replaceWith(_el); });para que el controlador del formulario reconozca su existencia recién formada y la incluya en la validación. No pude hacer que funcione en la propiedad de compilación de una directiva.
meconroy
5
Bien, estamos en 2015 y estoy bastante seguro de que hay algo terriblemente mal al generar marcas en los scripts manualmente .
BorisOkunskiy
38

Intenté utilizar la solución propuesta por Misko, pero en mi situación, algunos atributos, que debían fusionarse en mi plantilla html, eran en sí mismos directivas.

Desafortunadamente, no todas las directivas a las que hace referencia la plantilla resultante funcionaron correctamente. No tuve suficiente tiempo para sumergirme en el código angular y descubrir la causa raíz, pero encontré una solución que podría ser útil.

La solución fue mover el código, que crea la plantilla html, de compilar a una función de plantilla. Ejemplo basado en el código de arriba:

    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
fuente
Esto resolvió mi problema con un ng-clic incrustado en la plantilla
joshcomley
Gracias, esto también funcionó para mí. Quería envolver una directiva para aplicar algunos atributos predeterminados.
Martinoss
2
Gracias, ¡ni siquiera sabía que la plantilla aceptaba una función!
Jon Snow
2
Esta no es una solución alternativa. Es la respuesta correcta al OP. La elaboración condicional de una plantilla en función de los atributos del elemento es el propósito exacto de una función de plantilla directiva / componente. No deberías usar compilar para eso. El equipo de Angular está fomentando mucho este estilo de codificación (sin usar la función de compilación).
jose.angel.jimenez
Esta debería ser la respuesta correcta, incluso yo no sabía que la plantilla toma una función :)
NeverGiveUp161
5

Desafortunadamente, las respuestas anteriores no funcionan del todo. En particular, la etapa de compilación no tiene acceso al alcance, por lo que no puede personalizar el campo en función de atributos dinámicos. El uso de la etapa de vinculación parece ofrecer la mayor flexibilidad (en términos de creación asincrónica de dom, etc.) El siguiente enfoque aborda eso:

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

He creado una esencia con un código más completo y una redacción del enfoque.

JoeS
fuente
buen enfoque. Desafortunadamente, cuando lo uso con ngTransclude, aparece el siguiente error:Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found.
Alp
y ¿por qué no utilizar un ámbito aislado con 'campo: "="'?
IttayD
¡Muy bien, gracias! Desafortunadamente, su enfoque escrito está fuera de línea :(
Michiel
Tanto la esencia como la redacción son enlaces muertos.
binki
4

Esto es lo que terminé usando.

Soy muy nuevo en AngularJS, así que me encantaría ver soluciones mejores / alternativas.

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

Uso de ejemplo:

<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
fuente
10
Una mejor solución es: (1) usar una función de compilación en lugar de una función de enlace y hacer el reemplazo allí. La plantilla no funcionará en su caso ya que desea personalizarla. (2) deshacerse del alcance:
Misko Hevery
@MiskoHevery Gracias por los comentarios, ¿le importaría explicar por qué se prefiere una función de compilación a una función de enlace aquí?
Marty Pitt
4
Creo que esta es la respuesta, de docs.angularjs.org/guide/directive : "Cualquier operación que pueda compartirse entre la instancia de directivas [por ejemplo, transformar la plantilla DOM] debe moverse a la función de compilación por razones de rendimiento".
Mark Rajcok
@Marty ¿Todavía puede vincular una de sus entradas personalizadas a un modelo? (ej. <form-input ng-model="appName" label="Application Name" form-id="appName" required/></form-input>)
Jonathan Wilson
1
@MartyPitt Del libro "AngularJS" de O'Reilly: "Tenemos la compilefase, que se ocupa de transformar la plantilla, y la linkfase, que se ocupa de modificar los datos en la vista. En este sentido, la principal diferencia entre compiley las linkfunciones en las directivas es que las compilefunciones se ocupan de transformar la plantilla en sí, y las linkfunciones se ocupan de establecer una conexión dinámica entre el modelo y la vista. Es en esta segunda fase cuando los ámbitos se adjuntan a las linkfunciones compiladas y la directiva se activa mediante el enlace de datos "
Juliano