Personalização do modelo dentro de uma diretiva

98

Tenho um formulário que usa marcação do Bootstrap, como o seguinte:

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

Há muito código clichê lá, que eu gostaria de reduzir a uma nova diretiva - form-input, como a seguir:

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

gera:

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

Tenho tudo isso trabalhando por meio de um modelo simples.

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

        }
    })

No entanto, é quando eu venho para adicionar funcionalidades mais avançadas que estou ficando preso.

Como posso oferecer suporte a valores padrão no modelo?

Gostaria de expor o parâmetro "type" como um atributo opcional na minha diretiva, por exemplo:

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

No entanto, se nada for especificado, gostaria de usar como padrão "text". Como posso apoiar isso?

Como posso personalizar o modelo com base na presença / ausência de atributos?

Eu também gostaria de ser capaz de oferecer suporte ao atributo "required", se estiver presente. Por exemplo:

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

Se requiredestiver presente na diretiva, gostaria de adicioná-lo ao gerado <input />na saída e ignorá-lo caso contrário. Não tenho certeza de como fazer isso.

Suspeito que esses requisitos podem ter ido além de um simples modelo e precisam começar a usar as fases de pré-compilação, mas não sei por onde começar.

Marty Pitt
fonte
Sou o único a ver o elefante na sala :) -> E se typefor definido dinamicamente por meio de ligação, por exemplo. type="{{ $ctrl.myForm.myField.type}}"? Verifiquei todos os métodos abaixo e não encontrei nenhuma solução que funcione neste cenário. Parece que a função de modelo verá valores literais dos atributos, por exemplo. tAttr['type'] == '{{ $ctrl.myForm.myField.type }}'em vez de tAttr['type'] == 'password'. Estou confuso.
Dimitry K

Respostas:

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
fonte
6
Isso é um pouco tarde, mas se htmlTextvocê adicionar um ng-clickalgum lugar, a única modificação seria substituir element.replaceWith(htmlText)por element.replaceWith($compile(htmlText))?
jclancy
@Misko, você mencionou para se livrar do escopo. Por quê? Eu tenho uma diretiva que não compila quando usada com escopo isolado.
Syam
1
isso não funciona se htmlTextcontiver uma diretiva ng-transclude
Alp
3
Infelizmente, descobri que a validação do formulário não parece funcionar com isso, os $errorsinalizadores na entrada inserida nunca são definidos. Tive que fazer isso dentro da propriedade de link de uma diretiva: $compile(htmlText)(scope,function(_el){ element.replaceWith(_el); });para que o controlador do formulário reconhecesse sua existência recém-formada e incluísse na validação. Não consegui fazê-lo funcionar na propriedade de compilação de uma diretiva.
meconroy
5
Ok, estamos em 2015 e tenho certeza que há algo muito errado em gerar marcação em scripts manualmente .
BorisOkunskiy
38

Tentei usar a solução proposta por Misko, mas na minha situação, alguns atributos, que precisavam ser integrados ao meu template html, eram eles próprios diretivas.

Infelizmente, nem todas as diretivas referenciadas pelo modelo resultante funcionaram corretamente. Não tive tempo suficiente para mergulhar no código angular e descobrir a causa raiz, mas encontrei uma solução alternativa que poderia ser útil.

A solução foi mover o código, que cria o template html, de compilar para uma função de template. Exemplo baseado no código acima:

    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
fonte
Isso resolveu meu problema com um ng-click incorporado no modelo
joshcomley
Obrigado, isso funcionou para mim também. Queria quebrar uma diretiva para aplicar alguns atributos padrão.
Martinoss,
2
Obrigado, nem sabia que o template aceitava uma função!
Jon Snow
2
Esta não é uma solução alternativa. É a resposta certa para o OP. Elaborar condicionalmente um template dependendo dos atributos do elemento é o propósito exato de uma função de template de diretiva / componente. Você não deve usar compilar para isso. A equipe do Angular está encorajando fortemente esse estilo de codificação (não usando a função de compilação).
jose.angel.jimenez
Esta deve ser a resposta correta, mesmo eu não sabendo que o modelo leva uma função :)
NeverGiveUp161
5

As respostas acima, infelizmente, não funcionam bem. Em particular, o estágio de compilação não tem acesso ao escopo, portanto, você não pode personalizar o campo com base em atributos dinâmicos. Usar o estágio de vinculação parece oferecer mais flexibilidade (em termos de criação de dom de forma assíncrona, etc.) A abordagem a seguir aborda o seguinte:

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

Eu criei uma essência com um código mais completo e uma descrição da abordagem.

JoeS
fonte
boa abordagem. infelizmente, quando uso com ngTransclude, recebo o seguinte erro:Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found.
Alp
e por que não usar um escopo isolado com 'field: "="'?
IttayD
Muito bom, obrigado! Infelizmente, sua abordagem escrita está offline :(
Michiel
Tanto a essência quanto a redação são links mortos.
binki
4

Aqui está o que acabei usando.

Eu sou muito novo no AngularJS, então adoraria ver soluções melhores / 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);
            }
        }
    })

Exemplo de uso:

<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
fonte
10
Uma solução melhor é: (1) usar uma função de compilação em vez de uma função de ligação e fazer a substituição lá. O modelo não funcionará no seu caso, pois você deseja personalizá-lo. (2) livrar-se do escopo:
Misko Hevery
@MiskoHevery Obrigado pelo feedback - você se importaria de explicar por que uma função de compilação é preferida para uma função de link aqui?
Marty Pitt
4
Acho que esta é a resposta, de docs.angularjs.org/guide/directive : "Qualquer operação que pode ser compartilhada entre a instância de diretivas [por exemplo, transformar o modelo DOM] deve ser movida para a função de compilação por motivos de desempenho."
Mark Rajcok,
@Marty Você ainda consegue vincular uma de suas entradas personalizadas a um modelo? (ie. <form-input ng-model="appName" label="Application Name" form-id="appName" required/></form-input>)
Jonathan Wilson
1
@MartyPitt Do livro "AngularJS" de O'Reilly: "Temos a compilefase, que trata da transformação do modelo, e a linkfase, que trata da modificação dos dados na visualização. Ao longo dessas linhas, a principal diferença entre compilee linkfunções em diretivas é que as compilefunções lidam com a transformação do próprio modelo e as linkfunções lidam com a conexão dinâmica entre o modelo e a visualização. É nessa segunda fase que os escopos são anexados às linkfunções compiladas e a diretiva torna-se ativa por meio da vinculação de dados "
Juliano de