La actualización de la apariencia basada en el estado generalmente se separa en una capa separada llamada vista (View). En el caso más simple, la vista es una función que recibe el estado como entrada, lo analiza y realiza los cambios necesarios en el DOM.
// Es mejor separarlo en un archivo aparte, ya que generalmente hay mucho código allí
import render from './view.js';
const state = /* datos de estado */;
const input = document.querySelector(/* .... */);
input.addEventListener('keyup', (e) => {
state.registrationForm.value = e.target.value;
if (input.value === '' || input.value.match(/^\d+$/)) {
state.registrationForm.valid = true;
state.registrationForm.errors = [];
} else {
state.registrationForm.valid = false;
state.registrationForm.errors.push('formato incorrecto');
}
render(state);
});
Un ejemplo de cómo podría ser render() internamente:
const render = (state) => {
const submit = document.querySelector(/* .... */);
const input = document.querySelector(/* .... */);
submit.disabled = !state.registrationForm.valid;
if (state.registrationForm.valid) {
input.style.border = null;
} else {
input.style.border = 'thick solid red';
}
};
Este enfoque puede tener problemas de rendimiento debido a la necesidad de buscar constantemente elementos en el DOM. Para evitar esto, podemos convertir la vista en un objeto y guardar todos los elementos necesarios en él solo una vez, en el momento de la inicialización.
// Implementación utilizando clases:
class View {
constructor(){
this.submit = null;
this.input = null;
}
init() {
this.submit = document.querySelector(/* .... */);
this.input = document.querySelector(/* .... */);
}
render(state) {
this.submit.disabled = !state.registrationForm.valid;
if (state.registrationForm.valid) {
this.input.style.border = null;
} else {
this.input.style.border = 'thick solid red';
}
}
}
const view = new View();
view.init(); // aquí hacemos las selecciones
view.render(state);
// Implementación utilizando funciones:
const init = () => {
const submit = document.querySelector(/* .... */);
const input = document.querySelector(/* .... */);
const state = /* datos de estado */;
// ...
return {
state,
elements: {
input,
submit,
},
};
};
const render = (elements) => { ... };
const elements = init();
render(elements);
A medida que la aplicación crece, también aumenta la cantidad de controladores. Cada uno de ellos puede llevar a cambios solo en una parte de la página. ¿Cómo manejar esto?
La solución más simple sería vincular esas funciones a los elementos del estado. Supongamos que tenemos una página de administración de una lista de lecciones en un curso. En el estado, se vería así:
const state = {
lessons: [/* lista de lecciones */],
// el resto del estado
};
Para representar esta lista, una función renderLessons() sería adecuada, que se llamaría en todos los controladores que modifican esta lista: eliminando o agregando elementos.
// Agregar
el1.addEventListener('submit', (e) => {
// lógica
renderLessons(state.lessons);
})
// Eliminar
el2.addEventListener('submit', (e) => {
// lógica
renderLessons(state.lessons);
})
Y lo más interesante, ¿qué sucede dentro de esta función? Parece que dentro del proceso de renderizado, debemos determinar lo que ha ocurrido y luego actualizar la parte correspondiente del DOM, por ejemplo, eliminar un elemento que ya no existe. Sin embargo, este enfoque resulta muy costoso y complicado de programar, ya que implica muchas estructuras condicionales. Es mucho más sencillo realizar una representación completa en cada ocasión. Así, el código se mantiene lo más simple posible.
Este enfoque tiene una desventaja importante en términos de rendimiento. No obstante, es crucial considerar dos puntos importantes. En primer lugar, el rendimiento no siempre es un problema. Por ejemplo, al implementar una función de autocompletado, es exactamente lo que se necesita hacer y funcionará rápidamente de todos modos. En segundo lugar, este problema se resuelve mediante el uso de los modernos frameworks frontend, los cuales saben cómo actualizar el DOM de manera eficiente.
Ahora nuestra aplicación se divide en tres partes independientes: el estado (los datos de la aplicación), los controladores y la representación. Este modelo puede parecer redundante en aplicaciones simples (con solo unos pocos controladores), pero cuando hay al menos 10 controladores, se puede apreciar su conveniencia en el trabajo con la aplicación. Se puede observar el flujo de datos, es decir, cómo se mueven los datos dentro de la aplicación, desde los controladores hasta la representación en el DOM. Siempre es posible rastrear qué ha cambiado y cómo una parte de la aplicación depende de otra.
Además, se reduce la duplicación. Por ejemplo, un cambio en el estado puede originarse en diferentes partes de la aplicación, pero la lógica de representación sigue siendo la misma. En tal situación, solo es necesario describir una nueva forma de modificar el estado existente y la representación se encargará del resto.
Además de tener esta división en tres partes, es igualmente importante cómo interactúan entre sí:
- El estado no sabe nada sobre las otras partes del sistema: es el núcleo
- La representación utiliza el estado para representar (agregar, cambiar o eliminar elementos) y agrega nuevos controladores al DOM
- Los controladores conocen el estado, ya que lo actualizan e inician la representación
Este enfoque de división todavía tiene una desventaja importante, que resolveremos en la lección dedicada a MVC.
Para acceder completo a curso necesitas un plan básico
El plan básico te dará acceso completo a todos los cursos, ejercicios y lecciones de Códica, proyectos y acceso de por vida a la teoría de las lecciones completadas. La suscripción se puede cancelar en cualquier momento.