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.