AngularJS - Crie uma diretiva que use o ng-model

294

Estou tentando criar uma diretiva que criaria um campo de entrada com o mesmo modelo ng que o elemento que cria a diretiva.

Aqui está o que eu vim até agora:

HTML

<!doctype html>
<html ng-app="plunker" >
<head>
  <meta charset="utf-8">
  <title>AngularJS Plunker</title>
  <link rel="stylesheet" href="style.css">
  <script>document.write("<base href=\"" + document.location + "\" />");</script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.js"></script>
  <script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
  This scope value <input ng-model="name">
  <my-directive ng-model="name"></my-directive>
</body>
</html>

Javascript

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.name = "Felipe";
});

app.directive('myDirective', function($compile) {
  return {
    restrict: 'E',
    scope: {
      ngModel: '='
    },
    template: '<div class="some"><label for="{{id}}">{{label}}</label>' +
      '<input id="{{id}}" ng-model="value"></div>',
    replace: true,
    require: 'ngModel',
    link: function($scope, elem, attr, ctrl) {
      $scope.label = attr.ngModel;
      $scope.id = attr.ngModel;
      console.debug(attr.ngModel);
      console.debug($scope.$parent.$eval(attr.ngModel));
      var textField = $('input', elem).
        attr('ng-model', attr.ngModel).
        val($scope.$parent.$eval(attr.ngModel));

      $compile(textField)($scope.$parent);
    }
  };
});

No entanto, não estou confiante de que este é o caminho certo para lidar com esse cenário e há um erro que meu controle não está sendo inicializado com o valor do campo de destino do modelo ng.

Aqui está um plunker do código acima: http://plnkr.co/edit/IvrDbJ

Qual é a maneira correta de lidar com isso?

EDIT : Depois de remover o ng-model="value"do modelo, isso parece estar funcionando bem. No entanto, manterei essa questão em aberto, porque desejo verificar novamente se esta é a maneira correta de fazer isso.

kolrie
fonte
1
E se você remover scopee configurá-lo para scope: false? Como vincular ng-modelnesse caso?
Saeed Neamati

Respostas:

210

EDIT : Esta resposta é antiga e provavelmente desatualizada. Apenas um aviso, para que não desvie as pessoas. Como não uso mais o Angular, não estou em uma boa posição para fazer melhorias.


Na verdade, é uma lógica muito boa, mas você pode simplificar um pouco as coisas.

Directiva

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.model = { name: 'World' };
  $scope.name = "Felipe";
});

app.directive('myDirective', function($compile) {
  return {
    restrict: 'AE', //attribute or element
    scope: {
      myDirectiveVar: '=',
     //bindAttr: '='
    },
    template: '<div class="some">' +
      '<input ng-model="myDirectiveVar"></div>',
    replace: true,
    //require: 'ngModel',
    link: function($scope, elem, attr, ctrl) {
      console.debug($scope);
      //var textField = $('input', elem).attr('ng-model', 'myDirectiveVar');
      // $compile(textField)($scope.$parent);
    }
  };
});

Html com diretiva

<body ng-controller="MainCtrl">
  This scope value <input ng-model="name">
  <my-directive my-directive-var="name"></my-directive>
</body>

CSS

.some {
  border: 1px solid #cacaca;
  padding: 10px;
}

Você pode vê-lo em ação com este Plunker .

Aqui está o que eu vejo:

  • Entendo por que você deseja usar o 'ng-model', mas no seu caso não é necessário. ng-model é vincular elementos html existentes a um valor no escopo. Como você mesmo está criando uma diretiva, está criando um elemento html 'novo', portanto não precisa do ng-model.

Edição Como mencionado por Mark em seu comentário, não há razão para que você não possa usar o ng-model, apenas para manter a convenção.

  • Ao criar explicitamente um escopo em sua diretiva (um escopo 'isolado'), o escopo da diretiva não pode acessar a variável 'name' no escopo pai (é por isso que acho que você deseja usar o ng-model).
  • Eu removi o ngModel da sua diretiva e substituí-o por um nome personalizado que você pode alterar para qualquer que seja.
  • O que faz com que tudo ainda funcione é que '=' assine no escopo. Checkout os docs docs sob o cabeçalho 'escopo'.

Em geral, suas diretivas devem usar o escopo isolado (que você fez corretamente) e usar o escopo do tipo '=' se desejar que um valor em sua diretiva seja sempre mapeado para um valor no escopo pai.

Roy Truelove
fonte
18
+1, mas não tenho certeza se concordo com a afirmação "ng-model é vincular elementos HTML existentes a um valor no escopo". Os dois contenteditableexemplos de diretiva na documentação angular - página de formulários , página NgModelController - usam o ng-model. E a página ngModelController diz que esse controlador "deve ser estendido por outras diretrizes".
Mark Rajcok
33
Não sei por que essa resposta é classificada tão bem porque ela não atende ao que a pergunta original fez - que é usar o ngModel. Sim, é possível evitar o uso do ngModel colocando o estado no controlador pai, mas isso ocorre às custas de ter dois controladores firmemente ligados e não poder usá-los / reutilizá-los independentemente. É como usar uma variável global em vez de configurar um ouvinte entre dois componentes - tecnicamente pode ser mais simples, mas na maioria dos casos não é uma boa solução.
Pat Niemeyer
Eu acrescentaria que, se ele quisesse confiar no controlador pai, ele deveria injetar 'require: ^ parent' de qualquer maneira - para que ele possa tornar a dependência explícita e opcional, se desejado.
Pat Niemeyer
3
@ Jeroen A forma como vejo o principal benefício é a consistência com outros lugares onde o modelo é passado como hg-model(e não a questão do acoplamento, IMO). Dessa forma, o contexto de dados sempre usa o ng-model, seja uma <input>diretiva ou uma diretiva personalizada, simplificando a sobrecarga cognitiva para o gravador de HTML. Ou seja, evita que o gravador de HTML precise descobrir qual é o nome my-directive-varpara cada diretiva, especialmente porque não há preenchimento automático para ajudá-lo.
Zai chang
2
umm ... ok ... mas agora isso não funciona mais com ng-model-optionsnenhuma das outras coisas do modelo ng, funciona?
George Mauer
68

Peguei uma combinação de todas as respostas e agora tenho duas maneiras de fazer isso com o atributo ng-model:

  • Com um novo escopo que copia o ngModel
  • Com o mesmo escopo que compila no link

Não tenho certeza se gosto da compilação no momento do link. No entanto, se você estiver apenas substituindo o elemento por outro, não precisará fazer isso.

Ao todo, prefiro o primeiro. Basta definir o escopo {ngModel:"="}e definir ng-model="ngModel"onde você deseja no seu modelo.

Atualização : inclinei o trecho de código e o atualizei para o Angular v1.2. Acontece que isolar o escopo ainda é o melhor, especialmente quando não estiver usando o jQuery. Então, tudo se resume a:

  • Você está substituindo um único elemento: Apenas substitua-o, deixe o escopo em branco, mas observe que a substituição foi descontinuada para a v2.0:

    app.directive('myReplacedDirective', function($compile) {
      return {
        restrict: 'E',
        template: '<input class="some">',
        replace: true
      };
    });
  • Caso contrário, use isto:

    app.directive('myDirectiveWithScope', function() {
      return {
        restrict: 'E',
        scope: {
          ngModel: '=',
        },
        template: '<div class="some"><input ng-model="ngModel"></div>'
      };
    });
w00t
fonte
1
Atualizei o plunker com todas as três possibilidades de escopo e para elementos filho do modelo ou o elemento raiz do modelo.
w00t
1
Isso é ótimo, mas como você essencialmente torna isso opcional? Estou criando uma diretiva de caixa de texto para uma biblioteca de interface do usuário e quero que o modelo seja opcional, o que significa que a caixa de texto ainda funcionará se o ngModel não estiver definido.
precisa saber é o seguinte
1
@NickRadford Basta verificar se ngModel está definido no escopo $ e, se não, não o usa?
W00t
1
Haverá problemas ou sobrecarga adicional com a reutilização ng-modelem um escopo isolado?
Jeff Ling
2
@jeffling não tenho certeza, mas acho que não. A cópia do ngModel é bastante leve e o escopo isolado limita a exposição.
W00t
52

não é tão complicado: no seu diretório, use um apelido: scope:{alias:'=ngModel'}

.directive('dateselect', function () {
return {
    restrict: 'E',
    transclude: true,
    scope:{
        bindModel:'=ngModel'
    },
    template:'<input ng-model="bindModel"/>'
}

no seu html, use normalmente

<dateselect ng-model="birthday"></dateselect>
AiShiguang
fonte
1
Isso é muito mais fácil ao lidar com bibliotecas como a interface do Kendo. Obrigado!
Bytebender
30

Você só precisa do ng-model quando precisa acessar o $ viewValue ou $ modelValue do modelo. Consulte NgModelController . E nesse caso, você usaria require: '^ngModel'.

Quanto ao resto, veja a resposta de Roys .

asgoth
fonte
2
ng-model também é útil, mesmo que você não precise de $ viewValue ou $ modelValue. É útil mesmo que você queira apenas os recursos de ligação de dados do modelo ng, como o exemplo de @ kolrie.
Mark Rajcok
1
E o ^deve ser não somente se a NG-modelo é aplicado em um elemento pai
georgiosd
18

Esta é uma resposta um pouco tardia, mas achei este post incrível sobre o NgModelControllerqual acho que é exatamente o que você estava procurando.

TL; DR - você pode usar require: 'ngModel'e adicionar NgModelControllerà sua função de vinculação:

link: function(scope, iElement, iAttrs, ngModelCtrl) {
  //TODO
}

Dessa forma, não são necessários hacks - você está usando o recurso incorporado do Angular ng-model

Yaniv Efraim
fonte
2

Eu não definiria o ngmodel por meio de um atributo, você pode especificá-lo diretamente no modelo:

template: '<div class="some"><label>{{label}}</label><input data-ng-model="ngModel"></div>',

plunker : http://plnkr.co/edit/9vtmnw?p=preview

Mathew Berg
fonte
0

Desde o Angular 1.5, é possível usar componentes. Os componentes são o caminho a percorrer e resolvem esse problema com facilidade.

<myComponent data-ng-model="$ctrl.result"></myComponent>

app.component("myComponent", {
    templateUrl: "yourTemplate.html",
    controller: YourController,
    bindings: {
        ngModel: "="
    }
});

Dentro do YourController, tudo o que você precisa fazer é:

this.ngModel = "x"; //$scope.$apply("$ctrl.ngModel"); if needed
Niels Steenbeek
fonte
O que eu descobri é que funciona se você realmente usar "=" em vez de "<", o que, de outra forma, é uma prática recomendada usando Componentes. Não sei ao certo o que significa a parte "dentro do YourController" desta resposta. O objetivo disso não é definir o ngModel dentro do componente?
Marc Stober
1
@ MarcStober Com o "inside YourController", eu só queria mostrar que o ngModel está disponível como getter e setter. Neste exemplo, o $ ctrl.result se tornará "x".
Niels Steenbeek
Está bem. Eu acho que a outra parte importante é que você também pode, no seu modelo de controlador, fazer input ng-model="$ctrl.ngModel"e ele será sincronizado com o $ ctrl.result também.
21717 Marc Stober
0

Criar um escopo isolado é indesejável. Eu evitaria usar o atributo scope e faria algo assim. scope: true fornece um novo escopo filho, mas não é isolado. Em seguida, use a análise para apontar uma variável de escopo local para o mesmo objeto que o usuário forneceu ao atributo ngModel.

app.directive('myDir', ['$parse', function ($parse) {
    return {
        restrict: 'EA',
        scope: true,
        link: function (scope, elem, attrs) {
            if(!attrs.ngModel) {return;}
            var model = $parse(attrs.ngModel);
            scope.model = model(scope);
        }
    };
}]);
btm1
fonte