Diferencia entre revisiones de «CRUD en Javascript i Firebase»

De Jose Castillo Aliaga
Ir a la navegación Ir a la búsqueda
Línea 256: Línea 256:


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.
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.
<syntaxhighlight lang="javascript" style="font-family:monospace">
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
}
</syntaxhighlight>
La vista específica dels productes queda així:
<syntaxhighlight lang="javascript" style="font-family:monospace">
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
}
}
</syntaxhighlight>
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 ===
=== Controlador ===

Revisión del 09:24 24 may 2021

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.

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.

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.



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