A premissa

Em AngularJS, quando definimos um componente (ou uma diretiva), podemos criar variáveis de escopo interno a partir de atributos. A API para fazer isso é bastante convoluta:

bindings: {
attr1: '@',
attr2: '<',
attr3: '=',
attr4: '&'
}

Cansei-me de pagar o preço de envolver o meu cérebro cada vez que o usava, então neste post vamos dissecar a diferença entre os quatro símbolos de uma vez por todas.

Especificamente, vamos…

  • aprender como passar as cordas (@)
  • aprender como passar as expressões dinâmicas (<)
  • aprender como captar a saída (&)
  • aprender como configurar dois…way data bindings (=)
  • Aprenda a fazer tudo o que está acima sem usar nenhum dos quatro
  • Aprenda porque <Chutar o rabo dos outros três

Ler um atributo como texto

Comecemos com @, o mais simples dos quatro, uma vez que simplesmente lê o atributo como texto. Em outras palavras, passamos uma string ao componente.

Dizemos que temos este componente:

app.component("readingstring", {
bindings: { text: '@' },
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});

E rendemo-lo assim:

<readingstring text="hello"></readingstring>

Então aqui está o que obtemos:

Usando @ criamos uma variável interna preenchida com o conteúdo da string do atributo nomeado. Você poderia dizer que ela serve como uma configuração inicial do componente.

Avaliar um atributo como uma expressão

Mais interessante é a necessidade de avaliar um atributo como uma expressão, e tê-la reavaliada sempre que a expressão muda. Dynamic input!

Queremos ser capazes de fazer isto…

<dynamicinput in="outervariable"></dynamicinput>

…e passar a avaliação de outervariable para dynamicinput.

Prior to AngularJS 1.5, a única sintaxe que tínhamos para isto era =:

app.component("dynamicinput",{
bindings: { in: '=' },
template: '<p>dynamic input: <strong>{{$ctrl.in}}</strong></p>'
});

A desvantagem de = era que criava uma ligação de dados bidireccional, apesar de só precisarmos de um só sentido. Isto também significava que a expressão que passamos para dentro deve ser uma variável.

Mas com o AngularJS 1.5 temos <, o que significa um encadeamento de dados unidirecional. Isto nos permite usar qualquer expressão como input, como uma chamada de função:

<dynamicinput in="calculateSomething()"></dynamicinput>

A implementação do componente seria exatamente a mesma, exceto mudando = para <.

Catch output

Tempo para virar as coisas – como pegamos a saída de um componente? Veja a pequena aplicação abaixo – os botões são renderizados em uma criança, e quando eles são clicados queremos atualizar o valor externo de acordo.

Aqui é onde & entra. Ele interpreta o atributo como uma declaração e o envolve em uma função. O componente pode então chamar essa função à vontade, e preencher as variáveis na instrução. Saída para o comando!

Se o nosso html externo ficar assim…

Outer value: {{count}}
<output out="count = count + amount"></output>

…então uma implementação de output usando & poderia ficar assim:

app.component("output",{
bindings: { out: '&' },
template: `
<button ng-click="$ctrl.out({amount: 1})">buy one</button>
<button ng-click="$ctrl.out({amount: 5})">buy many</button> `
});

Note como passamos em um objeto com as variáveis a serem populadas. Esta sintaxe convoluta significa que para usar um componente com um output devemos saber duas coisas:

  • o(s) atributo(s) nome(s) para usar
  • os nomes das variáveis que serão criadas magicamente.

porque & é tão convoluto, muitos usam = para fazer output. Ao passar na variável a ser manipulada…

Outer value: {{count}}
<output out="count"></output>

…nós então simplesmente mudamos essa variável dentro do componente:

app.component("output",{
bindings: { out: '=' },
template: `<div>
<button ng-click="$ctrl.out = $ctrl.out + 1;">buy one</button>
<button ng-click="$ctrl.out = $ctrl.out + 5;">buy many</button>
</div>`
});

Isto realmente não é muito bonito:

  • estamos novamente a fazer uma ligação de dados bidireccional mesmo que só precisemos de uma maneira
  • talvez não queiramos salvar a saída, mas simplesmente actue sobre ela

Uma solução melhor do que todas as anteriores é usar < para criar a saída passando numa ligação de retorno!

Criamos o retorno de chamada no controlador externo…

$scope.callback = function(amount){
$scope.count += amount;
}

…e passamos para o componente:

<output out="callback"></output>

O componente agora simplesmente o chama de acordo:

app.component("output",{
bindings: { out: '<' },
template: `
<button ng-click="$ctrl.out(1)">buy one</button>
<button ng-click="$ctrl.out(5)">buy many</button>`
});

Muito semelhante a &, mas sem a magia enrolada!

Como um interessante aparte, este padrão é exatamente como a saída de um componente funciona em React.

Ligação de dados em dois sentidos

É aqui que = é normalmente permitido brilhar como um garoto-propaganda AngularJS. Pegue este app:

Se o tornarmos assim…

Outer: <input ng-model="value">
<twoway connection="value"></twoway>

…então podemos implementar twoway usando = assim:

app.component("twowayeq",{
bindings: { connection: '=' },
template: `inner: <input ng-model="$ctrl.connection">`
});

Admittedly easy, but note – é bastante raro precisar de encadernação de dados bidireccional. Muitas vezes o que você realmente quer é uma entrada e uma saída.

O que nos leva a como podemos implementar a ligação bidireccional usando apenas <! Se criarmos novamente uma função de callback no controlador externo…

$scope.callback = function(newval){
$scope.value = newval;
}

…e passarmos tanto o valor como o callback…

<twoway value="value" callback="callback"></twoway>

…então o componente pode ser criado assim:

app.component("twowayin",{
bindings: {
value: '<',
callback: '<'
},
template: `
<input ng-model="$ctrl.value" ng-change="$ctrl.callback($ctrl.value)">
`
});

Alcançamos uma ligação de dados bidirecional, mas ainda estamos aderindo a um fluxo de dados unidirecional. Melhor karma!

Largando os símbolos atrás de tudo

Facto é, os quatro símbolos são apenas atalhos. Podemos fazer tudo o que eles fazem sem eles.

Aplicação de passagem de strings…

…que nós renderizamos assim…

<readingstring text="hello"></readingstring>

…poderia ser implementada acessando a $element service:

app.component("readingstring", {
controller: function($element){
this.text = $element.attr("text");
},
template: '<p>text: <strong>{{$ctrl.text}}</strong></p>'
});

Or com uma diretiva, usando a attrs que são passadas para link:

app.directive("readingstring", function(){
return {
restrict: 'E',
scope: {},
link: function(scope,elem,attrs){
scope.text = attrs.text;
},
template: '<p>text: <strong>{{text}}</strong></p>'
};
});

Aplicação de entrada dinâmica…

…renderizado assim…

<dynamicinput in="outervariable"></dynamicinput>

…poderia ser implementado usando uma chamada .$watch no escopo pai:

app.component("dynamicinput",{
controller: ($scope,$element) => {
let expression = $element.attr("in");
$scope.$parent.$watch(expression, newVal => $scope.in = newVal);
},
template: '<p>dynamic input: <strong>{{in}}</strong></p>'
});

O aplicativo de saída…

…renderizado assim…

<output out="count = count + amount"></output>

…poderia ser implementado chamando $scope.$apply no escopo pai:

app.component("output",{
controller: ($scope,$element,$timeout) => {
let statement = $element.attr("out");
$scope.increaseBy = by => {
$timeout(function(){
$scope.$parent.$apply(`amount = ${by}; ${statement}`);
});
}
},
template: `
<button ng-click="increaseBy(1)">buy one</button>
<button ng-click="increaseBy(5)">buy many</button>`
});

Isso não é exatamente a mesma coisa que & já que agora também poluímos o escopo pai com uma variável amount, mas ainda assim, ele mostra o conceito bem o suficiente.

Finalmente a aplicação de duas vias…

…renderizada como com =

<twoway connection="value"></twoway>

…poderia ser implementada definindo um $watch tanto no âmbito dos pais como dos filhos:

app.component("twoway",{
controller: ($scope,$element,$timeout) => {
let variable = $element.attr("connection");
$scope.$parent.$watch(variable, newVal => $scope.inner = newVal;
$scope.$watch('inner', (newVal='') => $timeout( () => {
$scope.$parent.$apply(`${variable} = "${newVal}";`);
}));
},
template: `inner: <input ng-model="inner">`
});

Isto é uma ligeira batota, uma vez que estamos agora a assumir o valor limite para ser sempre uma string, mas, a essência ainda está lá!

Embrulhando

Esperamos que esta viagem tenha sido educativa, e que @, <, = e & agora se sintam menos intimidadores.

E que tenham reparado como < dá cabo do resto! Pode fazer tudo, o que também = pode, mas < parece muito melhor fazê-lo.

Alguns são desajeitados para ler cordas (< requer uma string numa string, e = precisa de uma variável proxy), mas isso é fácil o suficiente para fazer baunilha por isso @ não deve ficar muito convencido.

Também, & pode ir rodar num pau.

Deixe uma resposta

O seu endereço de email não será publicado.