Componentes
Estrutura
Componentes são um mecanismo para encapsular partes de uma view, tornando o código mais fácil de organizar e/ou reutilizar.
Qualquer objeto JavaScript que possua um método view
é um componente Mithril.js. Componentes podem ser utilizados através da função utilitária m()
:
// Defina seu componente
var Example = {
view: function (vnode) {
return m('div', 'Hello');
},
};
// Utilize seu componente
m(Example);
// HTML equivalente
// <div>Hello</div>
Métodos de ciclo de vida
Componentes podem ter os mesmos métodos de ciclo de vida que os nós do DOM virtual. Note que vnode
é passado como argumento para cada método de ciclo de vida, assim como para view
(com o vnode anterior passado adicionalmente para onbeforeupdate
):
var ComponentWithHooks = {
oninit: function (vnode) {
console.log('initialized');
},
oncreate: function (vnode) {
console.log('DOM created');
},
onbeforeupdate: function (newVnode, oldVnode) {
return true;
},
onupdate: function (vnode) {
console.log('DOM updated');
},
onbeforeremove: function (vnode) {
console.log('exit animation can start');
return new Promise(function (resolve) {
// Chame após a conclusão da animação
resolve();
});
},
onremove: function (vnode) {
console.log('removing DOM element');
},
view: function (vnode) {
return 'hello';
},
};
Assim como outros tipos de nós do DOM virtual, componentes podem ter métodos de ciclo de vida adicionais definidos quando utilizados como tipos de vnode.
function initialize(vnode) {
console.log('initialized as vnode');
}
m(ComponentWithHooks, { oninit: initialize });
Métodos de ciclo de vida em vnodes não substituem os métodos de componentes, e vice-versa. Métodos de ciclo de vida de componente são sempre executados após o método correspondente do vnode.
Tenha cuidado para não usar nomes de métodos de ciclo de vida para seus próprios nomes de função de callback em vnodes.
Para aprender mais sobre métodos de ciclo de vida, veja a página de métodos de ciclo de vida.
Passando dados para componentes
Dados podem ser passados para instâncias de componentes ao passar um objeto attrs
como o segundo parâmetro na função hyperscript:
m(Example, { name: 'Floyd' });
Esses dados podem ser acessados na view do componente ou nos métodos de ciclo de vida através de vnode.attrs
:
var Example = {
view: function (vnode) {
return m('div', 'Hello, ' + vnode.attrs.name);
},
};
NOTA: Métodos de ciclo de vida também podem ser definidos no objeto attrs
, então você deve evitar usar seus nomes para seus próprios callbacks, pois eles também seriam invocados pelo próprio Mithril.js. Utilize-os em attrs
apenas quando você deseja especificamente usá-los como métodos de ciclo de vida.
Estado
Como todos os nós do DOM virtual, vnodes de componente podem ter state. O state do componente é útil para suportar arquiteturas orientadas a objetos, encapsulamento e separação de responsabilidades.
Note que, ao contrário de muitos outros frameworks, mutar o state do componente não dispara redesenhos ou atualizações do DOM. Em vez disso, os redesenhos são realizados quando os manipuladores de eventos são disparados, quando as requisições HTTP feitas por m.request são concluídas ou quando o navegador navega para rotas diferentes. Os mecanismos de state de componente do Mithril.js existem simplesmente como uma conveniência para as aplicações.
Se uma mudança de state ocorrer que não seja resultado de nenhuma das condições acima (por exemplo, após um setTimeout
), então você pode usar m.redraw()
para disparar um redesenho manualmente.
Estado de componente de closure
Nos exemplos acima, cada componente é definido como um POJO (Plain Old JavaScript Object), que é usado internamente pelo Mithril.js como o protótipo para as instâncias desse componente. É possível usar o state do componente com um POJO (como discutiremos a seguir), mas não é a abordagem mais limpa ou simples. Para isso, usaremos um componente de closure, que é simplesmente uma função wrapper que retorna uma instância de componente POJO, que por sua vez carrega seu próprio escopo fechado.
Com um componente de closure, o state pode simplesmente ser mantido por variáveis que são declaradas dentro da função externa:
function ComponentWithState(initialVnode) {
// Variável de state do componente, única para cada instância
var count = 0;
// Instância de componente POJO: qualquer objeto com uma
// função view que retorna um vnode
return {
oninit: function (vnode) {
console.log('init a closure component');
},
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: function () {
count += 1;
},
},
'Increment count'
)
);
},
};
}
Qualquer função declarada dentro da closure também tem acesso às suas variáveis de state.
function ComponentWithState(initialVnode) {
var count = 0;
function increment() {
count += 1;
}
function decrement() {
count -= 1;
}
return {
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: increment,
},
'Increment'
),
m(
'button',
{
onclick: decrement,
},
'Decrement'
)
);
},
};
}
Componentes de closure são utilizados da mesma forma que POJOs, por exemplo, m(ComponentWithState, { passedData: ... })
.
Uma grande vantagem dos componentes de closure é que não precisamos nos preocupar em fazer o binding de this
ao anexar callbacks de manipulador de eventos. De fato, this
nunca é usado e nunca temos que pensar em ambiguidades de contexto de this
.
Estado de componente POJO
Geralmente, é recomendado que você use closures para gerenciar o state do componente. Se, no entanto, você tiver um motivo para gerenciar o state em um POJO, o state de um componente pode ser acessado de três maneiras: como um blueprint na inicialização, através de vnode.state
e através da palavra-chave this
nos métodos do componente.
Na inicialização
Para componentes POJO, o objeto componente é o protótipo de cada instância de componente, então qualquer propriedade definida no objeto componente estará acessível como uma propriedade de vnode.state
. Isso permite uma inicialização de state "blueprint" simples.
No exemplo abaixo, data
se torna uma propriedade do objeto vnode.state
do componente ComponentWithInitialState
.
var ComponentWithInitialState = {
data: 'Initial content',
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithInitialState);
// HTML Equivalente
// <div>Initial content</div>
Através de vnode.state
Como você pode ver, o state também pode ser acessado através da propriedade vnode.state
, que está disponível para todos os métodos de ciclo de vida, bem como para o método view
de um componente.
var ComponentWithDynamicState = {
oninit: function (vnode) {
vnode.state.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithDynamicState, { text: 'Hello' });
// HTML Equivalente
// <div>Hello</div>
Através da palavra-chave this
O state também pode ser acessado através da palavra-chave this
, que está disponível para todos os métodos de ciclo de vida, bem como para o método view
de um componente.
var ComponentUsingThis = {
oninit: function (vnode) {
this.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', this.data);
},
};
m(ComponentUsingThis, { text: 'Hello' });
// HTML Equivalente
// <div>Hello</div>
Esteja ciente de que, ao usar funções ES5, o valor de this
em funções anônimas aninhadas não é a instância do componente. Existem duas maneiras recomendadas de contornar essa limitação do JavaScript: usar arrow functions ou, se elas não forem suportadas, usar vnode.state
.
Classes
Se for adequado às suas necessidades (como em projetos orientados a objetos), os componentes também podem ser escritos usando classes:
class ClassComponent {
constructor(vnode) {
this.kind = 'class component';
}
view() {
return m('div', `Hello from a ${this.kind}`);
}
oncreate() {
console.log(`A ${this.kind} was created`);
}
}
Componentes de classe devem definir um método view()
, detectado via .prototype.view
, para obter a árvore a ser renderizada.
Eles podem ser utilizados da mesma forma que os componentes regulares.
// EXEMPLO: via m.render
m.render(document.body, m(ClassComponent));
// EXEMPLO: via m.mount
m.mount(document.body, ClassComponent);
// EXEMPLO: via m.route
m.route(document.body, '/', {
'/': ClassComponent,
});
// EXEMPLO: composição de componentes
class AnotherClassComponent {
view() {
return m('main', [m(ClassComponent)]);
}
}
Estado de componente de classe
Com classes, o state pode ser gerenciado por propriedades e métodos de instância de classe, e acessado via this
:
class ComponentWithState {
constructor(vnode) {
this.count = 0;
}
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
view() {
return m(
'div',
m('p', 'Count: ', this.count),
m(
'button',
{
onclick: () => {
this.increment();
},
},
'Increment'
),
m(
'button',
{
onclick: () => {
this.decrement();
},
},
'Decrement'
)
);
}
}
Note que devemos usar arrow functions para os callbacks do manipulador de eventos para que o contexto this
possa ser referenciado corretamente.
Misturando tipos de componentes
Os componentes podem ser misturados livremente. Um componente de classe pode ter componentes de closure ou POJO como filhos, etc.
Atributos especiais
Mithril.js atribui semânticas especiais a várias chaves de propriedade, portanto, você deve normalmente evitar usá-las em atributos de componentes normais.
- Métodos de ciclo de vida:
oninit
,oncreate
,onbeforeupdate
,onupdate
,onbeforeremove
eonremove
key
, que é usada para rastrear a identidade em fragmentos com chavetag
, que é usada para distinguir vnodes de objetos de atributos normais e outras coisas que são objetos não-vnode.
Evite anti-padrões
Embora Mithril.js seja flexível, alguns padrões de código são desencorajados:
Evite componentes gordos (fat components)
De modo geral, um componente "gordo" é um componente que tem métodos de instância personalizados. Em outras palavras, você deve evitar anexar funções a vnode.state
ou this
. É extremamente raro ter lógica que se encaixe logicamente em um método de instância de componente e que não possa ser reutilizada por outros componentes. É relativamente comum que essa lógica possa ser necessária por um componente diferente no futuro.
É mais fácil refatorar o código se essa lógica for colocada na camada de dados do que se estiver vinculada ao state de um componente.
Considere este componente gordo:
// views/Login.js
// EVITE
var Login = {
username: '',
password: '',
setUsername: function (value) {
this.username = value;
},
setPassword: function (value) {
this.password = value;
},
canSubmit: function () {
return this.username !== '' && this.password !== '';
},
login: function () {
/*...*/
},
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
this.setUsername(e.target.value);
},
value: this.username,
}),
m('input[type=password]', {
oninput: function (e) {
this.setPassword(e.target.value);
},
value: this.password,
}),
m(
'button',
{ disabled: !this.canSubmit(), onclick: this.login },
'Login'
),
]);
},
};
Normalmente, no contexto de uma aplicação maior, um componente de login como o acima existe ao lado de componentes para registro de usuário e recuperação de senha. Imagine que queremos ser capazes de preencher previamente o campo de e-mail ao navegar da tela de login para as telas de registro ou recuperação de senha (ou vice-versa), para que o usuário não precise redigitar seu e-mail se ele preencheu a página errada (ou talvez você queira direcionar o usuário para o formulário de registro se um nome de usuário não for encontrado).
Imediatamente, vemos que compartilhar os campos username
e password
deste componente para outro é difícil. Isso ocorre porque o componente gordo encapsula seu state, o que, por definição, torna esse state difícil de acessar de fora.
Faz mais sentido refatorar este componente e retirar o código de state do componente e colocá-lo na camada de dados da aplicação. Isso pode ser tão simples quanto criar um novo módulo:
// models/Auth.js
// PREFIRA
var Auth = {
username: '',
password: '',
setUsername: function (value) {
Auth.username = value;
},
setPassword: function (value) {
Auth.password = value;
},
canSubmit: function () {
return Auth.username !== '' && Auth.password !== '';
},
login: function () {
/*...*/
},
};
module.exports = Auth;
Então, podemos limpar o componente:
// views/Login.js
// PREFIRA
var Auth = require('../models/Auth');
var Login = {
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
Auth.setUsername(e.target.value);
},
value: Auth.username,
}),
m('input[type=password]', {
oninput: function (e) {
Auth.setPassword(e.target.value);
},
value: Auth.password,
}),
m(
'button',
{
disabled: !Auth.canSubmit(),
onclick: Auth.login,
},
'Login'
),
]);
},
};
Dessa forma, o módulo Auth
agora é a fonte confiável para o state relacionado à autenticação, e um componente Register
pode acessar facilmente esses dados e até mesmo reutilizar métodos como canSubmit
, se necessário. Além disso, se o código de validação for necessário (por exemplo, para o campo de e-mail), você só precisa modificar setEmail
, e essa alteração fará a validação de e-mail para qualquer componente que modifique um campo de e-mail.
Como um bônus, observe que não precisamos mais usar .bind
para manter uma referência ao state para os manipuladores de eventos do componente.
Não encaminhe o próprio vnode.attrs
para outros vnodes
Às vezes, você pode querer manter uma interface flexível e sua implementação mais simples, encaminhando atributos para um componente ou elemento filho específico, neste caso a modal do Bootstrap. Pode ser tentador encaminhar os atributos de um vnode assim:
// EVITE
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
// encaminhando `vnode.attrs` aqui ^
// ...
]);
},
};
Se você fizer como acima, você pode ter problemas ao usá-lo:
var MyModal = {
view: function () {
return m(
Modal,
{
// Isso o alterna duas vezes, então ele não aparece
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
[
// ...
]
);
},
};
Em vez disso, você deve encaminhar atributos únicos para vnodes:
// PREFIRA
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
// encaminhando `attrs:` aqui ^
// ...
]);
},
};
// Exemplo
var MyModal = {
view: function () {
return m(Modal, {
attrs: {
// Isso o alterna uma vez
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
// ...
});
},
};
Não manipule children
Se um componente é opinativo em como ele aplica atributos ou filhos, você deve mudar para usar atributos personalizados.
Muitas vezes, é desejável definir vários conjuntos de filhos, por exemplo, se um componente tem um título e corpo configuráveis.
Evite desestruturar a propriedade children
para este propósito.
// EVITE
var Header = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.children[0]),
m('.tagline', vnode.children[1]),
]);
},
};
m(Header, [m('h1', 'My title'), m('h2', 'Lorem ipsum')]);
// Caso de uso de consumo estranho
m(Header, [
[m('h1', 'My title'), m('small', 'A small note')],
m('h2', 'Lorem ipsum'),
]);
O componente acima quebra a suposição de que os filhos serão output no mesmo formato contíguo em que são recebidos. É difícil entender o componente sem ler sua implementação. Em vez disso, use atributos como parâmetros nomeados e reserve children
para conteúdo filho uniforme:
// PREFIRA
var BetterHeader = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.attrs.title),
m('.tagline', vnode.attrs.tagline),
]);
},
};
m(BetterHeader, {
title: m('h1', 'My title'),
tagline: m('h2', 'Lorem ipsum'),
});
// Caso de uso de consumo mais claro
m(BetterHeader, {
title: [m('h1', 'My title'), m('small', 'A small note')],
tagline: m('h2', 'Lorem ipsum'),
});
Defina componentes estaticamente, chame-os dinamicamente
Evite criar definições de componentes dentro de views
Se você criar um componente de dentro de um método view
(diretamente inline ou chamando uma função que o faça), cada redesenho terá um clone diferente do componente. Ao comparar vnodes de componente, se o componente referenciado pelo novo vnode não for estritamente igual ao referenciado pelo componente antigo, os dois são considerados componentes diferentes, mesmo que, em última análise, executem código equivalente. Isso significa que os componentes criados dinamicamente através de uma fábrica sempre serão recriados do zero.
Por esse motivo, você deve evitar recriar componentes. Em vez disso, utilize componentes idiomaticamente.
// EVITE
var ComponentFactory = function (greeting) {
// cria um novo componente a cada chamada
return {
view: function () {
return m('div', greeting);
},
};
};
m.render(document.body, m(ComponentFactory('hello')));
// chamar uma segunda vez recria div do zero em vez de não fazer nada
m.render(document.body, m(ComponentFactory('hello')));
// PREFIRA
var Component = {
view: function (vnode) {
return m('div', vnode.attrs.greeting);
},
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// chamar uma segunda vez não modifica o DOM
m.render(document.body, m(Component, { greeting: 'hello' }));
Evite criar instâncias de componentes fora de views
Inversamente, por razões semelhantes, se uma instância de componente for criada fora de uma view, os redesenhos futuros executarão uma verificação de igualdade no nó e o ignorarão. Portanto, as instâncias de componentes devem sempre ser criadas dentro de views:
// EVITE
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
var counter = m(Counter);
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), counter];
},
});
No exemplo acima, clicar no botão do componente contador aumentará sua contagem de state, mas sua view não será disparada porque o vnode que representa o componente compartilha a mesma referência e, portanto, o processo de renderização não os compara. Você deve sempre chamar componentes na view para garantir que um novo vnode seja criado:
// PREFIRA
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), m(Counter)];
},
});