Componentes
Estructura
Los componentes son un mecanismo para encapsular partes de una vista, facilitando la organización y reutilización del código.
Cualquier objeto de JavaScript que tenga un método view
es un componente en Mithril.js. Los componentes se pueden usar a través de la utilidad m()
:
// Define tu componente
var Example = {
view: function (vnode) {
return m('div', 'Hola');
},
};
// Usa tu componente
m(Example);
// HTML resultante
// <div>Hola</div>
Métodos de ciclo de vida
Los componentes pueden tener los mismos métodos de ciclo de vida que los nodos del DOM virtual. Es importante tener en cuenta que vnode
se pasa como argumento a cada método de ciclo de vida, así como a view
(con el vnode
anterior pasado adicionalmente a onbeforeupdate
):
var ComponentWithHooks = {
oninit: function (vnode) {
console.log('inicializado');
},
oncreate: function (vnode) {
console.log('DOM creado');
},
onbeforeupdate: function (newVnode, oldVnode) {
return true;
},
onupdate: function (vnode) {
console.log('DOM actualizado');
},
onbeforeremove: function (vnode) {
console.log('la animación de salida puede iniciarse');
return new Promise(function (resolve) {
// Llama a resolve después de que se complete la animación
resolve();
});
},
onremove: function (vnode) {
console.log('eliminando el elemento del DOM');
},
view: function (vnode) {
return 'hola';
},
};
Al igual que otros tipos de nodos del DOM virtual, los componentes pueden tener métodos de ciclo de vida adicionales definidos cuando se usan como tipos de vnode.
function initialize(vnode) {
console.log('inicializado como vnode');
}
m(ComponentWithHooks, { oninit: initialize });
Los métodos de ciclo de vida en los vnodes no sobrescriben los métodos de los componentes, ni viceversa. Los métodos de ciclo de vida de los componentes siempre se ejecutan después del método correspondiente del vnode.
Evita usar nombres de métodos de ciclo de vida como nombres para tus propias funciones de callback en los vnodes.
Para obtener más información sobre los métodos de ciclo de vida, consulta la página de métodos de ciclo de vida.
Pasar datos a los componentes
Se pueden pasar datos a las instancias de los componentes pasando un objeto attrs
como segundo parámetro en la función hyperscript:
m(Example, { name: 'Floyd' });
Estos datos son accesibles en la vista del componente o en los métodos de ciclo de vida mediante vnode.attrs
:
var Example = {
view: function (vnode) {
return m('div', 'Hola, ' + vnode.attrs.name);
},
};
NOTA: También puedes definir métodos de ciclo de vida en el objeto attrs
, por lo que debes evitar usar sus nombres para tus propios callbacks, ya que también serían invocados por Mithril.js. Úsalos en attrs
solo cuando quieras usarlos específicamente como métodos de ciclo de vida.
Estado
Al igual que todos los nodos del DOM virtual, los vnodes de los componentes pueden tener estado. El estado del componente es útil para soportar arquitecturas orientadas a objetos, para la encapsulación y para la separación de responsabilidades.
Ten en cuenta que, a diferencia de muchos otros frameworks, la mutación del estado del componente no provoca repintados ni actualizaciones del DOM. En cambio, los repintados ocurren cuando se activan los controladores de eventos, cuando se completan las solicitudes HTTP realizadas por m.request o cuando el navegador navega a diferentes rutas. Los mecanismos de estado de los componentes de Mithril.js simplemente existen como una conveniencia para las aplicaciones.
Si se produce un cambio de estado que no es el resultado de ninguna de las condiciones anteriores (por ejemplo, después de un setTimeout
), puedes usar m.redraw()
para desencadenar un repintado manualmente.
Estado del componente de cierre
En los ejemplos anteriores, cada componente se define como un POJO (Plain Old JavaScript Object, Objeto JavaScript Simple), que Mithril.js utiliza internamente como prototipo para las instancias de ese componente. Puedes usar el estado del componente con un POJO (como veremos a continuación), pero no es el enfoque más limpio ni el más simple. Para eso, usaremos un componente de cierre, que es simplemente una función envolvente que devuelve una instancia de componente POJO, que a su vez tiene su propio ámbito encapsulado.
Con un componente de cierre, el estado se puede mantener simplemente mediante variables que se declaran dentro de la función externa:
function ComponentWithState(initialVnode) {
// Variable de estado del componente, única para cada instancia
var count = 0;
// Instancia de componente POJO: cualquier objeto con una
// función view que devuelve un vnode
return {
oninit: function (vnode) {
console.log('inicializar un componente de cierre');
},
view: function (vnode) {
return m(
'div',
m('p', 'Contador: ' + count),
m(
'button',
{
onclick: function () {
count += 1;
},
},
'Incrementar contador'
)
);
},
};
}
Cualquier función declarada dentro del cierre tiene acceso a sus variables de estado.
function ComponentWithState(initialVnode) {
var count = 0;
function increment() {
count += 1;
}
function decrement() {
count -= 1;
}
return {
view: function (vnode) {
return m(
'div',
m('p', 'Contador: ' + count),
m(
'button',
{
onclick: increment,
},
'Incrementar'
),
m(
'button',
{
onclick: decrement,
},
'Decrementar'
)
);
},
};
}
Los componentes de cierre se usan de la misma manera que los POJO, por ejemplo, m(ComponentWithState, { passedData: ... })
.
Una gran ventaja de los componentes de cierre es que no tenemos que preocuparnos por vincular this
al adjuntar callbacks de controladores de eventos. De hecho, this
nunca se usa y nunca tenemos que pensar en las ambigüedades del contexto this
.
Estado del componente POJO
En general, se recomienda que uses cierres para gestionar el estado del componente. Sin embargo, si tienes una razón para administrar el estado en un POJO, el estado de un componente se puede acceder de tres maneras: como un esquema en la inicialización, a través de vnode.state
y a través de la palabra clave this
en los métodos del componente.
En la inicialización
Para los componentes POJO, el objeto del componente es el prototipo de cada instancia del componente, por lo que cualquier propiedad definida en el objeto del componente será accesible como una propiedad de vnode.state
. Esto permite una inicialización simple del estado.
En el ejemplo siguiente, data
se convierte en una propiedad del objeto vnode.state
del componente ComponentWithInitialState
.
var ComponentWithInitialState = {
data: 'Contenido inicial',
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithInitialState);
// HTML equivalente
// <div>Contenido inicial</div>
A través de vnode.state
Como puedes ver, también se puede acceder al estado a través de la propiedad vnode.state
, la cual está disponible para todos los métodos del ciclo de vida, así como para el método view
de un componente.
var ComponentWithDynamicState = {
oninit: function (vnode) {
vnode.state.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithDynamicState, { text: 'Hola' });
// HTML equivalente
// <div>Hola</div>
A través de la palabra clave this
También se puede acceder al estado a través de la palabra clave this
, que está disponible para todos los métodos de ciclo de vida, así como para el método view
de un componente.
var ComponentUsingThis = {
oninit: function (vnode) {
this.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', this.data);
},
};
m(ComponentUsingThis, { text: 'Hola' });
// HTML equivalente
// <div>Hola</div>
Ten en cuenta que cuando uses funciones ES5, el valor de this
en las funciones anónimas anidadas no es la instancia del componente. Hay dos formas recomendadas de evitar esta limitación de JavaScript: usar funciones de flecha o, si no son compatibles, usar vnode.state
.
Clases
Si se ajusta a tus necesidades (como en proyectos orientados a objetos), los componentes también se pueden escribir usando clases:
class ClassComponent {
constructor(vnode) {
this.kind = 'componente de clase';
}
view() {
return m('div', `Hola desde un ${this.kind}`);
}
oncreate() {
console.log(`Se creó un ${this.kind}`);
}
}
Los componentes de clase deben definir un método view()
, detectado a través de .prototype.view
, para que el árbol se renderice.
Se pueden usar de la misma manera que los componentes normales.
// EJEMPLO: a través de m.render
m.render(document.body, m(ClassComponent));
// EJEMPLO: a través de m.mount
m.mount(document.body, ClassComponent);
// EJEMPLO: a través de m.route
m.route(document.body, '/', {
'/': ClassComponent,
});
// EJEMPLO: composición de componentes
class AnotherClassComponent {
view() {
return m('main', [m(ClassComponent)]);
}
}
Estado del componente de clase
Con las clases, el estado se puede administrar mediante propiedades y métodos de instancia de clase, y se puede acceder a través de this
:
class ComponentWithState {
constructor(vnode) {
this.count = 0;
}
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
view() {
return m(
'div',
m('p', 'Contador: ', this.count),
m(
'button',
{
onclick: () => {
this.increment();
},
},
'Incrementar'
),
m(
'button',
{
onclick: () => {
this.decrement();
},
},
'Decrementar'
)
);
}
}
Ten en cuenta que debemos usar funciones de flecha para los callbacks de los controladores de eventos para que el contexto this
pueda referenciarse correctamente.
Mezcla de tipos de componentes
Los componentes pueden mezclarse libremente. Un componente de clase puede tener componentes de cierre o POJO como hijos, etc.
Atributos especiales
Mithril.js asigna una semántica especial a varias claves de propiedad, por lo que normalmente debes evitar usarlas en los atributos normales de los componentes.
- Métodos de ciclo de vida:
oninit
,oncreate
,onbeforeupdate
,onupdate
,onbeforeremove
yonremove
key
, que se usa para rastrear la identidad en fragmentos con clavetag
, que se usa para distinguir los vnodes de los objetos de atributos normales y otras cosas que no son objetos vnode.
Evitar antipatrones
Aunque Mithril.js es flexible, hay algunos patrones de código que se desaconsejan.
Evitar componentes gordos
En general, un componente "gordo" es aquel que tiene métodos de instancia personalizados. En otras palabras, debes evitar adjuntar funciones a vnode.state
o this
. Es muy raro tener lógica que encaje en un método de instancia de componente y que no pueda ser reutilizada por otros componentes. Es relativamente común que dicha lógica pueda ser necesaria para un componente diferente en el futuro.
Es más fácil refactorizar el código si esa lógica se coloca en la capa de datos que si está vinculada al estado de un componente.
Considera este componente gordo:
// views/Login.js
// EVITAR
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, dentro del contexto de una aplicación más grande, un componente de inicio de sesión como el anterior existe junto con los componentes para el registro de usuarios y la recuperación de contraseñas. Imagina que queremos poder rellenar previamente el campo de correo electrónico al navegar desde la pantalla de inicio de sesión a las pantallas de registro o recuperación de contraseñas (o viceversa), para que el usuario no tenga que volver a escribir su correo electrónico si accidentalmente llenó la página incorrecta (o tal vez quieras dirigir al usuario al formulario de registro si no se encuentra un nombre de usuario).
Inmediatamente, vemos que compartir los campos username
y password
de este componente a otro es complicado. Esto se debe a que el componente gordo encapsula su estado, lo que, por definición, hace que este estado sea difícil de acceder desde el exterior.
Tiene más sentido refactorizar este componente y extraer el código de estado a la capa de datos de la aplicación. Esto puede ser tan simple como crear un nuevo módulo:
// models/Auth.js
// PREFERIR
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;
Luego, podemos limpiar el componente:
// views/Login.js
// PREFERIR
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'
),
]);
},
};
De esta forma, el módulo Auth
se convierte en la fuente de verdad para el estado relacionado con la autenticación, y un componente Register
puede acceder fácilmente a estos datos, e incluso reutilizar métodos como canSubmit
, si es necesario. Además, si se requiere código de validación (por ejemplo, para el campo de correo electrónico), solo necesitas modificar setEmail
, y ese cambio realizará la validación del correo electrónico para cualquier componente que modifique un campo de correo electrónico.
Como ventaja adicional, nota que ya no necesitamos usar .bind
para conservar la referencia al estado en los controladores de eventos del componente.
No reenvíes vnode.attrs
en sí mismo a otros vnodes
A veces, podrías querer mantener una interfaz flexible y tu implementación más simple reenviando atributos a un componente o elemento hijo en particular, en este caso el modal de Bootstrap. Podría ser tentador reenviar los atributos de un vnode de esta manera:
// EVITAR
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
// reenviando `vnode.attrs` aquí ^
// ...
]);
},
};
Si lo haces así, podrías tener problemas al usarlo:
var MyModal = {
view: function () {
return m(
Modal,
{
// Esto lo activa dos veces, por lo que no se muestra
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
[
// ...
]
);
},
};
En cambio, debes reenviar atributos individuales a los vnodes:
// PREFERIR
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
// reenviando `attrs:` aquí ^
// ...
]);
},
};
// Ejemplo
var MyModal = {
view: function () {
return m(Modal, {
attrs: {
// Esto lo activa una vez
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
// ...
});
},
};
No manipules children
Si un componente tiene una opinión sobre cómo aplica los atributos o los hijos, debes cambiar a usar atributos personalizados.
A menudo, es deseable definir múltiples conjuntos de hijos, por ejemplo, si un componente tiene un título y un cuerpo configurables.
Evita desestructurar la propiedad children
para este propósito.
// EVITAR
var Header = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.children[0]),
m('.tagline', vnode.children[1]),
]);
},
};
m(Header, [m('h1', 'Mi título'), m('h2', 'Lorem ipsum')]);
// caso de uso de consumo incómodo
m(Header, [
[m('h1', 'Mi título'), m('small', 'Una pequeña nota')],
m('h2', 'Lorem ipsum'),
]);
El componente anterior rompe la suposición de que los hijos se mostrarán en la misma estructura continua en que se reciben. Es difícil entender el componente sin leer su implementación. En cambio, usa atributos como parámetros con nombre y reserva children
para contenido hijo uniforme:
// PREFERIR
var BetterHeader = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.attrs.title),
m('.tagline', vnode.attrs.tagline),
]);
},
};
m(BetterHeader, {
title: m('h1', 'Mi título'),
tagline: m('h2', 'Lorem ipsum'),
});
// caso de uso de consumo más claro
m(BetterHeader, {
title: [m('h1', 'Mi título'), m('small', 'Una pequeña nota')],
tagline: m('h2', 'Lorem ipsum'),
});
Define los componentes estáticamente, llámalos dinámicamente
Evita crear definiciones de componentes dentro de las vistas
Si creas un componente desde dentro de un método view
(ya sea directamente en línea o llamando a una función que lo haga), cada repintado tendrá un clon diferente del componente. Al comparar vnodes, si el componente referenciado por el nuevo vnode no es idéntico al referenciado por el vnode anterior, se asume que los dos son componentes diferentes, incluso si finalmente ejecutan código equivalente. Esto significa que los componentes creados dinámicamente a través de una fábrica siempre se volverán a crear desde cero.
Por esa razón, deberías evitar recrear componentes. En cambio, usa los componentes de la manera habitual.
// EVITAR
var ComponentFactory = function (greeting) {
// crea un nuevo componente en cada llamada
return {
view: function () {
return m('div', greeting);
},
};
};
m.render(document.body, m(ComponentFactory('hello')));
// llamar una segunda vez recrea div desde cero en lugar de no hacer nada
m.render(document.body, m(ComponentFactory('hello')));
// PREFERIR
var Component = {
view: function (vnode) {
return m('div', vnode.attrs.greeting);
},
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// llamar una segunda vez no modifica el DOM
m.render(document.body, m(Component, { greeting: 'hello' }));
Evita crear instancias de componentes fuera de las vistas
De manera similar, si creas una instancia de componente fuera de una vista, los redibujados posteriores harán una comparación de igualdad y la omitirán. Por lo tanto, las instancias de componentes siempre deben crearse dentro de las vistas:
// EVITAR
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Contador: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Aumentar contador'
)
);
},
};
var counter = m(Counter);
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'Mi aplicación'), counter];
},
});
En el ejemplo anterior, al hacer clic en el botón del componente contador, aumentará su recuento de estado, pero su vista no se activará porque el vnode que representa el componente comparte la misma referencia y, por lo tanto, el proceso de renderizado no los compara. Siempre debes invocar los componentes en la vista para asegurarte de que se cree un nuevo vnode:
// PREFERIR
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Contador: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Aumentar contador'
)
);
},
};
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'Mi aplicación'), m(Counter)];
},
});