Como hemos visto el desarrollo de aplicaciones web modernas suele seguir el patrón de arquitectura de software Modelo-Vista-Controlador (MVC). Este patrón propone separar la aplicación en tres componentes principales que se encargan de gestionar distintos aspectos de la misma. Esta separación facilita la organización del código, su mantenimiento y la colaboración entre varios desarrolladores.
El controlador actúa como intermediario entre el modelo y la vista, gestionando las interacciones del usuario y actualizando tanto el modelo como la vista según sea necesario. Cuando el usuario realiza una acción (como hacer clic en un botón o enviar un formulario), el controlador recibe esta acción, procesa la lógica de negocio correspondiente y actualiza el modelo. Luego, el controlador solicita a la vista que se actualice para reflejar los cambios en el modelo.
Para recibir las acciones del usuario en aplicaciones web se deben capturar eventos del DOM (Document Object Model), pero quién debe capturar estos eventos, la vista o el controlador?
Recordemos el ejemplo de la aplicación MVC para gestionar un almacén. En el HTML habrá un formulario para añadir nuevos productos al almacén:
<form id="product-form">
<input type="text" id="product-form-name" name="name">
<input type="number" id="product-form-price" name="price">
...
<button type="submit">Añadir producto</button>
</form>
El controlador debe tener un método, handleSubmitProductForm, que recibe los datos de un nuevo producto y se encarga de añadirlo al almacén cuando el usuario envía el formulario correspondiente:
export default class Controller {
constructor() {
this.store = new Store(1)
this.view = new View()
}
init() {
this.store.init()
this.view.init()
...
}
handleSubmitProductForm(payload) {
// haría las comprobaciones necesarias sobre los datos
if (!payload.name || payload.name.length < 5 || ...) {
this.view.showErrorMessage('error', 'Datos incorrectos')
return
}
...
// y luego dice al modelo que añada el producto
try {
const newProd = this.store.addProduct(payload)
// si lo ha hecho le dice a la vista que lo pinte
this.view.renderNewProduct(newProd)
} catch(err) {
this.view.showErrorMessage('error', 'Error al añadir el producto')
}
}
...
}
Y la vista un método, renderNewProduct para pintar el nuevo libro cuando el controlador se lo pase.
Siguiendo el patrón MVC puro, el controlador es el encargado de gestionar las interacciones del usuario con la aplicación. Por lo tanto, el controlador debería ser quien capture los eventos del DOM y responda a ellos. Pero para ello debe conocer los elementos del DOM que generan esos eventos, y esos elementos son gestionados por la vista.
En nuestro ejemplo:
export default class Controller {
...
init() {
this.store.init()
this.view.init()
// Pone el escuchador y coge los datos del formulario
document.getElementById('product-form').addEventListener('submit', (event) => {
event.preventDefault()
const name = document.getElementById('product-form-name').value
const price = document.getElementById('product-form-price').value
...
this.handleSubmitProductForm({ name, price, ... })
})
}
}
Esta opción es la más sencilla pero el problema es que el controlador ahora depende del HTML (usa los ids de los elementos del formulario). Si cambiamos el HTML tendremos que modificar el controlador, lo que rompe la separación entre vista y controlador.
Una mejora sería que la vista proporcionara el acceso a los elementos del DOM que el controlador necesita para capturar los eventos:
export default class View {
constructor {
this.productForm = document.getElementById('product-form')
this.productFormName = document.getElementById('product-form-name')
this.productFormPrice = document.getElementById('product-form-price')
...
}
...
}
De esta forma el controlador puede acceder a ellos sin depender del HTML:
export default class Controller {
...
init() {
this.store.init()
this.view.init()
// Pone el escuchador y coge los datos del formulario
this.view.productForm.addEventListener('submit', (event) => {
event.preventDefault()
const name = this.view.productFormName.value
const price = this.view.productFormPrice.value
...
this.handleSubmitProductForm({ name, price, ... })
})
}
}
Otra mejora aún mejor es que la vista capture los eventos del DOM y proporcione un método para que el controlador pueda suscribirse a dichos eventos del usuario sin necesidad de acceder a los elementos del DOM directamente. De esta forma el controlador no depende del HTML en absoluto.
Para suscribirse el controlador llama al método de la vista, setSubmitHandler, que pone el escuchador y obtiene los datos del formulario, pasándole como argumento la función del controlador a la que debe llamar (y pasarle los datos) cuando se envíe el formulario:
export default class Controller {
...
init() {
this.store.init()
this.view.init()
// Le indica a la vista qué función del controlador se encargará
// de procesar el envío del formulario
this.view.setSubmitHandler(this.handleSubmitProductForm.bind(this))
}
}
Y todo el código relacionado con el evento del formulario estará en la vista:
export default class View {
...
setSubmitHandler(callback) {
this.productForm.addEventListener('submit', (event) => {
event.preventDefault()
const name = this.productFormName.value
const price = this.productFormPrice.value
...
callback({ name, price, ... })
})
}
}
Esta opción es la más recomendable ya que mantiene una clara separación entre la vista y el controlador, facilitando el mantenimiento y la escalabilidad de la aplicación.