CRUD en Javascript i Firebase

De Jose Castillo Aliaga
Ir a la navegación Ir a la búsqueda
La versión para imprimir ya no se admite y puede contener errores de representación. Actualiza los marcadores del navegador y utiliza en su lugar la función de impresión predeterminada del navegador.

Aquest article és un tutorial i no pretén aprofundir en la teoria de Javascript o de Firebase. Sols explicarem les coses que necessitem per a l'objectiu.

Les possibilitats quan ens plantegem cóm fer una web amb frontend fet en Javascript i un backend. En aquest article sols anem a tractar una: Frontend fet en Javascript pur a partir de ES6 front a un API REST generat per una base de dades Realtime del servici Firebase. Els avantatges de fer-ho així són:

  • Quasi total compatibilitat en qualsevol navegador amb un cost de computació mínim, ja que no necessitem llibreries externes ni frameworks.
  • Simplicitat si el projecte és menut.
  • Velocitat i alta disponibilitat de les dades gràcies a un servici extern molt fiable.
  • Simplicitat al tractar amb una base de dades en JSON.
  • Avantatges dels protocols REST.
  • No tindre que programar un backend tradicional (PHP o JAVA, MySQL...)

Els desavantatges són:

  • No comptar amb els beneficis d'un framework com Angular, Vue o React que simplifiquen la programació de grans projectes.
  • Possibilitat de fer-ho mal o insegur al no utilitzar llibreries per comunicar amb el servidor.
  • Dependència d'un servici extern.
  • No poder programar el backend i comptar sols amb una simple base de dades que s'ha de gestionar en el frontend.

Tan sols anem a fer una gestió de productes i llistes de productes amb la possibilitat de Crear Llegir Actualitzar i Esborrar (CRUD).

Configuració de Firebase

La documentació de Firebase és molt completa i de gran qualitat, per tant no cal entrar en detalls. Sols cal crear una base de dades Realtime a la que, de moment, li ficarem com a regles de control d'accés que tots puguen llegir i escriure sense autenticar:

{
  "rules": {
    ".read": "now < 1621116000000",  // 2021-5-16
    ".write": "now < 1621116000000",  // 2021-5-16
  }
}

En l'exemple anterior tenen un límit de data que es pot ampliar si volem.

En quant a les dades de la base de dades, anem a començar en un JSON molt simple de productes i llistes de productes:

{"listas":{"Lista1":{"nombre":"lista1","productos":[1,2,3,4]},"Lista2":{"nombre":"lista2","productos":[1,2,3,5]}},"productos":{"1":{"id":1,"marca":"Nikon","precio":649.95,"referencia":"D3400"},"2":{"id":2,"marca":"Nikon","precio":6499.95,"referencia":"D5"},"3":{"id":3,"marca":"Nikon","precio":1999.95,"referencia":"D500"},"4":{"id":4,"marca":"Nikon","precio":1049.95,"referencia":"D7200"},"5":{"id":5,"marca":"Nikon","precio":1999.95,"referencia":"D500"},"6":{"id":6,"marca":"Canon","precio":3999.95,"referencia":"EOS 5D"},"7":{"id":7,"marca":"Canon","precio":1800.95,"referencia":"EOS 7D"},"8":{"id":8,"marca":"Canon","precio":2645.95,"referencia":"EOS 80D"},"9":{"id":9,"marca":"Canon","precio":989.95,"referencia":"EOS Rebel T6"},"10":{"id":10,"marca":"Canon","precio":753.95,"referencia":"EOS-1D"},"11":{"id":11,"marca":"Canon","precio":457.95,"referencia":"EOS 1D X"}}}

És important que els elements del JSON tinguen clau:valor, és a dir, no utilitzem [] a no ser que siguen ja valors molt concrets.

Per a que funcione correctament les rutes en el API, tot han de ser objectes, no arrays. Això en donarà possiblement problemes quan descarreguem les dades, ja que per a passarles a objectes Javascript tal vegada tenim que passar d'objecte a array.

En aquest cas, tenim unes llistes de productes que apunten als identificadors dels productes. Com es veu, la clau de cada producte és igual al valor de l'atribut id del producte, això és redundant i es pot llevar. En versions posteriors el llevarem.

Amb aquestes dades, si volem accedir a:

  • La llista de tots els productes anirem a la URL: http://<url de firebase>/productos.json
  • Un producte en concret: http://<url de firebase>/productos/2.json
  • La llista de productes de lista1: http://<url de firebase>/listas/Lista1/productos.json

Ho podem provar amb curl:

curl https://<url>/listas/Lista1/productos.json

O amb algun programa més còmode per fer moltes proves com és Postman

Pàgina web HTML inicial

El que anem a fer és confiar totalment la càrrega de les dades a Javascript amb les tècniques de AJAX. Per tant, l'HTML inicial serà molt simple. No obstant, utilitzarem Bootstrap per fer el disseny més agradable sense esforç. Necessitem un div contenidor on anar generant les llistes i els productes que inicialment el deixarem buit. També farem un menú per seleccionar el que necessitem.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf"
        crossorigin="anonymous"></script>
   <script src="script.js"></script>

</head>

<body>
    <header>
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <div class="container-fluid">
                <a class="navbar-brand" href="#">Navbar</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
                    data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
                    aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                        <li class="nav-item">
                            <a class="nav-link active" aria-current="page" href="#">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="#">Todos los productos</a>
                        </li>
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
                                data-bs-toggle="dropdown" aria-expanded="false">
                                Listas
                            </a>
                            <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
                                <li><a class="dropdown-item" href="#">Action</a></li>
                                <li><a class="dropdown-item" href="#">Another action</a></li>
                                <li>
                                    <hr class="dropdown-divider">
                                </li>
                                <li><a class="dropdown-item" href="#">Something else here</a></li>
                            </ul>
                        </li>
                    </ul>

                </div>
            </div>
        </nav>
    </header>
    <main>
        <section>
            <div id="container">

            </div>
        </section>

    </main>
</body>
</html>

Es tracta d'una web molt simple amb un menú superior en el que podem mostrar tots els productes, les llistes i un menú desplegable on ficarem un enllaç a cada llista.

Estructura del Javascript

Encara que siga una aplicació simple anem a intentar respectar els principis de MVC. D'aquesta manera, anem a fer un model per a productes i llistes encarregat de la comunicació amb el servidor, un controlador encarregat de comunicar el model en la vista i una vista encarregada de transformar les dades en elements del DOM i de proporcionar interacció amb l'usuari.

Per a fer-ho encara més fàcil de mantindre, podem fer una classe genèrica de model, vista i controlador per compartir coses comuns entre les diferents classes com l'accés al servidor o els mètodes de dibuixar o refrescar.

Model

La tasca del model passa per definir les classes, la manera d'aconseguir les dades del servidor i les dades que ja tenim. Tindrà funcions per a fer el CRUD contra el servidor i getters i setters per a obtindre o crear objectes de les classes. Amés, pot implementar el patró Observer per mantindre als altres informats dels canvis. En la primera versió al menys no anem a utilitzar l'Observer.

A l'hora de fer el model ens podem plantejar si és interessant mantindre'l sols per a la definició de les dades i dividir-ho en model-servici per gestionar l'accés al servidor en una classe separada. D'aquesta manera, el controlador demanarà coses al servici el qual mantindrà instàncies del model. Es pot fer tot junt, però d'aquesta manera s'entén millor el rol de cadascun. En el nostre cas, el model quedarà de forma testimonial, ja que el servici serà el que arreplegue les dades del servidor. Això és paregut amb el que passa en Angular, el model es pot equiparar a les Interfícies i els servicis ho fan tot. Com que en Javascript no hi ha tipat de dades ni interfícies, el model queda sols per a coses internes concretes de les dades que no passen en el servidor ni en la vista. Per exemple, el càlcul aleatori d'un identificador o similars.

Com que hem decidit crear una classe model base, quedarà així:

export { Model }
    class Model {
        static nombre;
        constructor(id) {
            this.id = id;
        }
        assign(plainObject) {   // El que vinga del servidor cal assignar-ho a la classe actual
            Object.assign(this, plainObject);
        }
    }

Com es pot veure, és molt simple, sols implementa la funció assign per a que quede més simple el codi del servici.

Aprofitant que ja tenim definida la classe model, anem a fer el model de productes:

import { Model } from "./model.js";
export { Product }

    class Product extends Model {
        constructor(id) {  // En realitat no necessitem indicar tots els camps perquè els assignarem amb assign
            super(id);
        }
        static nombre = 'productos'
    }

El model de productes no necessita res especial, aleshores sols crida a la classe pare per al constructor i defineix el atribut estàtic nombre per a que siga més fàcil obtindre les dades del servidor.

Per a la comunicació del model amb el servidor hem separat la lògica en model i servici. La classe genèrica de Service treballarà en Items que poden ser productes o qualsevol cosa, ja que el API REST de Firebase sempre funcionarà igual:

export { Service };

class Service {
	constructor(url,model) {
		this.url = url;
		this.Items = [];
		this.read();
		this.model = model;
	}

	read() {  // funció cridada per les demés per actualitzar les dades
		fetch(this.url + '.json')
			.then(response => response.json())
			.then(datosItems => {
				// En firebase retorna un objecte amb tots els productes. Serà més fàcil si fem un array
				// de pas, afegim el key com a id

				this.Items = Object.entries(datosItems).map(entrie => { 
					entrie[1].id = entrie[0];
					let item = new this.model(entrie[1].id); 
					item.assign(entrie[1]);
					return item; 
				});
				console.log(this.Items);
				this.onCambioItems(this.Items); // cridem al callback de la vista associat per el cotrolador amb notificarcambios
			});
	}

	add(Item) {
		console.log('add', Item);
		fetch(this.url + '.json', { method: 'post', headers: { "Content-type": "application/json; charset=UTF-8" }, body: JSON.stringify(Item) })
			.then(response => response.json())
			.then(datos => {
				this.read();
			});
	}
	update(Item) {
		// Com que volem actualitzar, els Items tenen id, que no és necessari en firebase, ja que és la clau
		// primer li llevem el id:
		let key = Item.id;
		delete Item.id;
		console.log('update', key);
		fetch(`${this.url}/${key}.json`,
			{ method: 'put', headers: { "Content-type": "application/json; charset=UTF-8" }, body: JSON.stringify(Item) })
			.then(response => response.json())
			.then(datos => {
				this.read();
			});


	}
	remove(id) {
		console.log('remove', id);
		fetch(`${this.url}/${id}.json`, { method: 'delete', headers: { "Content-type": "application/json; charset=UTF-8" }, body: {} })
			.then(response => response.json())
			.then(datos => {
				this.read();
			});
	}

	setNotificarCambios(callback) {  // Funció per a que el controlador associe els canvis amb la vista
		this.onCambioItems = callback; // callback serà una funció de la vista
	}

}

Les funcions del CRUD s'entenen sense problemes. Amb fetch li fem la petició al servidor i en complir la promesa cridem a this.read() que torna a demanar la llista sencera al servidor per actualitzar. Això pot resultar ineficient, així que en el futur deuríem sols actualitzar els ítems modificats i no tornar a demanar al servidor totes les dades. Per simplicitat no ho hem fet en aquest exercici.

Com es veu, per a crear nous productes, hem utilitzat POST, no li passem el ID que tindrà, ja que serà generat automàticament per Firebase. Aquest ID serà retornat com a la clau de cada producte en la col·lecció que conté tots els productes. Si observem la funció read() el que fem és obtindre de l'objecte de tots els productes obtingut per fetch les entries(), és a dir, un array de clau-valor de cada producte. A eixe array li fem un map per a retornar un array de productes on el ID és la clau del producte. Com que el ID no cal guardar-lo en la base de dades, el llevem en la funció update() abans d'enviar amb la funció PUT.

En Firebase es poden crear entrades en la base de dades en POST i PUT. Es recomana crear en POST i utilitzar PUT sols per a modificar. Al crear en POST, Firebase crearà una clau única.

La funció més complicada és la de setNotificarCambios. Aquesta rep un callback i l'assigna a la funció 'onCambioItems del servici. D'aquesta manera el servici no sap en temps de programació què fer quan acabe de demanar coses al servidor. Serà el controlador el que li diga què fer. En aquest cas serà una funció de la vista, com veurem després.

El servici de productes no necessita res especial que no tinga la classe genèrica, així que sols implementarà l'herència:

import { Product } from "../models/model_product.js";
import { Service } from "./service.js"
export { ProductService }

class ProductService extends Service {
	constructor() {
		super(`${app.url}${Product.nombre}`,Product);
	
	}
}

Com es veu, afegim l'atribut estàtic nombre per a tindre centralitzat el nom en l'API i la classe Product per a que el servici puga crear productes a partir de les dades.

Vista

La vista sols s'encarrega de mostrar les dades i d'informar de les interaccions de l'usuari. Les interaccions que suposen un canvi en les dades o una petició al servidor no seran ateses per la vista. No obstant la vista sí pot atendre alguns esdeveniments relacionats amb la interfície.

La vista tindrà algun mecanisme de notificacions cap al controlador. En el nostre cas, en principi el que farem és associar funcions del controlador a esdeveniments. D'aquesta manera la vista detecta l'esdeveniment i executa el callback que el controlador li ha manat.

Per una altra banda, la vista ha de ser notificada dels canvis de les dades en el model-servici o el controlador. Això serà amb funcions públiques que seran invocades pel controlador.

La classe vista genèrica té funcions buides per a ser heretades o sobreescrites que implementen el CRUD. La vista no s'encarrega de crear o llegir, però té botons que ho demanen i cal donar-los funcionalitat.

export { View }

class View {
    Items = []
    constructor(container) {
        this.container = container;
        this.divRow = document.createElement('div');
    }
    mostrarItems(Items) {
        this.Items = Items;
        this.container.innerHTML = '';
        this.divRow.innerHTML = '';
        this.divRow.classList.add('row', 'row-cols-1', 'row-cols-md-3', 'g-4');
        this.container.append(this.divRow);
        for (let key of Items) {
            this.render(key);
        }
        this.mostrarFormulario();
    }
    render(Item) {  // Esta funció serà sobreescrita per cada vista

    }

    removeItem(Item) {
        console.log(Item);  // al fer el bind, aquesta funció es sobreescriu
    }

    updateItem(Item) {
        console.log(Item); 
    }

    construirFormulario(Item, divItem) { } // Este és per a crear un formulari en un producte concret en un div concret

    mostrarFormulario() { // Este és per a mostrar el formulari de creació, crida internament a construirFormulario
    }


    editItem(Item, divItem) { // funció base que crea el formular i cancelar. El botó enviar es tindrà que escriurer
        divItem.innerHTML = '';
        this.construirFormulario(Item, divItem);
        let botonCancelar = document.createElement('button');
        botonCancelar.classList.add('btn', 'btn-danger');
        botonCancelar.innerHTML = 'Cancelar';
        divItem.append(botonCancelar);
        let botonEnviar = document.createElement('button');
        botonEnviar.classList.add('btn', 'btn-success');
        botonEnviar.innerHTML = 'Enviar';
        divItem.append(botonEnviar);
        botonCancelar.addEventListener('click', () => {
            this.mostrarItems(this.Items); // cal refrescar els items
        });
        botonEnviar.addEventListener('click', () => {
            this.updateItemEnviar(Item,divItem);
        });
      
    }

    updateItemEnviar(Item,divItem){
        console.log(divItem); // esta funció es fa per separat per poder sobreescriure en la vista específica
    }


    botonEnviar = document.createElement('button');  // Necessari per associar events abans de crear la vista
}

La vista específica dels productes queda així:

import { View } from "./view.js"

export { ProductView };

class ProductView extends View {
  constructor(container) { super(container); }

  render(producto) {
    // console.log(producto);
    let divProducto = document.createElement('div');
    divProducto.classList.add('col');
    divProducto.innerHTML = `
        <div class="card position-relative">
        <img src="${producto.foto}" class="card-img-top" alt="${producto.referencia}">
        <div class="card-body">
          <h5 class="card-title">${producto.marca}  ${producto.referencia}</h5>
          <p class="card-text">${producto.precio}</p>
        </div>
<div class="position-absolute top-0 end-0">
<button type="button" class="btn btn-secondary  edit">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-square" viewBox="0 0 16 16">
  <path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/>
  <path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5v11z"/>
</svg>
</button>

        <button type="button" class="btn btn-secondary  delete">
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"></path>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"></path>
</svg>
      </button>
      </div>
      
      </div>
        `;
    // El botó d'esborrat crida a removeItem que ha sigut sobreescrit per binRemoveProduct a la funció del controlador
    // que crida a la funció del servici
    divProducto.querySelector('button.delete').addEventListener('click',() => this.removeItem(producto));
    divProducto.querySelector('button.edit').addEventListener('click',() => this.editItem(producto,divProducto));

    this.divRow.append(divProducto);

  }

  construirFormulario(producto, divProducto){
    if (producto == undefined) { producto = {id:'',marca:'',referencia:'',precio:'', foto:''} }

    let formulario = `<div class="mb-3">
    <label for="formMarca" class="form-label">Marca</label>
    <input type="text" class="form-control" id="formMarca" placeholder="Marca" value="${producto.marca}">
  </div>
  <div class="mb-3">
    <label for="formReferencia" class="form-label">Rerefencia</label>
    <input type="text" class="form-control" id="formReferencia" placeholder="Referencia" value="${producto.referencia}">
  </div>
  <div class="mb-3">
    <label for="formPrecio" class="form-label">Precio</label>
    <input type="text" class="form-control" id="formPrecio" placeholder="Precio" value="${producto.precio}">
  </div>
  <div class="mb-3">
    <label for="formFoto" class="form-label">Foto</label>
    <input type="file" class="form-control" id="formFoto" placeholder="Foto">
    <img class="formFotoPreview" style="width:200px"/>
  </div>`
  
  divProducto.innerHTML=formulario;

  divProducto.querySelector('#formFoto').foto = producto.foto;  // per a afegir l'atribut foto a l'input
  divProducto.querySelector('.formFotoPreview').src=producto.foto;

  divProducto.querySelector('#formFoto').addEventListener('change', function(){
    let file = this.files[0];
    let reader = new FileReader();
    reader.onloadend = () => { 
      this.foto = reader.result; 
      divProducto.querySelector('.formFotoPreview').src=this.foto;
    }  // this.foto es guarda en el input
    reader.readAsDataURL(file);
  });
  }

  mostrarFormulario() {
    let divFormulario = document.createElement('div');
    divFormulario.classList.add('col');
    let botonNuevo = document.createElement('button');
    botonNuevo.classList.add('btn', 'btn-primary');
    botonNuevo.innerHTML = 'Nuevo';
    divFormulario.append(botonNuevo);
    let formularioProducto = document.createElement('div');
  
    this.construirFormulario(null,formularioProducto);
    formularioProducto.style.display = 'none';


    let botonCancelar = document.createElement('button');
    botonCancelar.classList.add('btn', 'btn-danger');
    botonCancelar.innerHTML = 'Cancelar';
    divFormulario.append(botonCancelar);
    botonCancelar.style.display = 'none';

    botonNuevo.addEventListener('click', function () {
      botonCancelar.style.display = '';
      formularioProducto.style.display = '';
      this.style.display = 'none';
    });

    botonCancelar.addEventListener('click', function () {
      botonNuevo.style.display = '';
      formularioProducto.style.display = 'none';
      this.style.display = 'none';
    });

    divFormulario.append(formularioProducto);
    this.formularioProducto = formularioProducto;  // Per comoditat he fet una variable de la funció però després ho assigne a un atribut

    this.botonEnviar.classList.add('btn', 'btn-success');
    this.botonEnviar.innerHTML = 'Enviar';
    formularioProducto.append(this.botonEnviar);

    this.divRow.append(divFormulario);
  }





  bindAddProduct(handler){
    this.botonEnviar.addEventListener('click', ()=>{
      let marca = this.formularioProducto.querySelector('#formMarca').value;
      let referencia = this.formularioProducto.querySelector('#formReferencia').value;
      let precio = this.formularioProducto.querySelector('#formPrecio').value;
      let foto = this.formularioProducto.querySelector('#formFoto').foto;

      handler({marca, referencia, precio, foto});  // La vista sols crea un objecte que envia al manejador
      // El controlador assignarà un manejador per a que el servici cree el producte
    });
  }


  bindRemoveProduct(handler){
    this.removeItem = handler;
  }

  bindEditProduct(handler){
    this.updateItem = handler;  // associem l'edició del producte a la funció que diga el controlador
    // aquesta rebrà el producte a modificar
  }

  updateItemEnviar(Item,divItem){
    
            Item.marca = divItem.querySelector('#formMarca').value;
            Item.referencia = divItem.querySelector('#formReferencia').value;
            Item.precio = divItem.querySelector('#formPrecio').value;
            Item.foto = divItem.querySelector('#formFoto').foto;
            this.updateItem(Item); // cal refrescar els items
}

}

La part més interessant és la de les funcions bind... en les que s'associa un manejador d'esdeveniment handler als esdeveniments que provoca la vista. Com que la vista no sap qué fer en les dades, delega en el servici. Aquestes funcions seran cridades pel controlador, el qual cridarà al servici.

Controlador

La construcció del controlador cridarà als constructors de la vista i del model-servici. El controlador té com a funció bàsica connectar la vista en el model i, si cal, fer la lògica del negoci en mig. En el nostre exemple, el controlador serà molt lleuger però imprescindible. El constructor del controlador relacionarà els esdeveniments del model i la vista entre ells. Per fer-ho més senzill d'entendre, es crearan unes funcions intermedies que seran enviades com a callback al model o a la vista.

La classe genèrica del controlador és molt simple:

export { Controller }

    class Controller {
        constructor(service,view) {
            this.service = service;
            this.view = view;
        }
    }

Sols necessita que li passem un servici (model) i una vista. Cada controlador específic ja podrà relacionar el servici el la vista.

El controlador dels productes serà:

 
 import { Controller } from "../controllers/controller.js";
 export { ProductController }
 
 //// Els controladors 
/*
El controlador té una serie de funcios handle que diuen cóm reaccionar a un esdeveniment de la vista.
Generalment será demanar alguna cosa al servici.

En el contructor, relacionem els esdeveniments de la vista o del servici al servici o la vista respectivament

Per exemple: Quan el servici ja ha carregat les dades, sap quina funció callback ha d'executar perquè el controlador en el
seu constructor li ha dit quina és. 
Per una altra banda, quan la vista vol crear un producte, sap qué funció del controlador executar perquè ho hem associat. Aquesta 
funció crida a una altra del servici. 

D'aquesta manera, la vista i els servici no es relacionen dirèctament i pot ser més fàcil de programar per separat.

*/ 
    class ProductController extends Controller {
        constructor(service,view){ 
            super(service,view);
            this.service.setNotificarCambios(this.onCambioItems); // Associar la funció de la vista amb el service
            this.view.bindAddProduct(this.handleAddProduct); // Quan en la vista li donem a crear
            this.view.bindRemoveProduct(this.handleremoveProduct);
            this.view.bindEditProduct(this.handleUpdateProduct);
        }
        onCambioItems = Items => {  // es te que fer el fletxa per a que agafe el this de la classe
            this.view.mostrarItems(Items);
        }
        handleAddProduct = (product) =>{
            this.service.add(product);
        }
        handleremoveProduct = (product) => {
            this.service.remove(product.id);
        }
        handleUpdateProduct = (product) =>{
            this.service.update(product);
        }
    }

Com es veu, les funcions no fan res més que cridar a la vista o al servici. Però podrien fer alguna cosa en les dades en mig.

Pàgines

Com que estem fent una SPA, definirem les pàgines també en Javascript. Esta és la classe base de les pàgines:

export { Page };

class Page {    ///////// Cada una de les pàgines de la web
    constructor(name){ this.name = name; }
    populate(container){
        
    }
}

Així quedarà la pàgina de productes:

import { Page } from "./page.js"
import { ProductController } from "../controllers/product_controller.js"
import { Product } from "../models/model_product.js";
import { ProductView } from "../views/product_view.js";
import { ProductService } from "../services/product_service.js";
export { PageProductos };

class PageProductos extends Page {

    constructor(name){
        super(name);
    }

    populate(container){
        container.innerHTML = `<h1>Productos</h1>`
        // sols creant el controlador ja invoques a la creació del servici/model i de la vista
        let productController = new ProductController(new ProductService(), new ProductView(container));
    }
}

Per a cridar a aquesta pàgina, en el programa principal la crearem un cridarem a populate():

...
app.productos = new PageProductos('Productos');
...
app.productos.populate(app.container);
...


Enllaç al repositori amb el codi possiblement més actualitzat: https://github.com/xxjcaxx/dwec-2022/tree/master/mvcbenfet


Altra documentació: https://github.com/Caballerog/VanillaJS-MVC-Users

https://www.natapuntes.es/patron-mvc-en-vanilla-javascript/

https://github.com/Fictizia/Curso-JS-Avanzado-para-desarrolladores-Front-end_ed3/blob/master/teoria/clase18.md

https://github.com/Fictizia/Curso-JS-Avanzado-para-desarrolladores-Front-end_ed3